Skip to content

Feature request: add an EVM batch precompile at 0x808 (one Call-Permit → a batch of sponsored EVM calls) #1495

Description

@acyberduck

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions