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); +}