Skip to content
8 changes: 6 additions & 2 deletions packages/testing/src/execution_testing/forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3386,8 +3386,12 @@ def max_code_size(
def precompiles(
cls, *, block_number: int = 0, timestamp: int = 0
) -> List[Address]:
"""Return spec from explicit parent."""
return MONAD_EIGHT.precompiles(
"""
Return spec from explicit parent plus reserve balance precompile.
"""
return [
Address(0x1001, label="RESERVE_BALANCE"),
] + MONAD_EIGHT.precompiles(
block_number=block_number, timestamp=timestamp
)

Expand Down
11 changes: 11 additions & 0 deletions src/ethereum/forks/monad_eight/vm/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@ class InvalidParameter(ExceptionalHalt):
pass


class RevertInMonadPrecompile(ExceptionalHalt):
"""
Raised by a Monad precompile to revert with an error message.

Consumes all gas like ExceptionalHalt but preserves evm.output
so the caller sees the revert reason.
"""

pass


class InvalidContractPrefix(ExceptionalHalt):
"""
Raised when the new contract code starts with 0xEF.
Expand Down
11 changes: 11 additions & 0 deletions src/ethereum/forks/monad_eight/vm/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
set_delegation,
)
from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas
from ..vm.precompiled_contracts import MONAD_PRECOMPILE_ADDRESSES
from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS
from . import Evm
from .exceptions import (
Expand All @@ -62,6 +63,7 @@
InvalidOpcode,
OutOfGasError,
Revert,
RevertInMonadPrecompile,
RevertOnReserveBalance,
StackDepthLimitError,
)
Expand Down Expand Up @@ -283,6 +285,10 @@ def process_message(message: Message) -> Evm:
evm_trace(evm, PrecompileStart(evm.message.code_address))
PRE_COMPILED_CONTRACTS[evm.message.code_address](evm)
evm_trace(evm, PrecompileEnd())
elif evm.message.code_address in MONAD_PRECOMPILE_ADDRESSES:
# Calling a precompile via delegation and it's a Monad
# precompile => revert.
raise RevertInMonadPrecompile
else:
while evm.running and evm.pc < ulen(evm.code):
try:
Expand All @@ -296,6 +302,11 @@ def process_message(message: Message) -> Evm:

evm_trace(evm, EvmStop(Ops.STOP))

except RevertInMonadPrecompile as error:
evm_trace(evm, OpException(error))
evm.gas_left = Uint(0)
# evm.output preserved — contains the raw error message
evm.error = error
except ExceptionalHalt as error:
evm_trace(evm, OpException(error))
evm.gas_left = Uint(0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"BLS12_MAP_FP_TO_G1_ADDRESS",
"BLS12_MAP_FP2_TO_G2_ADDRESS",
"P256VERIFY_ADDRESS",
"STAKING_ADDRESS",
"MONAD_PRECOMPILE_ADDRESSES",
)

ECRECOVER_ADDRESS = hex_to_address("0x01")
Expand All @@ -53,3 +55,12 @@
BLS12_MAP_FP_TO_G1_ADDRESS = hex_to_address("0x10")
BLS12_MAP_FP2_TO_G2_ADDRESS = hex_to_address("0x11")
P256VERIFY_ADDRESS = hex_to_address("0x100")
STAKING_ADDRESS = hex_to_address("0x1000")

# Monad-specific precompile addresses: calling these via a delegating EOA
# must revert rather than execute as empty code.
MONAD_PRECOMPILE_ADDRESSES: frozenset = frozenset(
{
STAKING_ADDRESS,
}
)
11 changes: 11 additions & 0 deletions src/ethereum/forks/monad_nine/vm/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@ class InvalidParameter(ExceptionalHalt):
pass


class RevertInMonadPrecompile(ExceptionalHalt):
"""
Raised by a Monad precompile to revert with an error message.

Consumes all gas like ExceptionalHalt but preserves evm.output
so the caller sees the revert reason.
"""

pass


class InvalidContractPrefix(ExceptionalHalt):
"""
Raised when the new contract code starts with 0xEF.
Expand Down
81 changes: 51 additions & 30 deletions src/ethereum/forks/monad_nine/vm/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from ..blocks import Log
from ..fork_types import Address
from ..state import (
State,
account_has_code_or_nonce,
account_has_storage,
begin_transaction,
Expand All @@ -46,13 +47,14 @@
rollback_transaction,
set_code,
)
from ..vm import Message
from ..vm import Message, TransactionEnvironment
from ..vm.eoa_delegation import (
get_delegated_code_address,
is_valid_delegation,
set_delegation,
)
from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas
from ..vm.precompiled_contracts import MONAD_PRECOMPILE_ADDRESSES
from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS
from . import Evm
from .exceptions import (
Expand All @@ -62,6 +64,7 @@
InvalidOpcode,
OutOfGasError,
Revert,
RevertInMonadPrecompile,
RevertOnReserveBalance,
StackDepthLimitError,
)
Expand All @@ -76,6 +79,40 @@
RESERVE_BALANCE = U256(10 * 10**18) # 10 MON


def is_reserve_balance_violated(
state: State,
tx_env: TransactionEnvironment,
) -> bool:
"""
Check if any EOA has violated the reserve balance constraint.

Returns True if a violation is detected, False otherwise.
"""
for addr in set(state._main_trie._data.keys()):
acc = get_account(state, addr)
if acc.code == b"" or is_valid_delegation(acc.code):
original_balance = get_balance_original(state, addr)
if tx_env.origin == addr:
# gas_fees already deducted, need to re-add if sender
# to match with spec.
gas_fees = U256(tx_env.gas_price * tx_env.tx_gas_limit)
original_balance += gas_fees
reserve = min(RESERVE_BALANCE, original_balance)
threshold = reserve - gas_fees
else:
threshold = RESERVE_BALANCE
is_exception = not is_sender_authority(
state, addr
) and not is_valid_delegation(acc.code)
if (
acc.balance < original_balance
and acc.balance < threshold
and not is_exception
):
return True
return False


@dataclass
class MessageCallOutput:
"""
Expand Down Expand Up @@ -293,6 +330,10 @@ def process_message(message: Message) -> Evm:
evm_trace(evm, PrecompileStart(evm.message.code_address))
PRE_COMPILED_CONTRACTS[evm.message.code_address](evm)
evm_trace(evm, PrecompileEnd())
elif evm.message.code_address in MONAD_PRECOMPILE_ADDRESSES:
# Calling a precompile via delegation and it's a Monad
# precompile => revert.
raise RevertInMonadPrecompile
else:
while evm.running and evm.pc < ulen(evm.code):
try:
Expand All @@ -306,6 +347,11 @@ def process_message(message: Message) -> Evm:

evm_trace(evm, EvmStop(Ops.STOP))

except RevertInMonadPrecompile as error:
evm_trace(evm, OpException(error))
evm.gas_left = Uint(0)
# evm.output preserved — contains the raw error message
evm.error = error
except ExceptionalHalt as error:
evm_trace(evm, OpException(error))
evm.gas_left = Uint(0)
Expand All @@ -322,34 +368,9 @@ def process_message(message: Message) -> Evm:
else:
# FIXME: index_in_block is a proxy for not being a system tx
if message.depth == 0 and message.tx_env.index_in_block is not None:
for addr in set(state._main_trie._data.keys()):
acc = get_account(state, addr)
if acc.code == b"" or is_valid_delegation(acc.code):
original_balance = get_balance_original(state, addr)
if message.tx_env.origin == addr:
# gas_fees already deducted, need to re-add if sender
# to match with spec.
gas_fees = U256(
message.tx_env.gas_price
* message.tx_env.tx_gas_limit
)
original_balance += gas_fees
reserve = min(RESERVE_BALANCE, original_balance)
threshold = reserve - gas_fees
else:
threshold = RESERVE_BALANCE
is_exception = not is_sender_authority(
state, addr
) and not is_valid_delegation(acc.code)
if (
acc.balance < original_balance
and acc.balance < threshold
and not is_exception
):
rollback_transaction(state, transient_storage)
evm.error = RevertOnReserveBalance()
return evm
# cannot do this because it fails the entire tx
# raise RevertOnReserveBalance
if is_reserve_balance_violated(state, message.tx_env):
rollback_transaction(state, transient_storage)
evm.error = RevertOnReserveBalance()
return evm
commit_transaction(state, transient_storage)
return evm
14 changes: 14 additions & 0 deletions src/ethereum/forks/monad_nine/vm/precompiled_contracts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
"BLS12_MAP_FP_TO_G1_ADDRESS",
"BLS12_MAP_FP2_TO_G2_ADDRESS",
"P256VERIFY_ADDRESS",
"RESERVE_BALANCE_ADDRESS",
"STAKING_ADDRESS",
"MONAD_PRECOMPILE_ADDRESSES",
)

ECRECOVER_ADDRESS = hex_to_address("0x01")
Expand All @@ -53,3 +56,14 @@
BLS12_MAP_FP_TO_G1_ADDRESS = hex_to_address("0x10")
BLS12_MAP_FP2_TO_G2_ADDRESS = hex_to_address("0x11")
P256VERIFY_ADDRESS = hex_to_address("0x100")
STAKING_ADDRESS = hex_to_address("0x1000")
RESERVE_BALANCE_ADDRESS = hex_to_address("0x1001")

# Monad-specific precompile addresses: calling these via a delegating EOA
# must revert rather than execute as empty code.
MONAD_PRECOMPILE_ADDRESSES: frozenset = frozenset(
{
STAKING_ADDRESS,
RESERVE_BALANCE_ADDRESS,
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
MODEXP_ADDRESS,
P256VERIFY_ADDRESS,
POINT_EVALUATION_ADDRESS,
RESERVE_BALANCE_ADDRESS,
RIPEMD160_ADDRESS,
SHA256_ADDRESS,
)
Expand All @@ -52,6 +53,7 @@
from .modexp import modexp
from .p256verify import p256verify
from .point_evaluation import point_evaluation
from .reserve_balance import reserve_balance
from .ripemd160 import ripemd160
from .sha256 import sha256

Expand All @@ -74,4 +76,5 @@
BLS12_MAP_FP_TO_G1_ADDRESS: bls12_map_fp_to_g1,
BLS12_MAP_FP2_TO_G2_ADDRESS: bls12_map_fp2_to_g2,
P256VERIFY_ADDRESS: p256verify,
RESERVE_BALANCE_ADDRESS: reserve_balance,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""
Ethereum Virtual Machine (EVM) RESERVE BALANCE PRECOMPILED CONTRACT.

.. contents:: Table of Contents
:backlinks: none
:local:

Introduction
------------

Implementation of the RESERVE BALANCE precompiled contract for MIP-4.
"""

from ethereum_types.numeric import U256

from ...vm import Evm
from ...vm.exceptions import InvalidParameter, RevertInMonadPrecompile
from ...vm.gas import GAS_WARM_ACCESS, charge_gas

# Function selector for dippedIntoReserve()
# keccak256("dippedIntoReserve()")[:4].hex() == "3a61584e"
DIPPED_INTO_RESERVE_SELECTOR = bytes.fromhex("3a61584e")


def _is_call(evm: Evm) -> bool:
# STATICCALL: is_static is True
# DELEGATECALL: should_transfer_value is False
# CALLCODE: code_address != current_target
if evm.message.is_static:
return False
if not evm.message.should_transfer_value:
return False
if evm.message.code_address != evm.message.current_target:
return False
return True


def reserve_balance(evm: Evm) -> None:
"""
Return whether execution is in reserve balance violation.

The precompile must be invoked via CALL. Invocations via STATICCALL,
DELEGATECALL, or CALLCODE must revert.

The method is not payable and must revert with the error message
"value is nonzero" when called with a nonzero value.

Calldata must be exactly the 4-byte function selector (0x3a61584e).
If the selector does not match, the precompile reverts with "method
not supported". If extra calldata is appended beyond the selector,
the precompile reverts with "input is invalid".

Reverts consume all gas provided to the call frame.

Parameters
----------
evm :
The current EVM frame.

"""
from ..interpreter import is_reserve_balance_violated

data = evm.message.data

# Must be invoked via CALL only (not STATICCALL, DELEGATECALL, CALLCODE)
if not _is_call(evm):
raise InvalidParameter

# GAS
charge_gas(evm, GAS_WARM_ACCESS)

if len(data) < 4:
evm.output = b"method not supported"
raise RevertInMonadPrecompile

if data[:4] != DIPPED_INTO_RESERVE_SELECTOR:
evm.output = b"method not supported"
raise RevertInMonadPrecompile

if evm.message.value != 0:
evm.output = b"value is nonzero"
raise RevertInMonadPrecompile

if len(data) > 4:
evm.output = b"input is invalid"
raise RevertInMonadPrecompile

# OPERATION
violation = is_reserve_balance_violated(
evm.message.block_env.state,
evm.message.tx_env,
)
# Return bool encoded as uint256 (32 bytes)
evm.output = U256(1 if violation else 0).to_be_bytes32()
Loading