@@ -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+ }
0 commit comments