Skip to content

Commit b7ddf2e

Browse files
committed
feat: added fee deduction in CEA -> smart contract route
1 parent bb362f9 commit b7ddf2e

10 files changed

Lines changed: 213 additions & 89 deletions

test/integration/uexecutor/inbound_cea_smart_contract_test.go

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,13 @@ func TestInboundCEASmartContractRecipient(t *testing.T) {
260260
})
261261

262262
t.Run("executeUniversalTx PCTx is recorded for smart contract recipient", func(t *testing.T) {
263-
chainApp, ctx, vals, inbound, coreVals, _ := setupInboundCEASmartContractTest(t, 4)
263+
chainApp, ctx, vals, inbound, coreVals, contractAddr := setupInboundCEASmartContractTest(t, 4)
264+
265+
// Fund the smart contract with upc so gas fee deduction succeeds
266+
contractAccAddr := sdk.AccAddress(contractAddr.Bytes())
267+
fundCoins := sdk.NewCoins(sdk.NewInt64Coin("upc", 1_000_000_000))
268+
require.NoError(t, chainApp.BankKeeper.MintCoins(ctx, "mint", fundCoins))
269+
require.NoError(t, chainApp.BankKeeper.SendCoinsFromModuleToAccount(ctx, "mint", contractAccAddr, fundCoins))
264270

265271
for i := 0; i < 3; i++ {
266272
valAddr, err := sdk.ValAddressFromBech32(coreVals[i].OperatorAddress)
@@ -283,13 +289,51 @@ func TestInboundCEASmartContractRecipient(t *testing.T) {
283289
require.Empty(t, callPcTx.ErrorMsg)
284290
})
285291

286-
t.Run("EOA recipient receives deposit and executeUniversalTx call", func(t *testing.T) {
292+
t.Run("gas fees deducted from smart contract recipient after executeUniversalTx", func(t *testing.T) {
293+
chainApp, ctx, vals, inbound, coreVals, contractAddr := setupInboundCEASmartContractTest(t, 4)
294+
295+
// Fund the smart contract with upc so fee deduction can succeed
296+
contractAccAddr := sdk.AccAddress(contractAddr.Bytes())
297+
fundCoins := sdk.NewCoins(sdk.NewInt64Coin("upc", 1_000_000_000))
298+
require.NoError(t, chainApp.BankKeeper.MintCoins(ctx, "mint", fundCoins))
299+
require.NoError(t, chainApp.BankKeeper.SendCoinsFromModuleToAccount(ctx, "mint", contractAccAddr, fundCoins))
300+
301+
balanceBefore := chainApp.BankKeeper.GetBalance(ctx, contractAccAddr, "upc")
302+
303+
// Reach quorum
304+
for i := 0; i < 3; i++ {
305+
valAddr, err := sdk.ValAddressFromBech32(coreVals[i].OperatorAddress)
306+
require.NoError(t, err)
307+
coreValAcc := sdk.AccAddress(valAddr).String()
308+
309+
err = utils.ExecVoteInbound(t, ctx, chainApp, vals[i], coreValAcc, inbound)
310+
require.NoError(t, err)
311+
}
312+
313+
// Verify executeUniversalTx PCTx has gas_used > 0
314+
utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound)
315+
utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKey)
316+
require.NoError(t, err)
317+
require.True(t, found)
318+
require.GreaterOrEqual(t, len(utx.PcTx), 2, "should have deposit + executeUniversalTx PCTxs")
319+
320+
callPcTx := utx.PcTx[1]
321+
require.Equal(t, "SUCCESS", callPcTx.Status)
322+
require.Greater(t, callPcTx.GasUsed, uint64(0), "executeUniversalTx should report gas used")
323+
324+
// Verify upc balance decreased (gas was deducted)
325+
balanceAfter := chainApp.BankKeeper.GetBalance(ctx, contractAccAddr, "upc")
326+
require.True(t, balanceAfter.Amount.LT(balanceBefore.Amount),
327+
"smart contract upc balance should decrease after gas fee deduction (before=%s, after=%s)",
328+
balanceBefore.Amount, balanceAfter.Amount)
329+
})
330+
331+
t.Run("EOA recipient receives deposit only, no executeUniversalTx", func(t *testing.T) {
287332
chainApp, ctx, vals, _, coreVals, _ := setupInboundCEASmartContractTest(t, 4)
288333
usdcAddress := utils.GetDefaultAddresses().ExternalUSDCAddr
289334
testAddress := utils.GetDefaultAddresses().DefaultTestAddr
290335

291-
// TargetAddr2 is a plain EOA — deposit lands there and executeUniversalTx is called
292-
// (calling to an EOA in EVM succeeds with empty output)
336+
// TargetAddr2 is a plain EOA (no contract code deployed)
293337
eoaRecipient := utils.GetDefaultAddresses().TargetAddr2
294338

295339
validUP := &uexecutortypes.UniversalPayload{
@@ -335,8 +379,15 @@ func TestInboundCEASmartContractRecipient(t *testing.T) {
335379
require.NoError(t, err)
336380
require.True(t, found)
337381

338-
// Expect 2 PCTxs: deposit + executeUniversalTx
339-
require.GreaterOrEqual(t, len(utx.PcTx), 2, "should have deposit and executeUniversalTx PCTxs")
382+
// EOA recipient: deposit PCTx only, no executeUniversalTx PCTx
383+
// (code size check skips executeUniversalTx for EOAs, but payload execution via UEA may still run)
384+
require.GreaterOrEqual(t, len(utx.PcTx), 1, "should have at least deposit PCTx for EOA recipient")
385+
386+
// Verify no executeUniversalTx-specific PCTx (the smart contract call path is skipped)
387+
// The deposit PCTx should succeed
388+
require.Equal(t, "SUCCESS", utx.PcTx[0].Status, "deposit should succeed for EOA recipient")
389+
390+
// No outbound should be created from executeUniversalTx (which was skipped)
340391
require.Equal(t, "SUCCESS", utx.PcTx[0].Status, "deposit should succeed for EOA recipient")
341392

342393
// isCEA path never creates a revert outbound

x/uexecutor/keeper/execute_inbound_funds_and_payload.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,13 @@ func (k Keeper) ExecuteInboundFundsAndPayload(ctx context.Context, utx types.Uni
8585
}
8686
}
8787
} else {
88-
// Non-UEA path (smart contract or EOA): deposit PRC20 and call executeUniversalTx
89-
isSmartContract = true
88+
// Non-UEA: check if recipient has code (smart contract) vs EOA
89+
codeHash := k.evmKeeper.GetCodeHash(sdkCtx, ueaAddr)
90+
if codeHash != types.EmptyCodeHash && codeHash != (common.Hash{}) {
91+
// Smart contract: will call executeUniversalTx after deposit
92+
isSmartContract = true
93+
}
94+
// EOA: just deposit, skip executeUniversalTx (no contract to call)
9095
if inboundAmount.Sign() > 0 {
9196
receipt, execErr = k.depositPRC20(
9297
sdkCtx,
@@ -262,12 +267,20 @@ func (k Keeper) ExecuteInboundFundsAndPayload(ctx context.Context, utx types.Uni
262267
BlockHeight: uint64(sdkCtx.BlockHeight()),
263268
Status: "FAILED",
264269
}
270+
if contractReceipt != nil {
271+
callPcTx.TxHash = contractReceipt.Hash
272+
callPcTx.GasUsed = contractReceipt.GasUsed
273+
}
265274
if contractErr != nil {
266275
callPcTx.ErrorMsg = contractErr.Error()
267276
} else {
268-
callPcTx.TxHash = contractReceipt.Hash
269-
callPcTx.GasUsed = contractReceipt.GasUsed
270-
callPcTx.Status = "SUCCESS"
277+
// Deduct gas fees from the recipient contract address
278+
if feeErr := k.DeductGasFeesFromReceipt(ctx, sdkCtx, ueaAddr, contractReceipt, utx.InboundTx.UniversalPayload); feeErr != nil {
279+
callPcTx.Status = "FAILED"
280+
callPcTx.ErrorMsg = fmt.Sprintf("gas fee deduction failed: %s", feeErr.Error())
281+
} else {
282+
callPcTx.Status = "SUCCESS"
283+
}
271284
}
272285
if updateErr := k.UpdateUniversalTx(ctx, universalTxKey, func(utx *types.UniversalTx) error {
273286
utx.PcTx = append(utx.PcTx, &callPcTx)

x/uexecutor/keeper/execute_inbound_gas_and_payload.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,12 @@ func (k Keeper) ExecuteInboundGasAndPayload(ctx context.Context, utx types.Unive
8787
}
8888
}
8989
} else {
90-
// Non-UEA path (smart contract or EOA): deposit + autoswap and call executeUniversalTx
91-
isSmartContract = true
90+
// Non-UEA: check if recipient has code (smart contract) vs EOA
91+
codeHash := k.evmKeeper.GetCodeHash(sdkCtx, ueaAddr)
92+
if codeHash != types.EmptyCodeHash && codeHash != (common.Hash{}) {
93+
isSmartContract = true
94+
}
95+
// EOA: just deposit, skip executeUniversalTx
9296
if amount.Sign() > 0 {
9397
prc20AddrHex := common.HexToAddress(tokenConfig.NativeRepresentation.ContractAddress)
9498
receipt, execErr = k.gasAndPayloadDepositAutoSwap(sdkCtx, prc20AddrHex, ueaAddr, amount)
@@ -261,12 +265,20 @@ func (k Keeper) ExecuteInboundGasAndPayload(ctx context.Context, utx types.Unive
261265
BlockHeight: uint64(sdkCtx.BlockHeight()),
262266
Status: "FAILED",
263267
}
268+
if contractReceipt != nil {
269+
callPcTx.TxHash = contractReceipt.Hash
270+
callPcTx.GasUsed = contractReceipt.GasUsed
271+
}
264272
if contractErr != nil {
265273
callPcTx.ErrorMsg = contractErr.Error()
266274
} else if contractReceipt != nil {
267-
callPcTx.TxHash = contractReceipt.Hash
268-
callPcTx.GasUsed = contractReceipt.GasUsed
269-
callPcTx.Status = "SUCCESS"
275+
// Deduct gas fees from the recipient contract address
276+
if feeErr := k.DeductGasFeesFromReceipt(ctx, sdkCtx, ueaAddr, contractReceipt, utx.InboundTx.UniversalPayload); feeErr != nil {
277+
callPcTx.Status = "FAILED"
278+
callPcTx.ErrorMsg = fmt.Sprintf("gas fee deduction failed: %s", feeErr.Error())
279+
} else {
280+
callPcTx.Status = "SUCCESS"
281+
}
270282
}
271283
if updateErr := k.UpdateUniversalTx(ctx, universalTxKey, func(utx *types.UniversalTx) error {
272284
utx.PcTx = append(utx.PcTx, &callPcTx)

x/uexecutor/keeper/execute_payload.go

Lines changed: 14 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ package keeper
22

33
import (
44
"context"
5-
"math/big"
5+
"fmt"
66

77
"cosmossdk.io/errors"
88
sdk "github.com/cosmos/cosmos-sdk/types"
9-
sdkErrors "github.com/cosmos/cosmos-sdk/types/errors"
109
vmtypes "github.com/cosmos/evm/x/vm/types"
1110
"github.com/ethereum/go-ethereum/common"
1211
"github.com/pushchain/push-chain-node/utils"
@@ -23,9 +22,8 @@ func (k Keeper) ExecutePayloadV2(ctx context.Context, evmFrom common.Address, ue
2322
"from", evmFrom.Hex(),
2423
)
2524

26-
// Step 1: Parse and validate payload and verificationData
27-
payload, err := types.NewAbiUniversalPayload(universalPayload)
28-
if err != nil {
25+
// Step 1: Validate payload and verificationData early (fast-fail before EVM work)
26+
if _, err := types.NewAbiUniversalPayload(universalPayload); err != nil {
2927
return nil, errors.Wrapf(err, "invalid universal payload")
3028
}
3129

@@ -35,46 +33,23 @@ func (k Keeper) ExecutePayloadV2(ctx context.Context, evmFrom common.Address, ue
3533
}
3634

3735
// Step 2: Execute payload through UEA
38-
receipt, err := k.CallUEAExecutePayload(sdkCtx, evmFrom, ueaAddr, universalPayload, verificationDataVal)
39-
if err != nil {
40-
// Return receipt even on EVM revert so callers can capture the tx hash for debugging.
41-
// DerivedEVMCall returns (receipt, error) on revert -- receipt.Hash is valid.
42-
return receipt, err
43-
}
44-
45-
gasUnitsUsed := receipt.GasUsed
46-
gasUnitsUsedBig := new(big.Int).SetUint64(gasUnitsUsed)
47-
48-
k.Logger().Debug("payload executed via UEA",
49-
"uea", ueaAddr.Hex(),
50-
"tx_hash", receipt.Hash,
51-
"gas_used", gasUnitsUsed,
52-
)
36+
receipt, execErr := k.CallUEAExecutePayload(sdkCtx, evmFrom, ueaAddr, universalPayload, verificationDataVal)
5337

54-
// Step 3: Handle fee calculation and deduction
55-
ueaAccAddr := sdk.AccAddress(ueaAddr.Bytes())
56-
57-
baseFee := k.feemarketKeeper.GetBaseFee(sdkCtx)
58-
if baseFee.IsNil() {
59-
return nil, errors.Wrapf(sdkErrors.ErrLogic, "base fee not found")
60-
}
61-
62-
gasCost, err := k.CalculateGasCost(baseFee, payload.MaxFeePerGas, payload.MaxPriorityFeePerGas, gasUnitsUsed)
63-
if err != nil {
64-
return nil, errors.Wrapf(err, "failed to calculate gas cost")
38+
// Step 3: Deduct gas fees regardless of success/failure.
39+
// If deduction fails, return error so the caller records a FAILED PCTx.
40+
// The receipt is still returned so callers can capture the tx hash.
41+
if feeErr := k.DeductGasFeesFromReceipt(ctx, sdkCtx, ueaAddr, receipt, universalPayload); feeErr != nil {
42+
return receipt, fmt.Errorf("gas fee deduction failed: %w", feeErr)
6543
}
6644

67-
if gasUnitsUsedBig.Cmp(payload.GasLimit) > 0 {
68-
return nil, errors.Wrapf(sdkErrors.ErrOutOfGas, "gas cost (%d) exceeds limit (%d)", gasCost, payload.GasLimit)
45+
if execErr != nil {
46+
return receipt, execErr
6947
}
7048

71-
if err = k.DeductAndBurnFees(ctx, ueaAccAddr, gasCost); err != nil {
72-
return nil, errors.Wrapf(err, "failed to deduct fees from %s", ueaAccAddr)
73-
}
74-
75-
k.Logger().Debug("fees deducted for payload execution",
49+
k.Logger().Debug("payload executed via UEA",
7650
"uea", ueaAddr.Hex(),
77-
"gas_cost", gasCost.String(),
51+
"tx_hash", receipt.Hash,
52+
"gas_used", receipt.GasUsed,
7853
)
7954

8055
return receipt, nil

x/uexecutor/keeper/fees.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77

88
sdkmath "cosmossdk.io/math"
99
sdk "github.com/cosmos/cosmos-sdk/types"
10+
evmtypes "github.com/cosmos/evm/x/vm/types"
11+
"github.com/ethereum/go-ethereum/common"
1012
pchaintypes "github.com/pushchain/push-chain-node/types"
1113
"github.com/pushchain/push-chain-node/x/uexecutor/types"
1214
)
@@ -87,3 +89,60 @@ func (k Keeper) CalculateGasCost(
8789

8890
return gasCost, nil
8991
}
92+
93+
// DeductGasFeesFromReceipt calculates and deducts gas fees from a recipient address
94+
// based on the EVM receipt and universal payload parameters.
95+
// Returns nil if receipt is nil (Go-level error, no EVM tx was created).
96+
// Returns error with gas details if deduction fails (insufficient balance, etc).
97+
func (k Keeper) DeductGasFeesFromReceipt(
98+
ctx context.Context,
99+
sdkCtx sdk.Context,
100+
recipient common.Address,
101+
receipt *evmtypes.MsgEthereumTxResponse,
102+
universalPayload *types.UniversalPayload,
103+
) error {
104+
if receipt == nil || receipt.GasUsed == 0 {
105+
return nil
106+
}
107+
if universalPayload == nil {
108+
return nil
109+
}
110+
111+
abiPayload, err := types.NewAbiUniversalPayload(universalPayload)
112+
if err != nil {
113+
return fmt.Errorf("failed to parse payload for gas deduction: %w", err)
114+
}
115+
116+
baseFee := k.feemarketKeeper.GetBaseFee(sdkCtx)
117+
if baseFee.IsNil() {
118+
return fmt.Errorf("base fee not found")
119+
}
120+
121+
gasCost, err := k.CalculateGasCost(baseFee, abiPayload.MaxFeePerGas, abiPayload.MaxPriorityFeePerGas, receipt.GasUsed)
122+
if err != nil {
123+
return fmt.Errorf("failed to calculate gas cost: %w", err)
124+
}
125+
if gasCost.Sign() <= 0 {
126+
return nil
127+
}
128+
129+
gasUsedBig := new(big.Int).SetUint64(receipt.GasUsed)
130+
if gasUsedBig.Cmp(abiPayload.GasLimit) > 0 {
131+
return fmt.Errorf("gas used (%d) exceeds gas limit (%s)", receipt.GasUsed, abiPayload.GasLimit.String())
132+
}
133+
134+
recipientAccAddr := sdk.AccAddress(recipient.Bytes())
135+
balance := k.bankKeeper.GetBalance(sdkCtx, recipientAccAddr, pchaintypes.BaseDenom)
136+
137+
if err := k.DeductAndBurnFees(ctx, recipientAccAddr, gasCost); err != nil {
138+
return fmt.Errorf("insufficient gas: required %s upc, available %s upc, gas_used %d, from %s: %w",
139+
gasCost.String(), balance.Amount.String(), receipt.GasUsed, recipient.Hex(), err)
140+
}
141+
142+
k.Logger().Debug("gas fees deducted",
143+
"recipient", recipient.Hex(),
144+
"gas_used", receipt.GasUsed,
145+
"gas_cost", gasCost.String(),
146+
)
147+
return nil
148+
}

0 commit comments

Comments
 (0)