Detect reserve violations mid-execution

Monad reserves 10 MON per EOA to ensure solvency across async execution. MIP-4 lets contracts check if that threshold has been crossed.

The information on this page should not be quoted. Please refer to MIP-4 for the authoritative spec.

10 MON reserve

25

MON balance

Starting balance

Why reserve balance exists

Monad separates consensus from execution. When block N is proposed, the leader only has state from block N-3 (~1.2 seconds ago).

Without protection, a user could appear solvent on stale state but have already spent their funds. The 10 MON reserve ensures EOAs remain solvent across the async execution gap.

Block N-3

State known to consensus

100 MON

Block N-2

Alice spends 95 MON

5 MON

Block N-1

Processing...

5 MON

Block N

Leader proposes Alice's tx

?

Leader sees 100 MON, but Alice actually has 5 MON

Without reserve

Alice submits txs that pass consensus validation (she looks solvent) but fail during execution. Network wastes blockspace processing invalid transactions.

With 10 MON reserve

Alice can only commit 10 MON in gas fees across the 3-block window. The leader rejects transactions that would exceed this budget, even on stale state.

The bundler problem

An ERC-4337 bundler processes multiple UserOperations in one transaction. A single reserve violation reverts the entire bundle.

Without MIP-4, the bundler has no way to know which UserOp caused the failure. With MIP-4, the bundler can call dippedIntoReserve() after each op to identify and revert only the offending op, letting the rest of the bundle complete.

Bundler: 5 UserOperations

1

UserOp #1: Swap 2 MON → USDC

2

UserOp #2: NFT mint (0.5 MON)

3

UserOp #3: Bridge 15 MON out

4

UserOp #4: Stake 1 MON

5

UserOp #5: Swap 0.1 MON → WETH

Watch the reserve in action

Step through a transaction that moves MON between accounts. The 10 MON reserve line shows when an account is in violation. Call dippedIntoReserve() at any point to check.

Alice

25 MON

reserve: 10

Bob

15 MON

reserve: 10

Pool

contract

50 MON

exempt

Step 0 / 5

Technical details

The precompile lives at 0x1001 with a single method: dippedIntoReserve() (selector 0x3a61584e). It costs 100 gas, equivalent to a transient storage read.

The check is global: it evaluates all accounts touched in the transaction, not just the caller's. It returns true if any account's balance is currently below its reserve threshold; it clears back to false if that balance recovers above the threshold mid-transaction.

Call restrictions

  • CALL works
  • STATICCALL, DELEGATECALL, CALLCODE revert
  • Nonzero value reverts
  • Extra calldata beyond the 4-byte selector reverts

Important behaviors

  • Reverts consume all gas (precompile behavior, not Solidity-style refund)
  • Smart contracts (non-EIP-7702) are exempt from reserve balance
  • Emptying exception: an undelegated EOA's first transaction in k blocks may spend below reserve, letting users fully withdraw. EIP-7702-delegated accounts cannot use this exception.
  • O(1) cost: tracks violations incrementally via a failed-address set

Usage in Solidity

interface IReserveBalance {
    function dippedIntoReserve() external returns (bool);
}

// Call the precompile at 0x1001
IReserveBalance reserve = IReserveBalance(address(0x1001));

// After a risky operation:
if (reserve.dippedIntoReserve()) {
    // Some account dropped below 10 MON reserve
    // Revert, adjust, or take alternate path
}

What to write differently

Patterns for integrating the reserve precompile into bundlers and other contracts that orchestrate multi-step MON flows.

Each suggestion below is backed by a transaction on Monad mainnet. Click any link to verify the gas cost on-chain.

Pattern 4A

Probe the reserve precompile after risky operations

Without checking, your bundle reverts at tx end with no information about which sub-call dipped the account below reserve. With the precompile, you can blame the right UserOp.

Before

function executeNaive(Op[] calldata ops) external payable {
    for (uint256 i = 0; i < ops.length; i++) {
        (bool ok, ) = ops[i].target.call{value: ops[i].value}(ops[i].data);
        require(ok, "op reverted");
    }
    // If one of the ops dropped the bundler below 10 MON reserve,
    // the WHOLE tx reverts at completion. The bundler has no way
    // to tell which op was the offender.
}

After

address constant RESERVE_PRECOMPILE = address(0x1001);
bytes4 constant DIPPED = 0x3a61584e;

function executeAware(Op[] calldata ops) external payable {
    for (uint256 i = 0; i < ops.length; i++) {
        (bool ok, ) = ops[i].target.call{value: ops[i].value}(ops[i].data);
        require(ok, "op reverted");
        // CALL (not STATICCALL) — required by MIP-4 spec.
        (bool pOk, bytes memory r) = RESERVE_PRECOMPILE.call(
            abi.encodeWithSelector(DIPPED)
        );
        require(pOk && r.length == 32);
        if (abi.decode(r, (bool))) revert BadOp(i);
    }
}

Before: naive contract

Tx trace contains no probe of 0x1001. If any op had dipped the sender below reserve, the bundler would have no way to identify the offender.

View tx on Monadscan

After: MIP-4 aware

Tx trace contains a CALL to 0x1001 with selector 0x3a61584e (dippedIntoReserve) after each op. Correct integration of the MIP-4 precompile.

View tx on Monadscan

MIP-4 adds a precompile at 0x1001 that returns whether the current account state violates the 10 MON reserve. Bundlers (ERC-4337 entrypoints, multicall aggregators) should call it after each sub-operation: if it returns true, revert immediately with a structured error naming the offender. Critical detail: the call must be a CALL, not STATICCALL. The spec explicitly disallows STATICCALL. Of the ~340 contracts attempting to use the precompile on mainnet today, every single one is calling it via STATICCALL and reverting. The 'after' tx linked below is, to our knowledge, the first correct integration of 0x1001 on Monad mainnet: a real bundler tx whose trace contains a successful CALL to 0x1001 with selector 0x3a61584e. Use this contract (src linked) as the reference.

Continue the discussion on Monad Forum

Questions, feedback, or a better idea? Weigh in on the forum thread.

Open forum thread