Protocol-level Gasless EVM Execution (Zero-Gas Actors) | Nibiru EVM #2518
Unique-Divine
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Protocol-level Gasless EVM Execution (Zero-Gas Actors) | Nibiru EVM
Conceptual Model
sequenceDiagram participant User participant Ante as EVM Ante Handler participant Exec as MsgServer (Execution) participant State as Chain State User->>Ante: Submit EthTx (to: Allowlisted) Note over Ante: AnteStepDetectZeroGas Ante->>Ante: Set ZeroGasMarker in Context Note over Ante: Skip: Balance Check & Fee Deduction Ante->>State: Create Account (if fresh onboarding) Ante-->>Exec: Pass Context with Marker Note over Exec: Execute Contract Logic Note over Exec: Skip: RefundGas Exec->>User: Success (Balance Unchanged)Why This Document Exists (And Who It Is For)
This document is for future maintainers and other developers on the team (knowledge transfer). It may also serve auditors later. The primary goal is to make the zero-gas EVM feature easy to grasp and to avoid future regressions. It is a living doc: it was used to implement the feature and is intended for ongoing maintenance.
Why Zero-Gas Exists: Instant Onboarding for an Onchain Perps Exchange
The main use case is a perpetual onchain future exchange. Zero-gas execution removes the need for users to obtain gas before using the exchange. Users can rely on collateral (e.g. USDC, stNIBI in ERC20 form) or exchange credits held in contract state and start using the app in seconds—without ever holding the chain’s native gas token.
Table of Contents
How This Design Question Arose
(Background and initial question that led to this design. If you are reading this
to maintain or audit the system, start at Design Summary (TL;DR) and
The Rules: What Qualifies for Zero Gas.)
Question Posed: Suppose we wanted to implement an EVM ante handler that
allowed a specific type of transaction to run without requiring a
balance of gas—what code would that handler be related to? Which checks in the
current ante stack would be relevant and need some custom bypass?
The Answer: An EVM "zero-balance gas" handler would live in the
EVM ante stack (
x/evm/evmante/). The EVM path is completely separatefrom the non-EVM (Cosmos SDK) ante chain. Several existing steps enforce
gas/balance requirements and would need conditional bypasses for the special
transaction type.
How Gas Normally Works (The Five-Step Flow)
At a high level, gas payment for an EVM transaction works as follows:
So a user's gas balance G ends up in two places:
The Flow: Detect, Mark, Skip
Zero-gas transactions are handled by detecting eligibility (see The Rules: What Qualifies for Zero Gas) and then skipping cost checks, fee deduction, and refund. No credit, no undo, no stored amounts.
AnteStepDetectZeroGaschecks eligibility; if so, it sets a context marker (see Design Q&A (Audit and Maintenance Notes) for marker semantics). No state mutations.Access:
evm.IsZeroGasEthTx(ctx)returns true when the marker is set. Here's askip matrix of differences for the tx when
evm.IsZeroGasEthTx(ctx)is true.tx.Value == 0)RefundGasThe Rules: What Qualifies for Zero Gas
This is the canonical definition of who is eligible and what is not. Other sections link here.
Eligible: A transaction is zero-gas eligible when:
tx.Tois in the governance-controlled list ZeroGasActors.always_zero_gas_contracts (EVM hex addresses on x/sudo),tx.Value == 0,Not eligible:
to == nil). Only calls to specific contracts are eligible; the detection step returns without setting the marker whento == nil.tx.Tois not in the list ortx.Value != 0, the transaction is treated as a normal EVM tx (no marker set).Marker propagation: The context from ante must reach the msg_server so RefundGas is skipped for zero-gas txs. Multiple tests assert that the marker is set in ante and visible in the msg_server (see Test Coverage: Claims Mapped to Tests and Constraints and Where They Are Enforced).
What We Still Enforce: Account Safety Comes First
Design requirement: We only waive gas payment (balance vs. tx cost and deduction). All other ante behavior—especially account verification—must remain in place.
Concretely:
tx.Value == 0. See The Bypass Points: What We Skip and Why injective node code not available #2 for details.Where This Fits in the Existing EVM Pipeline
Zero-Gas Execution Phases: No Charge, No Refund
For zero-gas txs, ante skips deduction and msg_server skips RefundGas. No charge, no refund. For normal txs, the usual flow applies: ante deducts, execution runs, msg_server calls RefundGas.
How Ante Signals Execution
The ante step sets a marker in the SDK context; the msg_server uses
evm.IsZeroGasEthTx(ctx)to decide whether to run RefundGas. See Design Q&A (Audit and Maintenance Notes) ("How does ante-to-execution communication work?") for full marker semantics.Design Q&A (Audit and Maintenance Notes)
These are questions I expect might arise while auditing or modifying the design.
If updating rules or bypass logic, make sure to reevaluate The Rules: What Qualifies for Zero Gas and the Skip matrix.)
Why Ante State Persists (And Why First-Time Users Need It)
In the Cosmos SDK baseapp, ante handler state is not isolated from DeliverTx state: both use the same block-level deliver state cache. When the ante handler succeeds, its branch is merged into that cache immediately (before
runMsgs). If the transaction later fails during message execution (EVM revert, consensus error), only the message-execution branch is discarded; ante writes remain and are committed at end of block. CometBFT still callsCommitwhen a DeliverTx returns an error.For zero-gas fresh user onboarding, this behavior is desirable: a user with no existing account and no NIBI (gas) balance can submit their first transaction as a zero-gas call to an allowlisted contract. The ante handler creates the sender account when missing (in
AnteStepVerifyEthAcc). If that transaction then fails in execution (e.g. contract revert), account creation and any other ante setup still persist—the user is effectively onboarded and can retry or submit other transactions. If ante state were rolled back on message failure, first-time users could not get an account from a zero-gas tx that failed during execution.Q: What makes a transaction eligible for zero-gas execution?
A: See The Rules: What Qualifies for Zero Gas. In short:
tx.ToinZeroGasActors.always_zero_gas_contracts,tx.Value == 0, any sender.Q: Why is there no sender allowlist for EVM zero-gas?
A: This feature adds a new type of zero-gas tx where anyone who calls a specific set of contracts does not need gas. The main use case is a specific onchain (perpetual) exchange; no sender allowlist keeps it simple and open for any user.
Q: Is contract creation (to == nil) eligible?
A: No. Only calls to specific contracts are eligible. Contract creation is not allowlisted; the detection step returns without setting the marker when
to == nil.Q: Why is ZeroGasMeta an empty struct?
A: In Go it has zero size and is uniquely identified by the runtime. The context value under
CtxKeyZeroGasMetaacts as a boolean marker; we only need presence, which is howIsZeroGasEthTx(ctx)uses it.Q: Who pays for gas in a zero-gas transaction?
A: No one. Zero-gas txs are never charged: ante skips the balance-vs-tx-cost check and fee deduction; the msg_server skips RefundGas. Account verification (including account creation when missing) still runs. The sender's balance is unchanged by gas.
Q: Can the sender use funds for anything other than gas in a zero-gas tx?
A: By design, no. Sponsored transactions are expected to have
tx.Value == 0, and allowlisted contracts are trusted not to transfer native balance. While it is theoretically possible for a mis-whitelisted contract to move funds, this is treated as a governance or operational error rather than a design flaw.Q: How does ante-to-execution communication work?
A: The ante step sets a marker (empty
ZeroGasMetaunderCtxKeyZeroGasMeta) in the SDK context. The context is preserved into the EthereumTx message handler during DeliverTx. The msg_server usesevm.IsZeroGasEthTx(ctx)to decide whether to run RefundGas. No amounts are stored or passed; only the presence of the marker matters.Q: How does zero-gas interact with other EVM features (devgas, priority tip, gas estimation)?
A: Zero-gas txs pay no gas fees, so there is no priority tip and no devgas distribution for them. Gas estimation (e.g.
eth_estimateGas) uses a path that does not run the ante chain and does not set the zero-gas marker; behavior is unchanged and no special handling is needed.Q: What is the trust model of this system?
A: The chain does not attempt to sandbox or restrict contract behavior at runtime. Safety relies on governance-controlled allowlisting of trusted contracts. Zero-gas execution is a policy surface, not a permissionless mechanism.
Code Map: What Lives Where
This section covers the mechanics: where the code lives, how the ante chain
is modified, what is skipped, and which tests enforce each claim.
What Runs When: Ante vs Execution Across ABCI Phases
Ante handler (definition)
The ante handler is the logic we register: what runs and in what order. In Nibiru it is defined in two places:
app.NewAnteHandler(...)inapp/ante.goreturns a singlesdk.AnteHandlerthat chooses either the non-EVM decorator chain or the EVM ante. For EVM txs it callsevmante.NewAnteHandlerEvm(options).x/evm/evmante/all_evmante.go, the EVM handler is a single decorator (AnteHandlerEvm) that runs a fixed list of steps (AnteStepSetupCtx, EthSigVerification, AnteStepDetectZeroGas, …, AnteStepDeductGas, etc.). That list is the "ante handler" for EVM: it defines signature checks, zero-gas detection, balance/cost checks, fee deduction, nonce increment, and so on.So "the ante handler" = the function we pass to
app.SetAnteHandler(...)and the step chain inside evmante. It is independent of when the chain is run; it only describes what runs.ABCI method mapping (when it runs)
The Cosmos SDK baseapp (internal to the SDK) decides when that ante handler runs. It wires ABCI methods to the same ante handler and (for some methods) to message execution:
ctx.IsCheckTx() == true). It does not run message execution (runMsgs). So the ante handler is used to accept or reject the tx for the mempool (e.g. mempool min gas price, balance checks).simulate == true; runMsgs is used for execution simulation. Paths likeeth_estimateGasmay bypass ante and call execution directly.So: ante runs in both CheckTx and DeliverTx; runMsgs runs only in DeliverTx (and in Simulate). The evmante code uses
sdb.Ctx().IsCheckTx()andevmstate.IsDeliverTx(sdb.Ctx())to branch (e.g. MempoolGasPrice only in CheckTx;sdb.Commit()only in DeliverTx).Relevance for zero-gas
Entry Point: EVM vs Non-EVM Routing
EVM vs non-EVM transactions are routed in
app/ante.go:app/ante.goEVM Ante Step Order (And Where Zero-Gas Is Detected)
The EVM ante handler is defined in
x/evm/evmante/all_evmante.go:x/evm/evmante/all_evmante.goThe Bypass Points: What We Skip and Why
1. AnteStepVerifyEthAcc: Skip Balance Checks, Keep Everything Else
Validates from address, ensures sender account exists (creates if missing), and checks that the sender’s balance covers the full transaction cost (fees + value). For zero-gas, only the balance-vs-cost check is skipped; validation and account creation still run. See What We Still Enforce: Account Safety Comes First.
x/evm/evmante/evmante_can_transfer.gox/evm/evmstate/gas_fees.goKey logic in
evmstate.CheckSenderBalance:cost := txData.Cost(); balanceWei.ToBig().Cmp(cost) < 0→ insufficient funds.2. AnteStepCanTransfer: Structural Validity Still Applies
Validates: (1)
gasFeeCap >= baseFee, (2) ifvalue > 0, balance >= value. Runs always for zero-gas; value check no-ops whentx.Value == 0(eligibility requirement). No bypass; structural validity is enforced.x/evm/evmante/evmante_can_transfer.goKey checks: Gas fee cap vs base fee (lines 110–116 approx.); value transfer vs balance (lines 121–132 approx.). Gas cap check passes because the effective gas fee cap used in the check is at least the block base fee (see
EffectiveGasCapWeiin EVM tx types).3. AnteStepDeductGas: Fee Collection Is Fully Skipped
Deducts transaction fees from the sender and sends them to the fee collector. For zero-gas txs, the entire step is skipped before fee computation.
x/evm/evmante/evmante_gas_consume.gox/evm/evmstate/gas_fees.gox/evm/evmstate/gas_fees.goPrimary bypass for zero-gas (runs before VerifyFee):
A secondary path skips deduction when
fees.IsZero()(after VerifyFee); zero-gas txs never reach it.4. AnteStepMempoolGasPrice: CheckTx Bypass Only
Enforces minimum gas price during CheckTx for mempool admission. Skip this step when the tx is identified as zero-gas.
x/evm/evmante/evmante_mempool_fees.goReference Point: How Non-EVM Zero-Gas Already Works
The non-EVM path has an existing zero-gas pattern for Wasm
MsgExecuteContract:app/ante/fixed_gas.goapp/ante/deduct_fee.goisZeroGasMeter(ctx)is truex/sudo/app/ante/fixed_gas.go, lines 71–109 (approximate)app/ante/deduct_fee.go, lines 53–57, 159–169 (isZeroGasMeter) (approximate)EVM does not use this path; it has its own ante stack, so an analogous mechanism is implemented in
x/evm/evmante/.Operating Zero-Gas: Governance, Validation, and Shutdown
Adding or removing zero-gas contracts: The list is governance-controlled: ZeroGasActors.always_zero_gas_contracts. Use the CLI to edit:
nibid tx sudo edit-zero-gas '{"senders":[],"contracts":[],"always_zero_gas_contracts":["0x..."]}'Validation:
ZeroGasActors.Validate()(inx/sudo/msgs.go) ensures each entry inalways_zero_gas_contracts(andcontracts) is a valid EVM address. Seex/sudo/msgs.goandx/sudo/cli/for the message and CLI implementation.Emergency disable: Remove or adjust
always_zero_gas_contractsvia governance (e.g. submit a new ZeroGasActors with an empty or updated list). See also Plan: Execution Model, Invariants, and Safety — Policy.Plan: Execution Model, Invariants, and Safety
Execution Model: Two Phases, One Marker
EthereumTx, and this msg handler runs only on DeliverTx + Simulate; CheckTx does not execute messages.EthereumTx).System Invariants (These Must Always Hold)
tx.To ∈ ZeroGasActors.always_zero_gas_contracts, any sender;tx.Value == 0. No sender allowlist for EVM.ZeroGasMetaunderCtxKeyZeroGasMeta; no stored amounts. Access viaevm.IsZeroGasEthTx(ctx)/evm.GetZeroGasMeta(ctx). Prereqs:CtxKeyZeroGasMeta,GetZeroGasMeta,ZeroGasMetain const.go/evm.go;always_zero_gas_contractsin ZeroGasActors (proto, default, validation). Config: EVM ante usesevmstate.Keeper.SudoKeeper.GetZeroGasEvmContracts; SudoKeeper not in AnteHandlerOptions.Policy: Sponsored txs pay no validator gas fees; no extra gas caps. Emergency disable: change ZeroGasActors (e.g. via governance) to remove or adjust
always_zero_gas_contracts. See Operating Zero-Gas: Governance, Validation, and Shutdown.Edge cases: Reverted execution (no RefundGas run); execution not attempted (framework reverts); simulation (
eth_call/eth_estimateGas) skips ante, no marker.Edge Cases and Failure Modes
eth_estimateGas,eth_call) — These skip ante and go straight toApplyEvmMsg. No marker is set; behavior unchanged.Detection Placement and Skip Behavior
Detection must run before any step that would reject a zero-gas tx. Data:
tx.Tofromevm.UnpackTxData(msgEthTx.Data).GetTo(); SDB shared. Placement:AnteStepDetectZeroGasafterAnteStepValidateBasic, beforeAnteStepMempoolGasPrice.AnteStepDetectZeroGasinx/evm/evmante/evmante_zero_gas.go. Eligibility: see The Rules: What Qualifies for Zero Gas. Set marker only:sdb.SetCtx(sdb.Ctx().WithValue(evm.CtxKeyZeroGasMeta, &evm.ZeroGasMeta{})). No state mutations. Registered inx/evm/evmante/all_evmante.goafter ValidateBasic, before MempoolGasPrice.IsZeroGasEthTx(sdb.Ctx()): MempoolGasPrice → skip. VerifyEthAcc → run validation and account creation; skip only balance-vs-tx-cost (see What We Still Enforce: Account Safety Comes First). CanTransfer → runs always. DeductGas → skip.Execution Handling and Test Coverage
x/evm/evmstate/msg_server.go, run RefundGas only when!evm.IsZeroGasEthTx(rootCtxGasless). Validatealways_zero_gas_contractsas EVM addresses in ZeroGasActors.Validate(); see Operating Zero-Gas: Governance, Validation, and Shutdown for CLI.Test Coverage: Claims Mapped to Tests
The following table maps design claims to the tests that validate them. Use this as the canonical reference for regression safety.
TestAnteStepDetectZeroGas_NonEligible_NoMetaTestAnteStepDetectZeroGas_Eligible_SetsMetaNoCreditTestAnteStepDetectZeroGas_Eligible_NonZeroValue_NoMetaTestAnteStepDetectZeroGas_SetsMetaInContextWhenCheckTxTestAnteStepDetectZeroGas_*testsTestVerifyEthAcc_ZeroGas_CreatesAccountWhenMissing,TestVerifyEthAcccase "zero-gas: sender has no balance, account created if missing, VerifyEthAcc passes"TestAnteStepDeductGas_SkipsForZeroGasTxTestAnteHandlerEVM"happy: signed tx, sufficient funds",TestEthereumTx_ABCITestAnteHandlerEVM"zero-gas: sender with no balance, full ante passes (first-time onboarding)"TestAnteHandlerEVM"zero-gas: allowlisted contract, meta populated after all ante steps" (MinGasPrices=1, gas price 0 or 1)TestMsgEthereumTx_ZeroGas,TestMsgEthereumTx_ZeroGas_WithRefundTestMsgEthereumTx_ZeroGas_RevertedImplicit validations (no dedicated unit test):
TestAnteHandlerEVM"zero-gas: sender with no balance..." usesMinGasPrices(1)and gas price 0. MempoolGasPrice would reject if it ran; the test passes.grpc_query.gocallsApplyEvmMsgdirectly; no test explicitly asserts "eth_call with zero-gas-like tx behaves normally" but the architecture ensures ante is never invoked for these paths. See Known Test Coverage Gaps.Constraints and Where They Are Enforced
evmante_zero_gas.goTestAnteStepDetectZeroGas_NonEligible_NoMeta,TestAnteStepDetectZeroGas_Eligible_SetsMetaNoCreditevmante_zero_gas.goTestAnteStepDetectZeroGas_Eligible_NonZeroValue_NoMeta,TestAnteStepDetectZeroGas_Eligible_SetsMetaNoCreditevmante_zero_gas.go(returns nil whento == nil)msg_server.goTestMsgEthereumTx_ZeroGas,TestMsgEthereumTx_ZeroGas_WithRefund,TestMsgEthereumTx_ZeroGas_Revertedevmante_can_transfer.goTestVerifyEthAcc_ZeroGas_CreatesAccountWhenMissing,TestVerifyEthAcc"zero-gas: sender has no balance, account created if missing, VerifyEthAcc passes"evmante_gas_consume.goTestAnteStepDeductGas_SkipsForZeroGasTxevmante_mempool_fees.goKnown Test Coverage Gaps
MempoolGasPrice zero-gas skip is validated implicitly via full ante chain tests in
all_evmante_test.go. A dedicated unit test for this step could strengthen regression safety. Simulation (eth_call/eth_estimateGas) skipping ante is not covered by a dedicated test; the code path is ingrpc_query.go.Files and End-to-End Flow
x/evm/const.go,x/evm/evm.goCtxKeyZeroGasMeta,GetZeroGasMeta,ZeroGasMeta,IsZeroGasEthTxproto/nibiru/sudo/v1/state.proto,x/sudo/always_zero_gas_contracts, DefaultZeroGasActors, Validate, getterx/evm/evmante/evmante_zero_gas.goAnteStepDetectZeroGasx/evm/evmante/all_evmante.gox/evm/evmante/evmante_can_transfer.gox/evm/evmante/evmante_gas_consume.gox/evm/evmante/evmante_mempool_fees.gox/evm/evmstate/msg_server.goIsZeroGasEthTxFlow:
Appendix
Rejected Design: Credit Then Undo
An alternative design would temporarily credit the sender, run the normal pipeline (deduct, execute, refund), then undo the net gas payment (reclaim from fee collector and sender). That approach is not implemented. The code uses the conditional bypass above: detect zero-gas, then skip cost checks, deduction, and refund.
Beta Was this translation helpful? Give feedback.
All reactions