Skip to content

Commit a773429

Browse files
authored
Merge pull request #205 from pushchain/feat/execute-payload-deploy-uea
feat: execute payload deploys UEA if UEA has non-zero balance
2 parents c84f9a9 + 286fe3b commit a773429

2 files changed

Lines changed: 195 additions & 2 deletions

File tree

test/integration/uexecutor/execute_payload_test.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,177 @@ func TestExecutePayload(t *testing.T) {
172172
})
173173

174174
}
175+
176+
// TestExecutePayload_AutoDeployOnPreFundedAddress exercises the griefing-recovery path:
177+
// when a non-deployed UEA address already holds a non-zero native balance (e.g. because
178+
// an attacker front-ran with a dust deposit to the precomputed address), MsgExecutePayload
179+
// should auto-deploy the UEA before running the payload, instead of rejecting the tx and
180+
// leaving the owner unable to deploy.
181+
func TestExecutePayload_AutoDeployOnPreFundedAddress(t *testing.T) {
182+
app, ctx, _ := utils.SetAppWithValidators(t)
183+
184+
chainConfigTest := uregistrytypes.ChainConfig{
185+
Chain: "eip155:11155111",
186+
VmType: uregistrytypes.VmType_EVM,
187+
PublicRpcUrl: "https://sepolia.drpc.org",
188+
GatewayAddress: "0x28E0F09bE2321c1420Dc60Ee146aACbD68B335Fe",
189+
BlockConfirmation: &uregistrytypes.BlockConfirmation{
190+
FastInbound: 5,
191+
StandardInbound: 12,
192+
},
193+
GatewayMethods: []*uregistrytypes.GatewayMethods{&uregistrytypes.GatewayMethods{
194+
Name: "addFunds",
195+
Identifier: "",
196+
EventIdentifier: "0xb28f49668e7e76dc96d7aabe5b7f63fecfbd1c3574774c05e8204e749fd96fbd",
197+
}},
198+
Enabled: &uregistrytypes.ChainEnabled{
199+
IsInboundEnabled: true,
200+
IsOutboundEnabled: true,
201+
},
202+
}
203+
app.UregistryKeeper.AddChainConfig(ctx, &chainConfigTest)
204+
205+
params := app.FeeMarketKeeper.GetParams(ctx)
206+
params.BaseFee = math.LegacyNewDec(1000000000)
207+
app.FeeMarketKeeper.SetParams(ctx, params)
208+
209+
ms := uexecutorkeeper.NewMsgServerImpl(app.UexecutorKeeper)
210+
211+
// Same fixture as TestExecutePayload/Success! — owner has a pre-signed verificationData
212+
// for this exact payload+nonce, so the execute step can succeed end-to-end.
213+
validUA := &uexecutortypes.UniversalAccountId{
214+
ChainNamespace: "eip155",
215+
ChainId: "11155111",
216+
Owner: "0x778d3206374f8ac265728e18e3fe2ae6b93e4ce4",
217+
}
218+
validUP := &uexecutortypes.UniversalPayload{
219+
To: "0x527F3692F5C53CfA83F7689885995606F93b6164",
220+
Value: "0",
221+
Data: "0x2ba2ed980000000000000000000000000000000000000000000000000000000000000312",
222+
GasLimit: "21000000",
223+
MaxFeePerGas: "1000000000",
224+
MaxPriorityFeePerGas: "200000000",
225+
Nonce: "1",
226+
Deadline: "0",
227+
VType: uexecutortypes.VerificationType(0),
228+
}
229+
230+
evmFrom := common.HexToAddress("0x1000000000000000000000000000000000000001")
231+
err := app.BankKeeper.MintCoins(
232+
ctx,
233+
uexecutortypes.ModuleName,
234+
sdk.NewCoins(sdk.NewCoin(types.BaseDenom, sdkmath.NewInt(2_000_000_000_000_000))),
235+
)
236+
require.NoError(t, err)
237+
238+
err = app.BankKeeper.SendCoinsFromModuleToAccount(
239+
ctx,
240+
uexecutortypes.ModuleName,
241+
sdk.AccAddress(evmFrom.Bytes()),
242+
sdk.NewCoins(sdk.NewCoin(types.BaseDenom, sdkmath.NewInt(1_000_000_000_000_000))),
243+
)
244+
require.NoError(t, err)
245+
246+
// Precompute the UEA address WITHOUT deploying — this is the attacker-grief setup.
247+
factoryAddr := utils.GetDefaultAddresses().FactoryAddr
248+
ueaAddr, isDeployed, err := app.UexecutorKeeper.CallFactoryToGetUEAAddressForOrigin(ctx, evmFrom, factoryAddr, validUA)
249+
require.NoError(t, err)
250+
require.False(t, isDeployed, "precondition: UEA must not be deployed before the test call")
251+
252+
// "Attacker" pre-funds the precomputed UEA address. This is what would confuse a
253+
// balance-based SDK into routing to MsgExecutePayload instead of the deploy msg.
254+
err = app.BankKeeper.SendCoinsFromModuleToAccount(
255+
ctx,
256+
uexecutortypes.ModuleName,
257+
sdk.AccAddress(ueaAddr.Bytes()),
258+
sdk.NewCoins(sdk.NewCoin(types.BaseDenom, sdkmath.NewInt(1_000_000_000_000_000))),
259+
)
260+
require.NoError(t, err)
261+
262+
// Submit MsgExecutePayload directly — no standalone DeployUEAV2 call beforehand.
263+
msg := &uexecutortypes.MsgExecutePayload{
264+
Signer: "cosmos1xpurwdecvsenyvpkxvmnge3cv93nyd34xuersef38pjnxen9xfsk2dnz8yek2drrv56qmn2ak9",
265+
UniversalAccountId: validUA,
266+
UniversalPayload: validUP,
267+
VerificationData: "0x91987784d56359fa91c3e3e0332f4f0cffedf9c081eb12874a63b41d5b5e5c660dc827947c2ae26e658d0551ad4b2d2aa073d62691429a0ae239d2cc58055bf11c",
268+
}
269+
270+
_, err = ms.ExecutePayload(ctx, msg)
271+
require.NoError(t, err, "auto-deploy + execute should succeed when precomputed UEA holds balance")
272+
273+
// Post-condition: the UEA must now be deployed.
274+
_, isDeployed, err = app.UexecutorKeeper.CallFactoryToGetUEAAddressForOrigin(ctx, evmFrom, factoryAddr, validUA)
275+
require.NoError(t, err)
276+
require.True(t, isDeployed, "UEA must be deployed after auto-deploy path runs successfully")
277+
}
278+
279+
// TestExecutePayload_RejectWhenUndeployedAndUnfunded asserts the rejection arm of the
280+
// auto-deploy logic: when the UEA is not deployed AND has zero native balance, there is
281+
// no griefing to recover from, so MsgExecutePayload must still reject with the existing
282+
// "UEA is not deployed" error rather than deploying on-demand for free.
283+
func TestExecutePayload_RejectWhenUndeployedAndUnfunded(t *testing.T) {
284+
app, ctx, _ := utils.SetAppWithValidators(t)
285+
286+
chainConfigTest := uregistrytypes.ChainConfig{
287+
Chain: "eip155:11155111",
288+
VmType: uregistrytypes.VmType_EVM,
289+
PublicRpcUrl: "https://sepolia.drpc.org",
290+
GatewayAddress: "0x28E0F09bE2321c1420Dc60Ee146aACbD68B335Fe",
291+
BlockConfirmation: &uregistrytypes.BlockConfirmation{
292+
FastInbound: 5,
293+
StandardInbound: 12,
294+
},
295+
GatewayMethods: []*uregistrytypes.GatewayMethods{&uregistrytypes.GatewayMethods{
296+
Name: "addFunds",
297+
Identifier: "",
298+
EventIdentifier: "0xb28f49668e7e76dc96d7aabe5b7f63fecfbd1c3574774c05e8204e749fd96fbd",
299+
}},
300+
Enabled: &uregistrytypes.ChainEnabled{
301+
IsInboundEnabled: true,
302+
IsOutboundEnabled: true,
303+
},
304+
}
305+
app.UregistryKeeper.AddChainConfig(ctx, &chainConfigTest)
306+
307+
params := app.FeeMarketKeeper.GetParams(ctx)
308+
params.BaseFee = math.LegacyNewDec(1000000000)
309+
app.FeeMarketKeeper.SetParams(ctx, params)
310+
311+
ms := uexecutorkeeper.NewMsgServerImpl(app.UexecutorKeeper)
312+
313+
// Distinct owner — keeps the UEA address disjoint from any other test fixture and
314+
// ensures neither deploy nor balance exists for this address in fresh state.
315+
validUA := &uexecutortypes.UniversalAccountId{
316+
ChainNamespace: "eip155",
317+
ChainId: "11155111",
318+
Owner: "0x1111111111111111111111111111111111111111",
319+
}
320+
// Payload and verificationData are well-formed (pass early validation) but the
321+
// signature does not need to be valid: the handler must reject at the deploy gate,
322+
// well before signature verification, so we never hit the UEA contract.
323+
validUP := &uexecutortypes.UniversalPayload{
324+
To: "0x527F3692F5C53CfA83F7689885995606F93b6164",
325+
Value: "0",
326+
Data: "0x2ba2ed980000000000000000000000000000000000000000000000000000000000000312",
327+
GasLimit: "21000000",
328+
MaxFeePerGas: "1000000000",
329+
MaxPriorityFeePerGas: "200000000",
330+
Nonce: "1",
331+
Deadline: "0",
332+
VType: uexecutortypes.VerificationType(0),
333+
}
334+
335+
msg := &uexecutortypes.MsgExecutePayload{
336+
Signer: "cosmos1xpurwdecvsenyvpkxvmnge3cv93nyd34xuersef38pjnxen9xfsk2dnz8yek2drrv56qmn2ak9",
337+
UniversalAccountId: validUA,
338+
UniversalPayload: validUP,
339+
VerificationData: "0x1234",
340+
}
341+
342+
_, err := ms.ExecutePayload(ctx, msg)
343+
// "UEA is not deployed" is the gate that fires *before* any auto-deploy attempt.
344+
// Any other error string (e.g. signature-verification revert) would indicate that
345+
// the handler stealth-deployed the UEA and then ran the payload — which must not
346+
// happen when the address has zero balance.
347+
require.ErrorContains(t, err, "UEA is not deployed")
348+
}

x/uexecutor/keeper/msg_execute_payload.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"cosmossdk.io/errors"
88
sdk "github.com/cosmos/cosmos-sdk/types"
99
"github.com/ethereum/go-ethereum/common"
10+
pchaintypes "github.com/pushchain/push-chain-node/types"
1011
"github.com/pushchain/push-chain-node/utils"
1112
"github.com/pushchain/push-chain-node/x/uexecutor/types"
1213
)
@@ -54,8 +55,26 @@ func (k Keeper) ExecutePayload(ctx context.Context, evmFrom common.Address, univ
5455
}
5556

5657
if !isDeployed {
57-
k.Logger().Warn("execute payload rejected: UEA not deployed", "chain", caip2Identifier, "owner", universalAccountId.Owner)
58-
return fmt.Errorf("UEA is not deployed")
58+
// only deploy if the UEA address has funds and not deployed yet
59+
ueaAccAddr := sdk.AccAddress(ueaAddr.Bytes())
60+
balance := k.bankKeeper.GetBalance(sdkCtx, ueaAccAddr, pchaintypes.BaseDenom)
61+
if balance.Amount.Sign() == 0 {
62+
k.Logger().Warn("execute payload rejected: UEA not deployed and has no balance",
63+
"chain", caip2Identifier,
64+
"owner", universalAccountId.Owner,
65+
)
66+
return fmt.Errorf("UEA is not deployed")
67+
}
68+
69+
k.Logger().Info("auto-deploying UEA before execute (pre-funded address)",
70+
"uea", ueaAddr.Hex(),
71+
"balance", balance.Amount.String(),
72+
"chain", caip2Identifier,
73+
"owner", universalAccountId.Owner,
74+
)
75+
if _, err := k.DeployUEAV2(ctx, evmFrom, universalAccountId); err != nil {
76+
return errors.Wrapf(err, "failed to auto-deploy pre-funded UEA")
77+
}
5978
}
6079

6180
k.Logger().Debug("executing payload via UEA",

0 commit comments

Comments
 (0)