@@ -471,26 +471,29 @@ func (tb *TxBuilder) GetGasFeeUsed(ctx context.Context, txHash string) (string,
471471}
472472
473473// GetFundMigrationSigningRequest builds a native token transfer for fund migration,
474- // transferring the maximum possible balance (balance minus gas cost).
474+ // transferring the maximum possible balance (balance minus gas cost minus L1 fee ).
475475// Fund migration only triggers when outbound is disabled and no pending outbounds remain,
476476// so the balance at signing time will equal the balance at broadcast time.
477+ // L1GasFee covers OP-stack sequencer data-availability charges; 0 for non-L2 chains.
477478func (tb * TxBuilder ) GetFundMigrationSigningRequest (ctx context.Context , data * common.FundMigrationData , nonce uint64 ) (* common.UnsignedSigningReq , error ) {
478479 fromAddr := ethcommon .HexToAddress (data .From )
479480 toAddr := ethcommon .HexToAddress (data .To )
480481
481482 if data .GasPrice == nil || data .GasPrice .Sign () == 0 {
482483 return nil , fmt .Errorf ("gas price must be provided for fund migration" )
483484 }
485+ if data .GasLimit == 0 {
486+ return nil , fmt .Errorf ("gas limit must be provided for fund migration" )
487+ }
484488
485489 balance , err := tb .rpcClient .GetBalance (ctx , fromAddr )
486490 if err != nil {
487491 return nil , fmt .Errorf ("failed to get balance of %s: %w" , data .From , err )
488492 }
489493
490- gasCost := new (big.Int ).Mul (data .GasPrice , new (big.Int ).SetUint64 (data .GasLimit ))
491- maxTransfer := new (big.Int ).Sub (balance , gasCost )
492- if maxTransfer .Sign () <= 0 {
493- return nil , fmt .Errorf ("insufficient balance for gas: balance=%s gasCost=%s" , balance .String (), gasCost .String ())
494+ maxTransfer , err := computeFundMigrationTransfer (balance , data .GasPrice , data .GasLimit , data .L1GasFee )
495+ if err != nil {
496+ return nil , err
494497 }
495498
496499 tb .logger .Info ().
@@ -499,6 +502,7 @@ func (tb *TxBuilder) GetFundMigrationSigningRequest(ctx context.Context, data *c
499502 Str ("balance" , balance .String ()).
500503 Str ("gas_price" , data .GasPrice .String ()).
501504 Uint64 ("gas_limit" , data .GasLimit ).
505+ Str ("l1_gas_fee" , l1GasFeeString (data .L1GasFee )).
502506 Str ("transfer_amount" , maxTransfer .String ()).
503507 Msg ("building fund migration tx" )
504508
@@ -521,6 +525,9 @@ func (tb *TxBuilder) GetFundMigrationSigningRequest(ctx context.Context, data *c
521525}
522526
523527// BroadcastFundMigrationTx assembles and broadcasts a signed fund migration transaction.
528+ // The sweep amount must be recomputed here using the same formula as signing
529+ // (balance - gasPrice*gasLimit - l1GasFee); otherwise the broadcast tx hash
530+ // diverges from the signed hash.
524531func (tb * TxBuilder ) BroadcastFundMigrationTx (ctx context.Context , req * common.UnsignedSigningReq , data * common.FundMigrationData , signature []byte ) (string , error ) {
525532 if len (signature ) != 65 {
526533 return "" , fmt .Errorf ("signature must be 65 bytes [r(32)|s(32)|v(1)], got %d" , len (signature ))
@@ -529,6 +536,9 @@ func (tb *TxBuilder) BroadcastFundMigrationTx(ctx context.Context, req *common.U
529536 if data .GasPrice == nil || data .GasPrice .Sign () == 0 {
530537 return "" , fmt .Errorf ("gas price must be provided for fund migration" )
531538 }
539+ if data .GasLimit == 0 {
540+ return "" , fmt .Errorf ("gas limit must be provided for fund migration" )
541+ }
532542
533543 fromAddr := ethcommon .HexToAddress (data .From )
534544 toAddr := ethcommon .HexToAddress (data .To )
@@ -538,10 +548,9 @@ func (tb *TxBuilder) BroadcastFundMigrationTx(ctx context.Context, req *common.U
538548 return "" , fmt .Errorf ("failed to get balance of %s: %w" , data .From , err )
539549 }
540550
541- gasCost := new (big.Int ).Mul (data .GasPrice , new (big.Int ).SetUint64 (data .GasLimit ))
542- maxTransfer := new (big.Int ).Sub (balance , gasCost )
543- if maxTransfer .Sign () <= 0 {
544- return "" , fmt .Errorf ("insufficient balance for gas during broadcast" )
551+ maxTransfer , err := computeFundMigrationTransfer (balance , data .GasPrice , data .GasLimit , data .L1GasFee )
552+ if err != nil {
553+ return "" , err
545554 }
546555
547556 tx := types .NewTransaction (
@@ -574,3 +583,31 @@ func (tb *TxBuilder) BroadcastFundMigrationTx(ctx context.Context, req *common.U
574583
575584 return txHashStr , nil
576585}
586+
587+ // computeFundMigrationTransfer returns the native amount to sweep from the old
588+ // TSS address to the new one: balance - (gasPrice * gasLimit) - l1GasFee.
589+ // The l1GasFee covers OP-stack sequencer data-availability charges (0 for
590+ // non-L2 chains). All validators must compute the same value — any drift
591+ // here breaks the TSS signing hash.
592+ func computeFundMigrationTransfer (balance , gasPrice * big.Int , gasLimit uint64 , l1GasFee * big.Int ) (* big.Int , error ) {
593+ gasCost := new (big.Int ).Mul (gasPrice , new (big.Int ).SetUint64 (gasLimit ))
594+ totalFee := new (big.Int ).Set (gasCost )
595+ if l1GasFee != nil && l1GasFee .Sign () > 0 {
596+ totalFee .Add (totalFee , l1GasFee )
597+ }
598+ maxTransfer := new (big.Int ).Sub (balance , totalFee )
599+ if maxTransfer .Sign () <= 0 {
600+ return nil , fmt .Errorf ("insufficient balance for gas: balance=%s gasCost=%s l1GasFee=%s" ,
601+ balance .String (), gasCost .String (), l1GasFeeString (l1GasFee ))
602+ }
603+ return maxTransfer , nil
604+ }
605+
606+ // l1GasFeeString returns a stable decimal representation of the L1 gas fee
607+ // for logging / error messages, treating nil as "0".
608+ func l1GasFeeString (v * big.Int ) string {
609+ if v == nil {
610+ return "0"
611+ }
612+ return v .String ()
613+ }
0 commit comments