Skip to content

Commit 74d7269

Browse files
authored
feat(tests): RESERVE_BALANCE precompile with tests (#14)
Co-Authored-By: Claude <claude-opus-4-5-20251101> Co-authored-by: greptile-apps[bot]
1 parent 832f1e4 commit 74d7269

23 files changed

Lines changed: 3095 additions & 42 deletions

File tree

packages/testing/src/execution_testing/forks/forks/forks.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3386,8 +3386,12 @@ def max_code_size(
33863386
def precompiles(
33873387
cls, *, block_number: int = 0, timestamp: int = 0
33883388
) -> List[Address]:
3389-
"""Return spec from explicit parent."""
3390-
return MONAD_EIGHT.precompiles(
3389+
"""
3390+
Return spec from explicit parent plus reserve balance precompile.
3391+
"""
3392+
return [
3393+
Address(0x1001, label="RESERVE_BALANCE"),
3394+
] + MONAD_EIGHT.precompiles(
33913395
block_number=block_number, timestamp=timestamp
33923396
)
33933397

src/ethereum/forks/monad_eight/vm/exceptions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,17 @@ class InvalidParameter(ExceptionalHalt):
134134
pass
135135

136136

137+
class RevertInMonadPrecompile(ExceptionalHalt):
138+
"""
139+
Raised by a Monad precompile to revert with an error message.
140+
141+
Consumes all gas like ExceptionalHalt but preserves evm.output
142+
so the caller sees the revert reason.
143+
"""
144+
145+
pass
146+
147+
137148
class InvalidContractPrefix(ExceptionalHalt):
138149
"""
139150
Raised when the new contract code starts with 0xEF.

src/ethereum/forks/monad_eight/vm/interpreter.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
set_delegation,
5454
)
5555
from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas
56+
from ..vm.precompiled_contracts import MONAD_PRECOMPILE_ADDRESSES
5657
from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS
5758
from . import Evm
5859
from .exceptions import (
@@ -62,6 +63,7 @@
6263
InvalidOpcode,
6364
OutOfGasError,
6465
Revert,
66+
RevertInMonadPrecompile,
6567
RevertOnReserveBalance,
6668
StackDepthLimitError,
6769
)
@@ -283,6 +285,10 @@ def process_message(message: Message) -> Evm:
283285
evm_trace(evm, PrecompileStart(evm.message.code_address))
284286
PRE_COMPILED_CONTRACTS[evm.message.code_address](evm)
285287
evm_trace(evm, PrecompileEnd())
288+
elif evm.message.code_address in MONAD_PRECOMPILE_ADDRESSES:
289+
# Calling a precompile via delegation and it's a Monad
290+
# precompile => revert.
291+
raise RevertInMonadPrecompile
286292
else:
287293
while evm.running and evm.pc < ulen(evm.code):
288294
try:
@@ -296,6 +302,11 @@ def process_message(message: Message) -> Evm:
296302

297303
evm_trace(evm, EvmStop(Ops.STOP))
298304

305+
except RevertInMonadPrecompile as error:
306+
evm_trace(evm, OpException(error))
307+
evm.gas_left = Uint(0)
308+
# evm.output preserved — contains the raw error message
309+
evm.error = error
299310
except ExceptionalHalt as error:
300311
evm_trace(evm, OpException(error))
301312
evm.gas_left = Uint(0)

src/ethereum/forks/monad_eight/vm/precompiled_contracts/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"BLS12_MAP_FP_TO_G1_ADDRESS",
3434
"BLS12_MAP_FP2_TO_G2_ADDRESS",
3535
"P256VERIFY_ADDRESS",
36+
"STAKING_ADDRESS",
37+
"MONAD_PRECOMPILE_ADDRESSES",
3638
)
3739

3840
ECRECOVER_ADDRESS = hex_to_address("0x01")
@@ -53,3 +55,12 @@
5355
BLS12_MAP_FP_TO_G1_ADDRESS = hex_to_address("0x10")
5456
BLS12_MAP_FP2_TO_G2_ADDRESS = hex_to_address("0x11")
5557
P256VERIFY_ADDRESS = hex_to_address("0x100")
58+
STAKING_ADDRESS = hex_to_address("0x1000")
59+
60+
# Monad-specific precompile addresses: calling these via a delegating EOA
61+
# must revert rather than execute as empty code.
62+
MONAD_PRECOMPILE_ADDRESSES: frozenset = frozenset(
63+
{
64+
STAKING_ADDRESS,
65+
}
66+
)

src/ethereum/forks/monad_nine/vm/exceptions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,17 @@ class InvalidParameter(ExceptionalHalt):
134134
pass
135135

136136

137+
class RevertInMonadPrecompile(ExceptionalHalt):
138+
"""
139+
Raised by a Monad precompile to revert with an error message.
140+
141+
Consumes all gas like ExceptionalHalt but preserves evm.output
142+
so the caller sees the revert reason.
143+
"""
144+
145+
pass
146+
147+
137148
class InvalidContractPrefix(ExceptionalHalt):
138149
"""
139150
Raised when the new contract code starts with 0xEF.

src/ethereum/forks/monad_nine/vm/interpreter.py

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from ..blocks import Log
3333
from ..fork_types import Address
3434
from ..state import (
35+
State,
3536
account_has_code_or_nonce,
3637
account_has_storage,
3738
begin_transaction,
@@ -46,13 +47,14 @@
4647
rollback_transaction,
4748
set_code,
4849
)
49-
from ..vm import Message
50+
from ..vm import Message, TransactionEnvironment
5051
from ..vm.eoa_delegation import (
5152
get_delegated_code_address,
5253
is_valid_delegation,
5354
set_delegation,
5455
)
5556
from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas
57+
from ..vm.precompiled_contracts import MONAD_PRECOMPILE_ADDRESSES
5658
from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS
5759
from . import Evm
5860
from .exceptions import (
@@ -62,6 +64,7 @@
6264
InvalidOpcode,
6365
OutOfGasError,
6466
Revert,
67+
RevertInMonadPrecompile,
6568
RevertOnReserveBalance,
6669
StackDepthLimitError,
6770
)
@@ -76,6 +79,40 @@
7679
RESERVE_BALANCE = U256(10 * 10**18) # 10 MON
7780

7881

82+
def is_reserve_balance_violated(
83+
state: State,
84+
tx_env: TransactionEnvironment,
85+
) -> bool:
86+
"""
87+
Check if any EOA has violated the reserve balance constraint.
88+
89+
Returns True if a violation is detected, False otherwise.
90+
"""
91+
for addr in set(state._main_trie._data.keys()):
92+
acc = get_account(state, addr)
93+
if acc.code == b"" or is_valid_delegation(acc.code):
94+
original_balance = get_balance_original(state, addr)
95+
if tx_env.origin == addr:
96+
# gas_fees already deducted, need to re-add if sender
97+
# to match with spec.
98+
gas_fees = U256(tx_env.gas_price * tx_env.tx_gas_limit)
99+
original_balance += gas_fees
100+
reserve = min(RESERVE_BALANCE, original_balance)
101+
threshold = reserve - gas_fees
102+
else:
103+
threshold = RESERVE_BALANCE
104+
is_exception = not is_sender_authority(
105+
state, addr
106+
) and not is_valid_delegation(acc.code)
107+
if (
108+
acc.balance < original_balance
109+
and acc.balance < threshold
110+
and not is_exception
111+
):
112+
return True
113+
return False
114+
115+
79116
@dataclass
80117
class MessageCallOutput:
81118
"""
@@ -293,6 +330,10 @@ def process_message(message: Message) -> Evm:
293330
evm_trace(evm, PrecompileStart(evm.message.code_address))
294331
PRE_COMPILED_CONTRACTS[evm.message.code_address](evm)
295332
evm_trace(evm, PrecompileEnd())
333+
elif evm.message.code_address in MONAD_PRECOMPILE_ADDRESSES:
334+
# Calling a precompile via delegation and it's a Monad
335+
# precompile => revert.
336+
raise RevertInMonadPrecompile
296337
else:
297338
while evm.running and evm.pc < ulen(evm.code):
298339
try:
@@ -306,6 +347,11 @@ def process_message(message: Message) -> Evm:
306347

307348
evm_trace(evm, EvmStop(Ops.STOP))
308349

350+
except RevertInMonadPrecompile as error:
351+
evm_trace(evm, OpException(error))
352+
evm.gas_left = Uint(0)
353+
# evm.output preserved — contains the raw error message
354+
evm.error = error
309355
except ExceptionalHalt as error:
310356
evm_trace(evm, OpException(error))
311357
evm.gas_left = Uint(0)
@@ -322,34 +368,9 @@ def process_message(message: Message) -> Evm:
322368
else:
323369
# FIXME: index_in_block is a proxy for not being a system tx
324370
if message.depth == 0 and message.tx_env.index_in_block is not None:
325-
for addr in set(state._main_trie._data.keys()):
326-
acc = get_account(state, addr)
327-
if acc.code == b"" or is_valid_delegation(acc.code):
328-
original_balance = get_balance_original(state, addr)
329-
if message.tx_env.origin == addr:
330-
# gas_fees already deducted, need to re-add if sender
331-
# to match with spec.
332-
gas_fees = U256(
333-
message.tx_env.gas_price
334-
* message.tx_env.tx_gas_limit
335-
)
336-
original_balance += gas_fees
337-
reserve = min(RESERVE_BALANCE, original_balance)
338-
threshold = reserve - gas_fees
339-
else:
340-
threshold = RESERVE_BALANCE
341-
is_exception = not is_sender_authority(
342-
state, addr
343-
) and not is_valid_delegation(acc.code)
344-
if (
345-
acc.balance < original_balance
346-
and acc.balance < threshold
347-
and not is_exception
348-
):
349-
rollback_transaction(state, transient_storage)
350-
evm.error = RevertOnReserveBalance()
351-
return evm
352-
# cannot do this because it fails the entire tx
353-
# raise RevertOnReserveBalance
371+
if is_reserve_balance_violated(state, message.tx_env):
372+
rollback_transaction(state, transient_storage)
373+
evm.error = RevertOnReserveBalance()
374+
return evm
354375
commit_transaction(state, transient_storage)
355376
return evm

src/ethereum/forks/monad_nine/vm/precompiled_contracts/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
"BLS12_MAP_FP_TO_G1_ADDRESS",
3434
"BLS12_MAP_FP2_TO_G2_ADDRESS",
3535
"P256VERIFY_ADDRESS",
36+
"RESERVE_BALANCE_ADDRESS",
37+
"STAKING_ADDRESS",
38+
"MONAD_PRECOMPILE_ADDRESSES",
3639
)
3740

3841
ECRECOVER_ADDRESS = hex_to_address("0x01")
@@ -53,3 +56,14 @@
5356
BLS12_MAP_FP_TO_G1_ADDRESS = hex_to_address("0x10")
5457
BLS12_MAP_FP2_TO_G2_ADDRESS = hex_to_address("0x11")
5558
P256VERIFY_ADDRESS = hex_to_address("0x100")
59+
STAKING_ADDRESS = hex_to_address("0x1000")
60+
RESERVE_BALANCE_ADDRESS = hex_to_address("0x1001")
61+
62+
# Monad-specific precompile addresses: calling these via a delegating EOA
63+
# must revert rather than execute as empty code.
64+
MONAD_PRECOMPILE_ADDRESSES: frozenset = frozenset(
65+
{
66+
STAKING_ADDRESS,
67+
RESERVE_BALANCE_ADDRESS,
68+
}
69+
)

src/ethereum/forks/monad_nine/vm/precompiled_contracts/mapping.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
MODEXP_ADDRESS,
3232
P256VERIFY_ADDRESS,
3333
POINT_EVALUATION_ADDRESS,
34+
RESERVE_BALANCE_ADDRESS,
3435
RIPEMD160_ADDRESS,
3536
SHA256_ADDRESS,
3637
)
@@ -52,6 +53,7 @@
5253
from .modexp import modexp
5354
from .p256verify import p256verify
5455
from .point_evaluation import point_evaluation
56+
from .reserve_balance import reserve_balance
5557
from .ripemd160 import ripemd160
5658
from .sha256 import sha256
5759

@@ -74,4 +76,5 @@
7476
BLS12_MAP_FP_TO_G1_ADDRESS: bls12_map_fp_to_g1,
7577
BLS12_MAP_FP2_TO_G2_ADDRESS: bls12_map_fp2_to_g2,
7678
P256VERIFY_ADDRESS: p256verify,
79+
RESERVE_BALANCE_ADDRESS: reserve_balance,
7780
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
Ethereum Virtual Machine (EVM) RESERVE BALANCE PRECOMPILED CONTRACT.
3+
4+
.. contents:: Table of Contents
5+
:backlinks: none
6+
:local:
7+
8+
Introduction
9+
------------
10+
11+
Implementation of the RESERVE BALANCE precompiled contract for MIP-4.
12+
"""
13+
14+
from ethereum_types.numeric import U256
15+
16+
from ...vm import Evm
17+
from ...vm.exceptions import InvalidParameter, RevertInMonadPrecompile
18+
from ...vm.gas import GAS_WARM_ACCESS, charge_gas
19+
20+
# Function selector for dippedIntoReserve()
21+
# keccak256("dippedIntoReserve()")[:4].hex() == "3a61584e"
22+
DIPPED_INTO_RESERVE_SELECTOR = bytes.fromhex("3a61584e")
23+
24+
25+
def _is_call(evm: Evm) -> bool:
26+
# STATICCALL: is_static is True
27+
# DELEGATECALL: should_transfer_value is False
28+
# CALLCODE: code_address != current_target
29+
if evm.message.is_static:
30+
return False
31+
if not evm.message.should_transfer_value:
32+
return False
33+
if evm.message.code_address != evm.message.current_target:
34+
return False
35+
return True
36+
37+
38+
def reserve_balance(evm: Evm) -> None:
39+
"""
40+
Return whether execution is in reserve balance violation.
41+
42+
The precompile must be invoked via CALL. Invocations via STATICCALL,
43+
DELEGATECALL, or CALLCODE must revert.
44+
45+
The method is not payable and must revert with the error message
46+
"value is nonzero" when called with a nonzero value.
47+
48+
Calldata must be exactly the 4-byte function selector (0x3a61584e).
49+
If the selector does not match, the precompile reverts with "method
50+
not supported". If extra calldata is appended beyond the selector,
51+
the precompile reverts with "input is invalid".
52+
53+
Reverts consume all gas provided to the call frame.
54+
55+
Parameters
56+
----------
57+
evm :
58+
The current EVM frame.
59+
60+
"""
61+
from ..interpreter import is_reserve_balance_violated
62+
63+
data = evm.message.data
64+
65+
# Must be invoked via CALL only (not STATICCALL, DELEGATECALL, CALLCODE)
66+
if not _is_call(evm):
67+
raise InvalidParameter
68+
69+
# GAS
70+
charge_gas(evm, GAS_WARM_ACCESS)
71+
72+
if len(data) < 4:
73+
evm.output = b"method not supported"
74+
raise RevertInMonadPrecompile
75+
76+
if data[:4] != DIPPED_INTO_RESERVE_SELECTOR:
77+
evm.output = b"method not supported"
78+
raise RevertInMonadPrecompile
79+
80+
if evm.message.value != 0:
81+
evm.output = b"value is nonzero"
82+
raise RevertInMonadPrecompile
83+
84+
if len(data) > 4:
85+
evm.output = b"input is invalid"
86+
raise RevertInMonadPrecompile
87+
88+
# OPERATION
89+
violation = is_reserve_balance_violated(
90+
evm.message.block_env.state,
91+
evm.message.tx_env,
92+
)
93+
# Return bool encoded as uint256 (32 bytes)
94+
evm.output = U256(1 if violation else 0).to_be_bytes32()

0 commit comments

Comments
 (0)