From 9550c7855e67ca41307af78ca5a35dbe0b860218 Mon Sep 17 00:00:00 2001 From: Bruce Collie Date: Wed, 19 Aug 2026 07:57:29 -0400 Subject: [PATCH] Add deterministic factory to devnet genesis EIP-7997 specifies the deterministic deployment factory as a genesis predeploy for new chains, not an irregular state transition at fork activation. Monad testnet and mainnet already have the contract live at 0x4e59b44847b379578588920cA78FbF26c0B4956C, so only the devnet genesis alloc needs it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015A1Xg6kHnQHbRYc6gsk7Zv --- .../ethereum/test/test_monad_chain.cpp | 2 +- .../monad/chain/monad_devnet_alloc.hpp | 9 + .../monad/deterministic_factory_test.cpp | 207 ++++++++++++++++++ 3 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 category/execution/monad/deterministic_factory_test.cpp diff --git a/category/execution/ethereum/test/test_monad_chain.cpp b/category/execution/ethereum/test/test_monad_chain.cpp index 3d3438c752..5e2a0f29ad 100644 --- a/category/execution/ethereum/test/test_monad_chain.cpp +++ b/category/execution/ethereum/test/test_monad_chain.cpp @@ -134,7 +134,7 @@ TYPED_TEST(TraitsTest, Genesis) to_bytes(keccak256(rlp::encode_block_header(header))); EXPECT_EQ( hash, - 0xb711505d8f46fc921ae824f847f26c5c3657bf6c8b9dcf07ffdf3357a143bca9_bytes32); + 0x1b92d942a27b0433f5b1b1d7b9e50a61c6e1a1a103f62a4d6693e5095b3e2b68_bytes32); auto result = static_validate_header(header); if constexpr (TestFixture::Trait::evm_rev() < MONAD_ETH_LONDON) { diff --git a/category/execution/monad/chain/monad_devnet_alloc.hpp b/category/execution/monad/chain/monad_devnet_alloc.hpp index 7a249ef01a..d3f6025198 100644 --- a/category/execution/monad/chain/monad_devnet_alloc.hpp +++ b/category/execution/monad/chain/monad_devnet_alloc.hpp @@ -19,8 +19,17 @@ MONAD_NAMESPACE_BEGIN +// The first entry is the EIP-7997 deterministic deployment factory +// (Arachnid's proxy) at its canonical predeploy address. Per the EIP it is +// part of genesis for new chains rather than an irregular state transition; +// Monad testnet and mainnet already have it deployed at this address. inline constexpr char const *MONAD_DEVNET_ALLOC = R"( { + "4e59b44847b379578588920cA78FbF26c0B4956C": { + "code": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3", + "nonce": "0x1", + "wei_balance": "0" + }, "f39Fd6e51aad88F6F4ce6aB8827279cffFb92266": { "wei_balance": "100000000000000000000000000000000000000" }, diff --git a/category/execution/monad/deterministic_factory_test.cpp b/category/execution/monad/deterministic_factory_test.cpp new file mode 100644 index 0000000000..8a66d216dd --- /dev/null +++ b/category/execution/monad/deterministic_factory_test.cpp @@ -0,0 +1,207 @@ +// Copyright (C) 2025-26 Category Labs, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +using namespace monad; + +namespace +{ + // EIP-7997 deterministic deployment factory (Arachnid's proxy). Monad + // testnet and mainnet already carry it at this address; devnet ships it + // in the genesis alloc. + constexpr Address FACTORY_ADDRESS{ + 0x4e59b44847b379578588920cA78FbF26c0B4956C_address}; + + // Independent copy of the runtime bytecode so a typo in the genesis + // alloc is caught rather than compared against itself. + byte_string const EXPECTED_FACTORY_CODE = + from_hex( + "0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + "fe03601600081602082378035828234f58015156039578182fd5b808252505050" + "6014600cf3") + .value(); + + struct DeterministicFactoryTest : public ::testing::Test + { + using Trait = MonadTraits; + + MonadDevnet const chain{}; + mpt::Db db; + TrieDb tdb; + vm::VM vm; + BlockState block_state; + State state; + + DeterministicFactoryTest() + : db{std::make_unique()} + , tdb{db} + , block_state{tdb, vm} + , state{block_state, Incarnation{1, 0}} + { + load_genesis_state(chain.get_genesis_state(), tdb); + } + + byte_string_view code_at(Address const &addr) + { + auto const icode = state.get_code(addr)->intercode(); + return {icode->code(), icode->size()}; + } + + // Initcode that deploys the single-byte runtime `0xAA`. + static std::vector trivial_initcode() + { + using namespace monad::vm::utils; + + auto eb = evm_as::EvmBuilder{}; + eb.push(0xAA).push0().mstore8().push(1).push0().return_(); + std::vector bytecode{}; + EXPECT_TRUE(evm_as::validate(eb)); + evm_as::compile(eb, bytecode); + return bytecode; + } + + // Call the factory with `salt || initcode` as calldata, as a user + // would, and return the raw execution result. + evmc::Result call_factory( + bytes32_t const &salt, std::vector const &initcode) + { + static constexpr Address sender = + 0xcccccccccccccccccccccccccccccccccccccccc_address; + + byte_string calldata{salt.bytes, sizeof(salt.bytes)}; + calldata.append(initcode.data(), initcode.size()); + + Transaction const tx{}; + BlockHeader const header{.number = 1}; + evmc_tx_context const tx_context = get_tx_context( + tx, + sender, + header, + chain.get_chain_id(), + chain.get_blob_schedule(header.timestamp)); + NoopCallTracer call_tracer{}; + trace::StateTracer noop_state_tracer = std::monostate{}; + BlockHashBufferFinalized const buffer{}; + uint256_t base_fee{0}; + EvmcHost host{ + call_tracer, + noop_state_tracer, + tx_context, + buffer, + state, + tx, + base_fee, + 0, + ChainContext::debug_empty()}; + + auto msg_memory = state.vm().message_memory_ref(); + evmc_message const msg{ + .kind = EVMC_CALL, + .gas = 1'000'000, + .recipient = FACTORY_ADDRESS, + .sender = sender, + .input_data = calldata.data(), + .input_size = calldata.size(), + .code_address = FACTORY_ADDRESS, + .memory_handle = msg_memory.get(), + .memory = msg_memory.get(), + .memory_capacity = state.vm().message_memory_capacity()}; + auto const hash = state.get_code_hash(msg.code_address); + auto const &code = state.read_code(hash); + return state.vm().template execute(host, &msg, hash, code); + } + }; +} + +TEST_F(DeterministicFactoryTest, present_in_devnet_genesis) +{ + EXPECT_TRUE(state.account_has_code_or_nonce(FACTORY_ADDRESS)); + EXPECT_EQ(state.get_nonce(FACTORY_ADDRESS), 1); + EXPECT_EQ(state.get_balance(FACTORY_ADDRESS), 0); + EXPECT_EQ(code_at(FACTORY_ADDRESS), EXPECTED_FACTORY_CODE); +} + +TEST_F(DeterministicFactoryTest, create2_deploys_at_expected_address) +{ + bytes32_t const salt = to_bytes(uint256_t{0x1234}); + auto const initcode = trivial_initcode(); + byte_string_view const initcode_view{initcode.data(), initcode.size()}; + Address const expected = create2_contract_address( + FACTORY_ADDRESS, salt, keccak256(initcode_view)); + + auto const result = call_factory(salt, initcode); + ASSERT_EQ(result.status_code, EVMC_SUCCESS); + ASSERT_EQ(result.output_size, sizeof(Address)); + Address actual{}; + memcpy(actual.bytes, result.output_data, sizeof(Address)); + EXPECT_EQ(actual, expected); + + // The deployed contract has the runtime code emitted by the initcode, + // and CREATE2 bumped the factory's nonce. + EXPECT_EQ(code_at(expected), from_hex("0xaa").value()); + EXPECT_EQ(state.get_nonce(expected), 1); + EXPECT_EQ(state.get_nonce(FACTORY_ADDRESS), 2); +} + +TEST_F(DeterministicFactoryTest, create2_collision_reverts) +{ + bytes32_t const salt = to_bytes(uint256_t{0x5678}); + auto const initcode = trivial_initcode(); + + auto const first = call_factory(salt, initcode); + ASSERT_EQ(first.status_code, EVMC_SUCCESS); + + // Same salt and initcode: CREATE2 fails on the address collision and + // the proxy reverts with no data. + auto const second = call_factory(salt, initcode); + EXPECT_EQ(second.status_code, EVMC_REVERT); + EXPECT_EQ(second.output_size, 0); +}