Skip to content

Commit 28434b1

Browse files
authored
Merge pull request #190 from pushchain/feat/uea-migration-upgrade
feat: uea migration changes
2 parents 6a0f4a5 + b7ddf2e commit 28434b1

17 files changed

+269
-100
lines changed

app/upgrades.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/pushchain/push-chain-node/app/upgrades"
99
aiauditfixes "github.com/pushchain/push-chain-node/app/upgrades/ai-audit-fixes"
1010
aiauditfixes2 "github.com/pushchain/push-chain-node/app/upgrades/ai-audit-fixes-2"
11+
ueamigration "github.com/pushchain/push-chain-node/app/upgrades/uea-migration"
1112
ceagasandpayload "github.com/pushchain/push-chain-node/app/upgrades/cea-gas-and-payload"
1213
ceapayloadverificationfix "github.com/pushchain/push-chain-node/app/upgrades/cea-payload-verification-fix"
1314
chainmeta "github.com/pushchain/push-chain-node/app/upgrades/chain-meta"
@@ -55,6 +56,7 @@ var Upgrades = []upgrades.Upgrade{
5556
ceapayloadverificationfix.NewUpgrade(),
5657
aiauditfixes.NewUpgrade(),
5758
aiauditfixes2.NewUpgrade(),
59+
ueamigration.NewUpgrade(),
5860
}
5961

6062
// RegisterUpgradeHandlers registers the chain upgrade handlers
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package ueamigration
2+
3+
import (
4+
"context"
5+
6+
storetypes "cosmossdk.io/store/types"
7+
upgradetypes "cosmossdk.io/x/upgrade/types"
8+
9+
"github.com/cosmos/cosmos-sdk/types/module"
10+
11+
"github.com/pushchain/push-chain-node/app/upgrades"
12+
)
13+
14+
const UpgradeName = "uea-migration"
15+
16+
func NewUpgrade() upgrades.Upgrade {
17+
return upgrades.Upgrade{
18+
UpgradeName: UpgradeName,
19+
CreateUpgradeHandler: CreateUpgradeHandler,
20+
StoreUpgrades: storetypes.StoreUpgrades{
21+
Added: []string{},
22+
Deleted: []string{},
23+
},
24+
}
25+
}
26+
27+
func CreateUpgradeHandler(
28+
mm upgrades.ModuleManager,
29+
configurator module.Configurator,
30+
ak *upgrades.AppKeepers,
31+
) upgradetypes.UpgradeHandler {
32+
return func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
33+
return mm.RunMigrations(ctx, configurator, fromVM)
34+
}
35+
}

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

test/integration/uexecutor/inbound_initiated_outbound_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ func setupInboundInitiatedOutboundTest(t *testing.T, numVals int) (*app.ChainApp
116116

117117
ueModuleAccAddress, _ := app.UexecutorKeeper.GetUeModuleAddress(ctx)
118118
receipt, err := app.UexecutorKeeper.DeployUEAV2(ctx, ueModuleAccAddress, validUA)
119-
ueaAddrHex := common.BytesToAddress(receipt.Ret)
120119
require.NoError(t, err)
120+
ueaAddrHex := common.BytesToAddress(receipt.Ret)
121121

122122
// signature
123123
validVerificationData := "0xa7531ada733322bd6708c94cba5a7dbd1ce25bccf010f774777b039713fc330643e23b7ef2a4609244900c6ab9a03d83d3ecf73edf6b451f21cc7dbda625a3211b"

test/integration/uexecutor/vote_inbound_validation_test.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,16 @@ func TestVoteInboundValidation(t *testing.T) {
123123
t.Run("inbound with invalid payload records failed PCTx", func(t *testing.T) {
124124
chainApp, ctx, vals, coreVals, _ := setupInboundValidationTest(t, 4)
125125

126-
// Construct a FUNDS_AND_PAYLOAD inbound with a malformed UniversalPayload:
127-
// empty "to" encodes as zero address in RawPayload, passes ValidateForExecution,
128-
// but fails at EVM execution level. The failure is recorded as a failed PCTx.
126+
// Construct a FUNDS_AND_PAYLOAD inbound whose payload will revert at EVM execution.
127+
// The UEA's executeUniversalTx skips verification for module-sender calls, so we
128+
// must trigger a revert in the execution step itself.
129+
// Strategy: call the Handler contract with an invalid function selector — the
130+
// Handler has no fallback, so the low-level .call() returns success=false and
131+
// the UEA reverts with ExecutionFailed().
129132
malformedPayload := &uexecutortypes.UniversalPayload{
130-
To: "", // empty "to" address -- fails at EVM execution
133+
To: utils.GetDefaultAddresses().HandlerAddr.Hex(), // real contract, no fallback
131134
Value: "0",
132-
Data: "0x",
135+
Data: "0xdeadbeef", // invalid selector — no matching function
133136
GasLimit: "21000000",
134137
MaxFeePerGas: "1000000000",
135138
MaxPriorityFeePerGas: "200000000",

test/utils/bytecode.go

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

x/uexecutor/keeper/evm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ func (k Keeper) CallUEAExecutePayload(
184184
false, // gasless = false (@dev: we need gas to be emitted in the tx receipt)
185185
false, // not a module sender
186186
nil,
187-
"executePayload",
187+
"executeUniversalTx",
188188
abiUniversalPayload,
189189
verificationData,
190190
)

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 & 37 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,44 +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 nil, err
41-
}
42-
43-
gasUnitsUsed := receipt.GasUsed
44-
gasUnitsUsedBig := new(big.Int).SetUint64(gasUnitsUsed)
45-
46-
k.Logger().Debug("payload executed via UEA",
47-
"uea", ueaAddr.Hex(),
48-
"tx_hash", receipt.Hash,
49-
"gas_used", gasUnitsUsed,
50-
)
36+
receipt, execErr := k.CallUEAExecutePayload(sdkCtx, evmFrom, ueaAddr, universalPayload, verificationDataVal)
5137

52-
// Step 3: Handle fee calculation and deduction
53-
ueaAccAddr := sdk.AccAddress(ueaAddr.Bytes())
54-
55-
baseFee := k.feemarketKeeper.GetBaseFee(sdkCtx)
56-
if baseFee.IsNil() {
57-
return nil, errors.Wrapf(sdkErrors.ErrLogic, "base fee not found")
58-
}
59-
60-
gasCost, err := k.CalculateGasCost(baseFee, payload.MaxFeePerGas, payload.MaxPriorityFeePerGas, gasUnitsUsed)
61-
if err != nil {
62-
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)
6343
}
6444

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

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

7855
return receipt, nil

0 commit comments

Comments
 (0)