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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions