Skip to content

Commit 422353a

Browse files
Merge branch 'master' into fix/stableswap-asset-tradability
2 parents 723647c + 43a6c5d commit 422353a

1 file changed

Lines changed: 276 additions & 0 deletions

File tree

integration-tests/src/evm_permit.rs

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2542,3 +2542,279 @@ pub fn init_omnipol() {
25422542
TREASURY_ACCOUNT_INIT_BALANCE,
25432543
));
25442544
}
2545+
2546+
// Tests validating that the CALLPERMIT precompile and dispatch_permit share
2547+
// a single permit domain by design. dispatch_permit is a self-relay mechanism:
2548+
// the user signs one permit and submits it as an unsigned extrinsic to pay fees
2549+
// in a non-native currency. The shared EIP-712 digest and nonce space are intentional.
2550+
2551+
#[test]
2552+
fn permit_is_accepted_by_both_callpermit_and_dispatch_permit_by_design() {
2553+
// The CALLPERMIT precompile and dispatch_permit share the same EIP-712 domain
2554+
// and nonce space. A permit signed once can be submitted via either interface.
2555+
// This is by design — dispatch_permit is a self-relay path, not a separate trust domain.
2556+
TestNet::reset();
2557+
2558+
let user_evm_address = alith_evm_address();
2559+
let user_secret_key = alith_secret_key();
2560+
let user_acc = MockAccount::new(alith_truncated_account());
2561+
2562+
Hydra::execute_with(|| {
2563+
init_omnipool_with_oracle_for_block_10();
2564+
pallet_transaction_payment::pallet::NextFeeMultiplier::<hydradx_runtime::Runtime>::put(
2565+
hydradx_runtime::MinimumMultiplier::get(),
2566+
);
2567+
2568+
assert_ok!(Tokens::set_balance(
2569+
RawOrigin::Root.into(),
2570+
user_acc.address(),
2571+
WETH,
2572+
to_ether(1),
2573+
0,
2574+
));
2575+
assert_ok!(hydradx_runtime::Currencies::update_balance(
2576+
hydradx_runtime::RuntimeOrigin::root(),
2577+
user_acc.address(),
2578+
HDX,
2579+
(10 * UNITS) as i128,
2580+
));
2581+
2582+
let initial_user_weth = user_acc.balance(WETH);
2583+
2584+
let omni_sell =
2585+
hydradx_runtime::RuntimeCall::Omnipool(pallet_omnipool::Call::<hydradx_runtime::Runtime>::sell {
2586+
asset_in: HDX,
2587+
asset_out: DAI,
2588+
amount: 10_000_000,
2589+
min_buy_amount: 0,
2590+
});
2591+
2592+
let gas_limit = 1_000_000u64;
2593+
let deadline = U256::from(1_000_000_000_000u128);
2594+
2595+
// Generate permit using the shared CALLPERMIT domain
2596+
let permit =
2597+
pallet_evm_precompile_call_permit::CallPermitPrecompile::<hydradx_runtime::Runtime>::generate_permit(
2598+
CALLPERMIT,
2599+
user_evm_address,
2600+
DISPATCH_ADDR,
2601+
U256::from(0),
2602+
omni_sell.encode(),
2603+
gas_limit,
2604+
U256::zero(),
2605+
deadline,
2606+
);
2607+
let secret_key = SecretKey::parse(&user_secret_key).unwrap();
2608+
let message = Message::parse(&permit);
2609+
let (rs, v) = sign(&message, &secret_key);
2610+
2611+
// Submit via dispatch_permit (self-relay path)
2612+
assert_ok!(MultiTransactionPayment::dispatch_permit(
2613+
hydradx_runtime::RuntimeOrigin::none(),
2614+
user_evm_address,
2615+
DISPATCH_ADDR,
2616+
U256::from(0),
2617+
omni_sell.encode(),
2618+
gas_limit,
2619+
deadline,
2620+
v.serialize(),
2621+
H256::from(rs.r.b32()),
2622+
H256::from(rs.s.b32()),
2623+
));
2624+
2625+
// Signer pays the EVM fee via dispatch_permit (expected for self-relay)
2626+
let fee_paid = initial_user_weth - user_acc.balance(WETH);
2627+
assert!(
2628+
fee_paid > 0,
2629+
"signer should pay fee when self-relaying via dispatch_permit"
2630+
);
2631+
2632+
// Permit nonce consumed — prevents reuse via either interface
2633+
let permit_nonce =
2634+
<hydradx_runtime::Runtime as pallet_transaction_multi_payment::Config>::EvmPermit::permit_nonce(
2635+
user_evm_address,
2636+
);
2637+
assert_eq!(permit_nonce, U256::one());
2638+
})
2639+
}
2640+
2641+
#[test]
2642+
fn shared_nonce_prevents_permit_reuse_across_submission_paths() {
2643+
// The shared nonce space ensures a permit can only be used once, regardless
2644+
// of which interface it was submitted through. This is the intended replay protection.
2645+
TestNet::reset();
2646+
2647+
let user_evm_address = alith_evm_address();
2648+
let user_secret_key = alith_secret_key();
2649+
let user_acc = MockAccount::new(alith_truncated_account());
2650+
2651+
Hydra::execute_with(|| {
2652+
init_omnipool_with_oracle_for_block_10();
2653+
pallet_transaction_payment::pallet::NextFeeMultiplier::<hydradx_runtime::Runtime>::put(
2654+
hydradx_runtime::MinimumMultiplier::get(),
2655+
);
2656+
2657+
assert_ok!(Tokens::set_balance(
2658+
RawOrigin::Root.into(),
2659+
user_acc.address(),
2660+
WETH,
2661+
to_ether(1),
2662+
0,
2663+
));
2664+
assert_ok!(hydradx_runtime::Currencies::update_balance(
2665+
hydradx_runtime::RuntimeOrigin::root(),
2666+
user_acc.address(),
2667+
HDX,
2668+
(10 * UNITS) as i128,
2669+
));
2670+
2671+
let omni_sell =
2672+
hydradx_runtime::RuntimeCall::Omnipool(pallet_omnipool::Call::<hydradx_runtime::Runtime>::sell {
2673+
asset_in: HDX,
2674+
asset_out: DAI,
2675+
amount: 10_000_000,
2676+
min_buy_amount: 0,
2677+
});
2678+
2679+
let gas_limit = 1_000_000u64;
2680+
let deadline = U256::from(1_000_000_000_000u128);
2681+
2682+
let permit =
2683+
pallet_evm_precompile_call_permit::CallPermitPrecompile::<hydradx_runtime::Runtime>::generate_permit(
2684+
CALLPERMIT,
2685+
user_evm_address,
2686+
DISPATCH_ADDR,
2687+
U256::from(0),
2688+
omni_sell.encode(),
2689+
gas_limit,
2690+
U256::zero(),
2691+
deadline,
2692+
);
2693+
let secret_key = SecretKey::parse(&user_secret_key).unwrap();
2694+
let message = Message::parse(&permit);
2695+
let (rs, v) = sign(&message, &secret_key);
2696+
2697+
// First use succeeds
2698+
assert_ok!(MultiTransactionPayment::dispatch_permit(
2699+
hydradx_runtime::RuntimeOrigin::none(),
2700+
user_evm_address,
2701+
DISPATCH_ADDR,
2702+
U256::from(0),
2703+
omni_sell.encode(),
2704+
gas_limit,
2705+
deadline,
2706+
v.serialize(),
2707+
H256::from(rs.r.b32()),
2708+
H256::from(rs.s.b32()),
2709+
));
2710+
2711+
assert_eq!(
2712+
<hydradx_runtime::Runtime as pallet_transaction_multi_payment::Config>::EvmPermit::permit_nonce(
2713+
user_evm_address,
2714+
),
2715+
U256::one()
2716+
);
2717+
2718+
// Second use of the same permit is rejected — nonce already consumed
2719+
let call = pallet_transaction_multi_payment::Call::dispatch_permit {
2720+
from: user_evm_address,
2721+
to: DISPATCH_ADDR,
2722+
value: U256::from(0),
2723+
data: omni_sell.encode(),
2724+
gas_limit,
2725+
deadline,
2726+
v: v.serialize(),
2727+
r: H256::from(rs.r.b32()),
2728+
s: H256::from(rs.s.b32()),
2729+
};
2730+
assert!(
2731+
MultiTransactionPayment::validate_unsigned(TransactionSource::External, &call).is_err(),
2732+
"same permit cannot be used twice — shared nonce prevents replay"
2733+
);
2734+
})
2735+
}
2736+
2737+
#[test]
2738+
fn dispatch_permit_fee_currency_override_works_with_any_to_address() {
2739+
// dispatch_permit decodes fee currency from `data` regardless of the `to` address.
2740+
// This is safe because `data` is part of the signed permit — the signer explicitly
2741+
// committed to this data. An external party cannot alter it post-signature.
2742+
TestNet::reset();
2743+
2744+
let user_evm_address = alith_evm_address();
2745+
let user_secret_key = alith_secret_key();
2746+
let user_acc = MockAccount::new(alith_truncated_account());
2747+
2748+
Hydra::execute_with(|| {
2749+
init_omnipool_with_oracle_for_block_10();
2750+
pallet_transaction_payment::pallet::NextFeeMultiplier::<hydradx_runtime::Runtime>::put(
2751+
hydradx_runtime::MinimumMultiplier::get(),
2752+
);
2753+
2754+
assert_ok!(hydradx_runtime::Currencies::update_balance(
2755+
hydradx_runtime::RuntimeOrigin::root(),
2756+
user_acc.address(),
2757+
DAI,
2758+
100_000_000_000_000_000_000i128,
2759+
));
2760+
assert_ok!(Tokens::set_balance(
2761+
RawOrigin::Root.into(),
2762+
user_acc.address(),
2763+
WETH,
2764+
to_ether(1),
2765+
0,
2766+
));
2767+
2768+
let initial_dai = user_acc.balance(DAI);
2769+
let initial_weth = user_acc.balance(WETH);
2770+
2771+
// The signer explicitly signs a permit with set_currency(DAI) as data.
2772+
// The `to` address does not need to be DISPATCH_ADDR for fee currency
2773+
// detection to work — this is by design since data is signer-committed.
2774+
let set_currency_call = hydradx_runtime::RuntimeCall::MultiTransactionPayment(
2775+
pallet_transaction_multi_payment::Call::set_currency { currency: DAI },
2776+
);
2777+
let data = set_currency_call.encode();
2778+
2779+
let arbitrary_to: sp_core::H160 = sp_core::H160::from_low_u64_be(0xdeadbeef);
2780+
2781+
let gas_limit = 1_000_000u64;
2782+
let deadline = U256::from(1_000_000_000_000u128);
2783+
2784+
let permit =
2785+
pallet_evm_precompile_call_permit::CallPermitPrecompile::<hydradx_runtime::Runtime>::generate_permit(
2786+
CALLPERMIT,
2787+
user_evm_address,
2788+
arbitrary_to,
2789+
U256::from(0),
2790+
data.clone(),
2791+
gas_limit,
2792+
U256::zero(),
2793+
deadline,
2794+
);
2795+
let secret_key = SecretKey::parse(&user_secret_key).unwrap();
2796+
let message = Message::parse(&permit);
2797+
let (rs, v) = sign(&message, &secret_key);
2798+
2799+
assert_ok!(MultiTransactionPayment::dispatch_permit(
2800+
hydradx_runtime::RuntimeOrigin::none(),
2801+
user_evm_address,
2802+
arbitrary_to,
2803+
U256::from(0),
2804+
data,
2805+
gas_limit,
2806+
deadline,
2807+
v.serialize(),
2808+
H256::from(rs.r.b32()),
2809+
H256::from(rs.s.b32()),
2810+
));
2811+
2812+
let dai_spent = initial_dai - user_acc.balance(DAI);
2813+
let weth_spent = initial_weth - user_acc.balance(WETH);
2814+
2815+
// Fee currency override applied from data regardless of `to` address.
2816+
// This is safe: the signer chose this data and signed over it.
2817+
assert!(dai_spent > 0, "DAI should be used as fee currency per signer's data");
2818+
assert_eq!(weth_spent, 0, "WETH should not be touched when DAI is overridden");
2819+
})
2820+
}

0 commit comments

Comments
 (0)