@@ -2959,6 +2959,152 @@ mod sponsored_paymaster {
29592959 } ) ;
29602960 }
29612961
2962+ #[ test]
2963+ fn signed_dispatch_permit_should_sponsor_entire_cost_when_signer_has_zero_balance ( ) {
2964+ TestNet :: reset ( ) ;
2965+
2966+ Hydra :: execute_with ( || {
2967+ init_omnipool_with_oracle_for_block_10 ( ) ;
2968+
2969+ let paymaster = paymaster_account ( ) ;
2970+ assert_ok ! ( Balances :: mint_into( & paymaster, 100 * UNITS ) ) ;
2971+
2972+ let user_acc = MockAccount :: new ( alith_truncated_account ( ) ) ;
2973+ for asset in [ HDX , WETH , DAI ] {
2974+ let held = user_acc. balance ( asset) ;
2975+ if held > 0 {
2976+ assert_ok ! ( Currencies :: update_balance(
2977+ RuntimeOrigin :: root( ) ,
2978+ user_acc. address( ) ,
2979+ asset,
2980+ -( held as i128 ) ,
2981+ ) ) ;
2982+ }
2983+ assert_eq ! ( user_acc. balance( asset) , 0 , "user must start with nothing" ) ;
2984+ }
2985+
2986+ let initial_paymaster_hdx = Currencies :: free_balance ( HDX , & paymaster) ;
2987+
2988+ let inner_call = RuntimeCall :: System ( frame_system:: Call :: < Runtime > :: remark_with_event {
2989+ remark : b"sponsored" . to_vec ( ) ,
2990+ } ) ;
2991+ let ( from, data, gas_limit, deadline, v, r, s) =
2992+ build_permit_for_call ( & inner_call, 1_000_000 , U256 :: from ( 1_000_000_000_000u128 ) ) ;
2993+
2994+ let result = MultiTransactionPayment :: dispatch_permit (
2995+ RuntimeOrigin :: signed ( paymaster. clone ( ) ) ,
2996+ from,
2997+ DISPATCH_ADDR ,
2998+ U256 :: from ( 0 ) ,
2999+ data,
3000+ gas_limit,
3001+ deadline,
3002+ v,
3003+ r,
3004+ s,
3005+ ) ;
3006+ assert_ok ! ( result) ;
3007+
3008+ let events = frame_system:: Pallet :: < Runtime > :: events ( ) ;
3009+
3010+ let executed_as_user = events. iter ( ) . any ( |record| {
3011+ matches ! (
3012+ & record. event,
3013+ RuntimeEvent :: System ( frame_system:: Event :: Remarked { sender, .. } )
3014+ if * sender == user_acc. address( )
3015+ )
3016+ } ) ;
3017+ assert ! (
3018+ executed_as_user,
3019+ "inner call MUST execute under the zero-balance signer's own origin"
3020+ ) ;
3021+
3022+ let alith = alith_evm_address ( ) ;
3023+ let sponsored = events. iter ( ) . any ( |record| {
3024+ matches ! (
3025+ & record. event,
3026+ RuntimeEvent :: MultiTransactionPayment ( pallet_transaction_multi_payment:: Event :: FeeSponsored {
3027+ from,
3028+ fee_payer,
3029+ ..
3030+ } ) if * from == alith && * fee_payer == paymaster
3031+ )
3032+ } ) ;
3033+ assert ! ( sponsored, "FeeSponsored MUST name the paymaster as fee payer" ) ;
3034+
3035+ assert_eq ! ( user_acc. balance( HDX ) , 0 , "signer MUST NOT be charged in HDX" ) ;
3036+ assert_eq ! ( user_acc. balance( WETH ) , 0 , "signer MUST NOT be charged in WETH" ) ;
3037+ assert ! (
3038+ Currencies :: free_balance( HDX , & paymaster) < initial_paymaster_hdx,
3039+ "paymaster MUST bear the whole cost"
3040+ ) ;
3041+ assert_dispatch_permit_not_paused ( ) ;
3042+ } ) ;
3043+ }
3044+
3045+ #[ test]
3046+ fn signed_dispatch_permit_should_fail_without_executing_when_paymaster_cannot_cover_gas ( ) {
3047+ TestNet :: reset ( ) ;
3048+
3049+ Hydra :: execute_with ( || {
3050+ init_omnipool_with_oracle_for_block_10 ( ) ;
3051+
3052+ let paymaster: AccountId = [ 9u8 ; 32 ] . into ( ) ;
3053+ assert_eq ! (
3054+ Currencies :: free_balance( HDX , & paymaster) ,
3055+ 0 ,
3056+ "control requires a paymaster with nothing"
3057+ ) ;
3058+
3059+ let user_acc = MockAccount :: new ( alith_truncated_account ( ) ) ;
3060+ for asset in [ HDX , WETH , DAI ] {
3061+ let held = user_acc. balance ( asset) ;
3062+ if held > 0 {
3063+ assert_ok ! ( Currencies :: update_balance(
3064+ RuntimeOrigin :: root( ) ,
3065+ user_acc. address( ) ,
3066+ asset,
3067+ -( held as i128 ) ,
3068+ ) ) ;
3069+ }
3070+ }
3071+
3072+ let inner_call = RuntimeCall :: System ( frame_system:: Call :: < Runtime > :: remark_with_event {
3073+ remark : b"sponsored" . to_vec ( ) ,
3074+ } ) ;
3075+ let ( from, data, gas_limit, deadline, v, r, s) =
3076+ build_permit_for_call ( & inner_call, 1_000_000 , U256 :: from ( 1_000_000_000_000u128 ) ) ;
3077+
3078+ let err = MultiTransactionPayment :: dispatch_permit (
3079+ RuntimeOrigin :: signed ( paymaster) ,
3080+ from,
3081+ DISPATCH_ADDR ,
3082+ U256 :: from ( 0 ) ,
3083+ data,
3084+ gas_limit,
3085+ deadline,
3086+ v,
3087+ r,
3088+ s,
3089+ )
3090+ . expect_err ( "an unfunded paymaster must not be able to sponsor" ) ;
3091+
3092+ assert_eq ! (
3093+ err. error,
3094+ pallet_transaction_multi_payment:: Error :: <Runtime >:: EvmPermitRunnerError . into( ) ,
3095+ "insufficient fee-payer balance surfaces as a pre-execution runner error"
3096+ ) ;
3097+
3098+ let executed = frame_system:: Pallet :: < Runtime > :: events ( ) . iter ( ) . any ( |record| {
3099+ matches ! (
3100+ & record. event,
3101+ RuntimeEvent :: System ( frame_system:: Event :: Remarked { .. } )
3102+ )
3103+ } ) ;
3104+ assert ! ( !executed, "inner call MUST NOT execute when sponsorship cannot be paid" ) ;
3105+ } ) ;
3106+ }
3107+
29623108 #[ test]
29633109 fn signed_dispatch_permit_should_emit_fee_sponsored_event_on_success ( ) {
29643110 TestNet :: reset ( ) ;
0 commit comments