Sammai's blog

Testing MIP-4 reserve tracking locally

A delegated externally owned account (EOA) starts with 11 MON and sends out 2 MON. For this delegated-account path, Monad's MIP-4 reserve signal should change from false to true once the balance reaches 9 MON.

Monad Testnet reports the dip. The anvil --monad integration suite reports it too. Forge on my installed 1.5.0-stable-monad build keeps returning false.

This mismatch surfaced while I was testing reserve-aware contract flows in monad-mip-lab, which I maintain with the Monad Improvement Proposals subgroup of the Cortex Open Source Working Group.

Anvil and Testnet can confirm this protocol behavior, but neither replaces Forge in the contract development loop. Forge lets the reserve transition live beside Solidity code as a repeatable assertion. This local assertion can run in seconds, fail in CI, and catch a broken guard or toolchain regression before you even submit a transaction.

Without this same signal in Forge, every reserve-related contract change had to leave the local development loop before we could trust its behavior. Someone had to fund a Testnet account, submit a transaction, and inspect the receipt just to verify behavior that should have taken seconds to test locally.

I installed Monad Foundry in May 2026, when the normal stable channel installed 1.5.0-stable-monad. The Forge binary running my experiment therefore came from the official installation path, not a custom build or an unusual local checkout.

Monad Foundry published v1.7.1-monad-v1.0.0 on July 10, 2026. This release did not exist when I installed the toolchain, and the legacy foundryup 1.5.0 launcher could not update itself into the new naming scheme. After reinstalling the launcher, upgrading Forge, and configuring monad-mip-lab for Monad, Prague, and isolated test transactions, the current regression produces the expected transition:

11 MON -> 9 MON -> 11 MON
false  -> true  -> false

Getting this result solved the immediate testing problem. Comparing the two releases also clarified what Forge can prove locally, where Anvil fits, and which MIP-4 claims still require Monad Testnet.

Overview

MIP-4 lets contracts check whether the current transaction state violates Monad's reserve-balance rules. Forge can reproduce the signal locally, but only when the Monad Foundry version and test setup are correct. This article compares the failing and passing releases, explains the delegated-account regression, and separates what Forge, Anvil, and Monad Testnet can each prove.

How MIP-4 works

Monad can reject a transaction when a relevant account's balance decreases and ends below its reserve threshold. For the delegated EOA in this experiment, the blocked case is a balance decrement that leaves the account below 10 MON. Contract developers face a timing problem because the protocol performs the final reserve check after execution, when the contract no longer has a chance to recover.

MIP-4 adds a precompile at 0x1001 so contracts can inspect the reserve-violation condition while the transaction is still running. A precompile is protocol code available at a fixed address. MIP-4 exposes one method, dippedIntoReserve(), which returns:

This signal can change during execution. If a balance decrement creates a reserve violation and a later transfer repairs it, the return value can move from false to true and back to false before the transaction ends.

The local regression exercises both transitions: entering the violation state and recovering from it.

Reproducing the reserve dip

The Forge experiment gives the EOA EIP-7702 delegation. EIP-7702 lets an EOA point to contract code and execute the code in the EOA's own account context. As a result, delegated code can act through the EOA's balance and storage.

This test follows six steps:

  1. The delegated EOA starts with 11 MON.
  2. dippedIntoReserve() returns false.
  3. Another account calls the delegated EOA.
  4. The delegated code sends 2 MON to a receiver, leaving the EOA with 9 MON.
  5. dippedIntoReserve() should now return true.
  6. In the restoration case, the receiver returns the 2 MON and the signal should clear to false.

So the two expected paths are:

11 MON -> 9 MON             false -> true
11 MON -> 9 MON -> 11 MON   false -> true -> false

Both paths provided a common baseline for comparing Forge, Anvil, and Testnet.

Why Forge returned false

Monad Testnet produced the expected reserve transition. A local anvil --monad node also tracked the dip during the transaction. But the Forge test running on the legacy stable release, 1.5.0-stable-monad, returned false before and after the balance moved from 11 MON to 9 MON.

The precompile itself was available in Forge, so a missing precompile address could not explain the stale signal. The call succeeded; the tracker simply remained clear.

Based on these three results, Forge looked like the limitation. I assumed its in-process test runner exposed the precompile without populating the transaction-level reserve state.

My broader conclusion was wrong: Forge was not inherently unable to track MIP-4 reserve state.

Monad Foundry's installation notes now explain that existing users with the legacy foundryup 1.5.0 launcher must reinstall it once. Reinstalling the launcher allowed foundryup --network monad to resolve v1.7.1-monad-v1.0.0, published on July 10, 2026.

I then ran the current Solidity test fixture and configuration directly with both installed binaries. The 1.5.0-stable-monad run returned false throughout. The 1.7.1-monad-v1.0.0 run produced false -> true, while the restoration case produced false -> true -> false.

The newer release describes its Monad work as a port to Foundry's generic network and EVM architecture rather than new Monad VM functionality. The controlled comparison still establishes a practical compatibility boundary: the same Forge regression behaves differently across the two releases. It does not identify the exact internal change responsible for the difference.

Configuring Forge

Existing users with the legacy launcher must reinstall it once before foundryup can resolve the new Monad release scheme:

curl -L https://raw.githubusercontent.com/category-labs/foundry/monad/foundryup/install | bash
foundryup --network monad
forge --version

The verified run reported forge Version: 1.7.1-monad-v1.0.0.

The delegated-account regression uses three project settings:

evm_version = "prague"
network = "monad"
isolate = true

network = "monad" selects Monad's EVM behavior, including the reserve-balance precompile and tracker.

evm_version = "prague" enables the EVM version used by the EIP-7702 delegation path in the test.

With isolate = true, Forge treats each top-level call as a separate test transaction. The EOA receives its 11 MON balance and delegation during setUp(), before the delegated call that measures the 11-to-9 MON transition. The before, during, and after tracker checks remain inside that delegated call because a later top-level call starts a new transaction.

Separating setup from the test transaction changes what the experiment measures. Monad's reserve calculation depends on the account's balance at the start of the transaction. Funding the EOA inside the measured transaction would change the starting condition instead of reproducing the Testnet path.

Reproducibility also depends on the toolchain version. The monad-mip-lab CI now pins the tested Monad Foundry release and reruns the reserve transition before accepting a toolchain upgrade.

Run the repo-local regression from the repository root:

cd MIP-4/examples/reserve-probes
forge test --match-contract DelegatedDrainTest -vvv

With Monad execution, Prague, isolation, and a pinned toolchain in place, Forge can answer a specific class of MIP-4 questions.

What the Forge test proves

The passing Forge regression proves that decrementing an EIP-7702 delegated EOA from 11 MON to 9 MON makes the MIP-4 reserve tracker return true during an isolated test transaction. A working local signal gives contract developers a fast loop for reserve guards, revert behavior, nested calls, restoration paths, and smart-account logic.

Forge creates the delegation with vm.signAndAttachDelegation(). This cheatcode establishes the EIP-7702 delegation state needed by the EVM, but it does not submit a type-4 authorization-list transaction through JSON-RPC. Questions about the transaction envelope therefore belong in Anvil rather than the Forge regression.

The local regression covers two paths. The no-restore case observes false -> true after the delegated EOA reaches 9 MON. The restoration case returns the balance to 11 MON inside the same delegated call and observes false -> true -> false. These assertions prove tracker behavior. They do not prove Monad's final end-of-transaction rejection rule.

Separating tracker behavior from the transaction envelope and final enforcement gives each environment a clear role.

Choosing between Forge, Anvil, and Testnet

Each environment answers a different question:

Environment Use it for Boundary
Forge Contract behavior inside an isolated test transaction Does not submit a real type-4 transaction through JSON-RPC or prove final protocol enforcement
anvil --monad Local JSON-RPC and type-4 transaction flows The recorded experiment tracked the dip but did not reproduce Testnet's final reserve-violation rejection
Monad Testnet Live protocol enforcement, public-node behavior, gas accounting, and transaction receipts Requires network funds and a submitted transaction

Forge is the fastest first step for contract behavior. Move to Anvil when the transaction envelope matters, then use Testnet when the claim depends on the live network.

Preventing future regressions

Finding the working version solves the immediate development problem, but it leaves another one: a future release could silently break the tracker again.

I have prepared the delegated-account transition for Monad Foundry's Forge CLI integration tests and pushed it to a review branch. The regression loads a Solidity fixture, enables Prague and isolated test transactions, runs through the same --network monad CLI path developers invoke, and fails if either expected transition disappears. The branch is ready for upstream review.

The accompanying README change makes the minimum setup visible to users:

Monad Foundry v1.7.1-monad-v1.0.0, or a later release that passes the regression
network = "monad"
evm_version = "prague"
isolate = true
balance and delegation established before the measured transaction

Publishing this setup beside the Monad network configuration would have shortened the investigation. A developer should not need to infer a version boundary from a Testnet mismatch.

A faster local workflow

Before the Forge signal worked locally, even a small reserve-related contract change had to be checked through a funded Testnet account and a submitted transaction.

A working local signal shortens the development loop:

write the contract
run the Forge regression
use Anvil for RPC and type-4 behavior
use Testnet for final protocol evidence

Forge, Anvil, and Testnet now have specific roles. Forge handles iteration. Anvil handles submitted local transactions. Testnet is reserved for claims that depend on the live protocol.