@@ -293,7 +293,7 @@ describe('ModuleAuthDynamic Bootstrap Flow', () => {
293293 } )
294294
295295 describe ( 'Bootstrap flow - Negative cases' , ( ) => {
296- it ( 'Should reject first transaction without ImmutableSigner when wallet was not pre-deployed with correct salt' , async ( ) => {
296+ it ( 'Should accept first transaction without ImmutableSigner when image hash matches deployment salt' , async ( ) => {
297297 // Create a random signer that is NOT the ImmutableSigner
298298 const randomSigner = ethers . Wallet . createRandom ( ) . connect ( hardhat . provider )
299299
@@ -332,15 +332,63 @@ describe('ModuleAuthDynamic Bootstrap Flow', () => {
332332 false
333333 )
334334
335- // This should still work because the wallet was deployed with the correct salt
336- // The signature validation will verify against the deployment address
337- // (This tests the existing flow, not the bootstrap flow)
335+ // This should work because the signature's image hash matches the deployment salt
336+ // This is the normal first-tx flow (not the bootstrap flow with ImmutableSigner)
338337 await wallet . execute ( [ transaction ] , nonce , signature )
339338 expect ( await wallet . nonce ( ) ) . to . equal ( 1 )
340339 } )
341340
342- it ( 'Should reject first transaction with wrong image hash even with ImmutableSigner' , async ( ) => {
343- // Deploy wallet with specific signers
341+ it ( 'Should reject first transaction without ImmutableSigner when image hash does not match deployment salt' , async ( ) => {
342+ // Create two different random signers
343+ const deploymentSigner = ethers . Wallet . createRandom ( ) . connect ( hardhat . provider )
344+ const attackerSigner = ethers . Wallet . createRandom ( ) . connect ( hardhat . provider )
345+
346+ // Deploy wallet with deploymentSigner
347+ const walletSalt = encodeImageHash ( 1 , [
348+ { weight : 1 , address : deploymentSigner . address }
349+ ] )
350+ const walletAddress = addressOf ( factory . address , startupWallet . address , walletSalt )
351+ await factory . connect ( walletDeployerEOA ) . deploy ( startupWallet . address , walletSalt )
352+
353+ const wallet = MainModule__factory . connect ( walletAddress , relayerEOA )
354+
355+ // Transfer funds
356+ await relayerEOA . sendTransaction ( { to : walletAddress , value : 1 } )
357+
358+ const transaction = {
359+ delegateCall : false ,
360+ revertOnError : true ,
361+ gasLimit : 1000000 ,
362+ target : await randomEOA . getAddress ( ) ,
363+ value : 1 ,
364+ data : [ ]
365+ }
366+
367+ const networkId = ( await hardhat . provider . getNetwork ( ) ) . chainId
368+ const nonce = 0
369+ const data = encodeMetaTransactionsData ( wallet . address , [ transaction ] , networkId , nonce )
370+
371+ // Sign with attackerSigner (different from deployment signer, NOT ImmutableSigner)
372+ // This creates a different image hash than what the wallet was deployed with
373+ const signature = await walletMultiSign (
374+ [
375+ { weight : 1 , owner : attackerSigner }
376+ ] ,
377+ 1 ,
378+ data ,
379+ false
380+ )
381+
382+ // This should FAIL because:
383+ // 1. Image hash doesn't match deployment salt
384+ // 2. ImmutableSigner is not present to vouch via bootstrap flow
385+ await expect ( wallet . execute ( [ transaction ] , nonce , signature ) ) . to . be . revertedWith (
386+ 'ModuleCalls#execute: INVALID_SIGNATURE'
387+ )
388+ } )
389+
390+ it ( 'Should accept first transaction with different image hash when ImmutableSigner is present' , async ( ) => {
391+ // Deploy wallet with specific signers (threshold 2, userEOA + immutableSigner)
344392 const walletSalt = encodeImageHash ( 2 , [
345393 { weight : 1 , address : await userEOA . getAddress ( ) } ,
346394 { weight : 1 , address : immutableSigner . address }
@@ -366,21 +414,29 @@ describe('ModuleAuthDynamic Bootstrap Flow', () => {
366414 const nonce = 0
367415 const data = encodeMetaTransactionsData ( wallet . address , [ transaction ] , networkId , nonce )
368416
369- // Sign with ONLY ImmutableSigner (threshold 1, but wallet requires threshold 2)
417+ // Sign with ONLY ImmutableSigner (threshold 1, but wallet was deployed with threshold 2)
370418 // This creates a different image hash than what the wallet was deployed with
419+ // Bootstrap flow allows this because ImmutableSigner can vouch for any first transaction
371420 const signature = await walletMultiSign (
372421 [
373422 { weight : 1 , owner : immutableSigner . address , signature : ( await ethSign ( immutableEOA as ethers . Wallet , data ) ) + '03' }
374423 ] ,
375- 1 , // Wrong threshold - wallet was deployed with threshold 2
424+ 1 , // Different threshold than deployment - but allowed via bootstrap
376425 data ,
377426 false
378427 )
379428
380- // This should fail because the image hash doesn't match
381- await expect ( wallet . execute ( [ transaction ] , nonce , signature ) ) . to . be . revertedWith (
382- 'ModuleCalls#execute: INVALID_SIGNATURE'
383- )
429+ const originalBalance = await randomEOA . getBalance ( )
430+
431+ // This should SUCCEED because ImmutableSigner can vouch for any first transaction
432+ // regardless of whether the signature's image hash matches the deployment salt
433+ await wallet . execute ( [ transaction ] , nonce , signature )
434+
435+ // Verify funds were transferred
436+ expect ( await randomEOA . getBalance ( ) ) . to . equal ( originalBalance . add ( 1 ) )
437+
438+ // Verify nonce incremented
439+ expect ( await wallet . nonce ( ) ) . to . equal ( 1 )
384440 } )
385441
386442 it ( 'Should reject second transaction with wrong signers after bootstrap' , async ( ) => {
0 commit comments