Summary
Proposes adding a standard EVM batch precompile at address 0x808, of the same
lineage as Hydration's existing Call-Permit precompile (0x080a) — the one we already
use for sponsored, gasless transactions. It would let a single signed Call-Permit
execute a batch of EVM calls inside one frame, so a multi-step EVM flow can run gaslessly
for the user in a single sponsored transaction. No changes to any fee, signing, or
paymaster code.
To be explicit: this is not a flash-loan proposal — no flash-borrowing, no receiver
contract, no credit delegation. It's simply a batch of the user's own calls, run in one
sponsored frame.
Motivation
We run a HOLLAR leverage vault on Hydration and use the signed dispatch_permit flow
(the Call-Permit precompile at 0x080a) for gasless, sponsored user transactions — it
works great for one EVM call per permit.
Our vault's core action is a short loop (supply HOLLAR → borrow → supply → …). We'd like
the whole loop to execute in one sponsored transaction for a zero-balance user
account. Today that's the one thing we can't express:
- Sponsorship itself is not the blocker — the
EvmFeePayer override already spans
nested frames, and we verified (read-only mainnet dry-runs) that the entire loop
executes and commits atomically in one transaction when run non-re-entrantly.
- The blocker is EVM re-entrancy: batching the loop's EVM calls at the substrate
layer — dispatch_permit(to=0x401 dispatch precompile, data=utility.batchAll([ dispatcher.dispatchEvmCall(evm.call{source=user}), … ])) — opens a fresh
pallet_evm::call from inside the outer permit's EVM frame. That fails at the runner
level (EvmPermitRunnerError, zero events), the expected result of starting a new EVM
execution inside an executing one.
A batch precompile sidesteps this: it runs each subcall as an internal EVM CALL
(preserving msg.sender = user) inside the single sponsored frame — no re-entrancy,
batchAll atomicity, and the existing sponsorship covers the whole batch end-to-end.
Proposed change
Register the standard pallet-evm-precompile-batch at the free address 0x808, exactly
as the Call-Permit precompile is registered:
-
runtime/hydradx/Cargo.toml: add the pallet-evm-precompile-batch dependency.
-
runtime/hydradx/src/evm/precompiles/mod.rs: one arm in HydraDXPrecompiles::execute
(alongside the existing CALLPERMIT arm in the address map), e.g.
a if a == BATCH => Some(BatchPrecompile::<Runtime>::execute(handle)),
with BATCH = addr(0x808).
Happy to open the PR against your conventions and add an integration test under
integration-tests/src/evm_permit.rs (the harness already exists). The snippet above is
illustrative — your engineers would finalize the exact wiring/versions.
Security
- No new sponsorship surface. Sponsorship stays gated by the existing signed
dispatch_permit; the submitter alone sets the gas limit and enforces its per-tx /
window caps (unchanged). Subcalls carry only the user's own authority, so the blast
radius is identical to a single permit today — just more legs.
batchAll semantics are all-or-nothing (a partially-built position can't strand).
- Optional hardening: gate it behind a caller allowlist — the same pattern you already
use to restrict a precompile to specific callers — if you'd prefer not to expose a
general batch primitive chain-wide.
What we verified (read-only, mainnet)
- The full 14-leg loop dry-ran green on mainnet both as
utility.batch and
(atomically) as utility.batchAll, executed under the account's own origin:
execOk=true, all 5 LiquidityAdded events emitted, no BatchInterrupted,
no evm.BalanceLow, no weight-ceiling error.
- The only failing configuration was the sponsored
dispatch_permit → 0x401 → dispatchEvmCall path, which returns EvmPermitRunnerError with zero events — the
re-entrancy described above. Not gas, balance, fee currency, leg logic, or the PoV
weight ceiling.
Happy to iterate on the address, the allowlist, or the crate version — and to contribute
the PR if that's useful.
Summary
Proposes adding a standard EVM
batchprecompile at address0x808, of the samelineage as Hydration's existing Call-Permit precompile (
0x080a) — the one we alreadyuse for sponsored, gasless transactions. It would let a single signed Call-Permit
execute a batch of EVM calls inside one frame, so a multi-step EVM flow can run gaslessly
for the user in a single sponsored transaction. No changes to any fee, signing, or
paymaster code.
To be explicit: this is not a flash-loan proposal — no flash-borrowing, no receiver
contract, no credit delegation. It's simply a batch of the user's own calls, run in one
sponsored frame.
Motivation
We run a HOLLAR leverage vault on Hydration and use the signed
dispatch_permitflow(the Call-Permit precompile at
0x080a) for gasless, sponsored user transactions — itworks great for one EVM call per permit.
Our vault's core action is a short loop (supply HOLLAR → borrow → supply → …). We'd like
the whole loop to execute in one sponsored transaction for a zero-balance user
account. Today that's the one thing we can't express:
EvmFeePayeroverride already spansnested frames, and we verified (read-only mainnet dry-runs) that the entire loop
executes and commits atomically in one transaction when run non-re-entrantly.
layer —
dispatch_permit(to=0x401 dispatch precompile, data=utility.batchAll([ dispatcher.dispatchEvmCall(evm.call{source=user}), … ]))— opens a freshpallet_evm::callfrom inside the outer permit's EVM frame. That fails at the runnerlevel (
EvmPermitRunnerError, zero events), the expected result of starting a new EVMexecution inside an executing one.
A
batchprecompile sidesteps this: it runs each subcall as an internal EVMCALL(preserving
msg.sender = user) inside the single sponsored frame — no re-entrancy,batchAllatomicity, and the existing sponsorship covers the whole batch end-to-end.Proposed change
Register the standard
pallet-evm-precompile-batchat the free address0x808, exactlyas the Call-Permit precompile is registered:
runtime/hydradx/Cargo.toml: add thepallet-evm-precompile-batchdependency.runtime/hydradx/src/evm/precompiles/mod.rs: one arm inHydraDXPrecompiles::execute(alongside the existing
CALLPERMITarm in the address map), e.g.with
BATCH = addr(0x808).Happy to open the PR against your conventions and add an integration test under
integration-tests/src/evm_permit.rs(the harness already exists). The snippet above isillustrative — your engineers would finalize the exact wiring/versions.
Security
dispatch_permit; the submitter alone sets the gas limit and enforces its per-tx /window caps (unchanged). Subcalls carry only the user's own authority, so the blast
radius is identical to a single permit today — just more legs.
batchAllsemantics are all-or-nothing (a partially-built position can't strand).use to restrict a precompile to specific callers — if you'd prefer not to expose a
general batch primitive chain-wide.
What we verified (read-only, mainnet)
utility.batchand(atomically) as
utility.batchAll, executed under the account's own origin:execOk=true, all 5LiquidityAddedevents emitted, noBatchInterrupted,no
evm.BalanceLow, no weight-ceiling error.dispatch_permit → 0x401 → dispatchEvmCallpath, which returnsEvmPermitRunnerErrorwith zero events — there-entrancy described above. Not gas, balance, fee currency, leg logic, or the PoV
weight ceiling.
Happy to iterate on the address, the allowlist, or the crate version — and to contribute
the PR if that's useful.