diff --git a/category/execution/monad/reserve_balance/reserve_balance_contract.cpp b/category/execution/monad/reserve_balance/reserve_balance_contract.cpp index 9b9dae910f..c7b7334f5c 100644 --- a/category/execution/monad/reserve_balance/reserve_balance_contract.cpp +++ b/category/execution/monad/reserve_balance/reserve_balance_contract.cpp @@ -46,7 +46,7 @@ static_assert(PrecompileSelector::DIPPED_INTO_RESERVE == 0x3a61584e); constexpr uint64_t DIPPED_INTO_RESERVE_OP_COST = 100; // warm sload coast -constexpr uint64_t FALLBACK_COST = 40'000; +constexpr uint64_t FALLBACK_COST = 100; MONAD_ANONYMOUS_NAMESPACE_END diff --git a/category/execution/monad/reserve_balance/reserve_balance_contract_test.cpp b/category/execution/monad/reserve_balance/reserve_balance_contract_test.cpp index 9e03b56313..53dca29ca0 100644 --- a/category/execution/monad/reserve_balance/reserve_balance_contract_test.cpp +++ b/category/execution/monad/reserve_balance/reserve_balance_contract_test.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -48,11 +49,12 @@ #include #include -#include +#include #include #include +#include #include using namespace monad; @@ -364,10 +366,74 @@ EXPLICIT_MONAD_TRAITS(run_dipped_into_reserve_test); TEST_F(ReserveBalanceEvm, precompile_fallback) { - auto input = std::array{}; + { + auto input = std::array{}; + + auto const m = evmc_message{ + .gas = 100, + .recipient = RESERVE_BALANCE_CA, + .sender = account_a, + .input_data = input.data(), + .input_size = input.size(), + .code_address = RESERVE_BALANCE_CA, + }; + + init_reserve_balance_context>( + state, + Address{m.sender}, + empty_tx, + h.base_fee_per_gas_, + h.i_, + h.chain_ctx_); + + auto const result = h.call(m); + EXPECT_EQ(result.status_code, EVMC_REVERT); + EXPECT_EQ(result.gas_left, 0); + EXPECT_EQ(result.gas_refund, 0); + EXPECT_EQ(result.output_size, 20); + + auto const message = std::string_view{ + reinterpret_cast(result.output_data), 20}; + EXPECT_EQ(message, "method not supported"); + } + + // Not enough gas to execute fallback, should fail with OOG and not REVERT + { + auto input = std::array{}; + + auto const m = evmc_message{ + .gas = 99, + .recipient = RESERVE_BALANCE_CA, + .sender = account_a, + .input_data = input.data(), + .input_size = input.size(), + .code_address = RESERVE_BALANCE_CA, + }; + + init_reserve_balance_context>( + state, + Address{m.sender}, + empty_tx, + h.base_fee_per_gas_, + h.i_, + h.chain_ctx_); + + auto const result = h.call(m); + EXPECT_EQ(result.status_code, EVMC_OUT_OF_GAS); + EXPECT_EQ(result.gas_left, 0); + EXPECT_EQ(result.gas_refund, 0); + EXPECT_EQ(result.output_size, 0); + } +} + +TEST_F(ReserveBalanceEvm, precompile_dipped_into_reserve_present) +{ + u32_be selector = abi_encode_selector("dippedIntoReserve()"); + auto const *s = selector.bytes; + auto input = std::array{s[0], s[1], s[2], s[3]}; auto const m = evmc_message{ - .gas = 40'000, + .gas = 100, .recipient = RESERVE_BALANCE_CA, .sender = account_a, .input_data = input.data(), @@ -384,24 +450,20 @@ TEST_F(ReserveBalanceEvm, precompile_fallback) h.chain_ctx_); auto const result = h.call(m); - EXPECT_EQ(result.status_code, EVMC_REVERT); + EXPECT_EQ(result.status_code, EVMC_SUCCESS); EXPECT_EQ(result.gas_left, 0); EXPECT_EQ(result.gas_refund, 0); - EXPECT_EQ(result.output_size, 20); - - auto const message = std::string_view{ - reinterpret_cast(result.output_data), 20}; - EXPECT_EQ(message, "method not supported"); + EXPECT_EQ(result.output_size, 32); } -TEST_F(ReserveBalanceEvm, precompile_dipped_into_reserve_present) +TEST_F(ReserveBalanceEvm, precompile_dipped_into_reserve_oog) { u32_be selector = abi_encode_selector("dippedIntoReserve()"); auto const *s = selector.bytes; auto input = std::array{s[0], s[1], s[2], s[3]}; auto const m = evmc_message{ - .gas = 100, + .gas = 99, .recipient = RESERVE_BALANCE_CA, .sender = account_a, .input_data = input.data(), @@ -418,10 +480,10 @@ TEST_F(ReserveBalanceEvm, precompile_dipped_into_reserve_present) h.chain_ctx_); auto const result = h.call(m); - EXPECT_EQ(result.status_code, EVMC_SUCCESS); + EXPECT_EQ(result.status_code, EVMC_OUT_OF_GAS); EXPECT_EQ(result.gas_left, 0); EXPECT_EQ(result.gas_refund, 0); - EXPECT_EQ(result.output_size, 32); + EXPECT_EQ(result.output_size, 0); } TEST_F(ReserveBalanceEvm, precompile_dipped_into_reserve_with_argument) @@ -484,3 +546,296 @@ TYPED_TEST(MonadTraitsTest, reverttransaction_revert) run_dipped_into_reserve_test(15, 11, outcome); } + +template + requires is_monad_trait_v +void run_check_call_precompile_test( + State &state, evmc_message const &msg, evmc_status_code expected_status, + std::string_view expected_message = "") +{ + NoopCallTracer call_tracer; + auto const result = check_call_precompile(state, call_tracer, msg); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->status_code, expected_status); + EXPECT_EQ(result->gas_left, 0); + EXPECT_EQ(result->gas_refund, 0); + EXPECT_EQ(result->output_size, expected_message.size()); + + auto const message = std::string_view{ + reinterpret_cast(result->output_data), + expected_message.size()}; + EXPECT_EQ(message, expected_message); +} + +template +struct MonadPrecompileTest : public ::MonadTraitsTest +{ + static constexpr auto account_a = Address{0xdeadbeef}; + + OnDiskMachine machine; + vm::VM vm; + mpt::Db db{machine}; + TrieDb tdb{db}; + BlockState bs{tdb, vm}; + State state{bs, Incarnation{0, 0}}; + NoopCallTracer call_tracer; + + BlockHashBufferFinalized const block_hash_buffer; + Transaction const empty_tx{}; + + ankerl::unordered_dense::segmented_set
const + grandparent_senders_and_authorities{}; + ankerl::unordered_dense::segmented_set
const + parent_senders_and_authorities{}; + ankerl::unordered_dense::segmented_set
const + senders_and_authorities{}; + // The {}s are needed here to pass the 0 < senders.size() assertion checks + // in `dipped_into_reserve`. + std::vector
const senders{{}}; + std::vector>> const authorities{{}}; + ChainContext> const chain_ctx{ + grandparent_senders_and_authorities, + parent_senders_and_authorities, + senders_and_authorities, + senders, + authorities}; + + EvmcHost> h{ + call_tracer, + EMPTY_TX_CONTEXT, + block_hash_buffer, + state, + empty_tx, + 0, + 0, + chain_ctx}; +}; + +DEFINE_MONAD_TRAITS_FIXTURE(MonadPrecompileTest); + +TYPED_TEST( + MonadPrecompileTest, precompile_dipped_into_reserve_wellformedness_checks) +{ + u32_be const selector = abi_encode_selector("dippedIntoReserve()"); + byte_string const calldata = {selector.bytes, 4}; + // Generates a basic OK message + auto const make_msg = [this, &calldata]() -> evmc_message { + return evmc_message{ + .kind = EVMC_CALL, + .flags = 0, + .gas = 100, + .recipient = RESERVE_BALANCE_CA, + .sender = this->account_a, + .input_data = calldata.data(), + .input_size = calldata.size(), + .code_address = RESERVE_BALANCE_CA, + }; + }; + + if constexpr (TestFixture::Trait::monad_rev() < MONAD_NINE) { + // The precompile should be unavailable prior to MONAD_NINE. + NoopCallTracer call_tracer; + auto const result = check_call_precompile( + this->state, call_tracer, make_msg()); + EXPECT_FALSE(result.has_value()); + return; + } + + ASSERT_TRUE(is_precompile(RESERVE_BALANCE_CA)); + + // Wellformedness checking order is specified as: + // clang-format off + // 1. Invocation method is not `CALL`: Reject with message "" + // 2. gas < 100: OOG with message "" + // 3. len(calldata) < 4 => Reject with message "method not supported" + // 4. calldata[:4] != dippedIntoReserve.selector: Reject with message "method not supported" + // 5. calldata[:4] == dippedIntoReserve.selector && value > 0: Reject with message "value is nonzero" + // 6. calldata[:4] == dippedIntoReserve.selector && len(calldata) > 4: Reject with message "input is invalid" + // clang-format on + + uint8_t const *s = selector.bytes; + std::vector> calldata_variants = { + {s[0], s[1], s[2]}, // too short + {s[0], s[1], s[2], s[3]}, // correct selector + {s[0], s[1], s[2], s[3], 0x00}, // too long + {0xFF, 0xFF, 0xFF, 0xFF} // wrong selector + }; + + // 1. Invocation method is not `CALL`: Reject with message "" + { + for (auto const call_kind : + {EVMC_CALL, + EVMC_DELEGATECALL, + EVMC_CALLCODE, + EVMC_CREATE, + EVMC_CREATE2, + EVMC_EOFCREATE}) { + + evmc_message msg = make_msg(); + msg.kind = call_kind; + + for (int64_t const gas : std::initializer_list{99, 100}) { + msg.gas = gas; + for (uint8_t const flags : std::initializer_list{ + 0u, + static_cast(EVMC_STATIC), + static_cast(EVMC_DELEGATED), + static_cast(EVMC_STATIC) | + static_cast(EVMC_DELEGATED)}) { + if (call_kind == EVMC_CALL && flags == 0u) { + // This is the valid CALL case, which should be + // accepted, so skip it in this loop and test it in the + // loops below. + continue; + } + msg.flags = flags; + + for (evmc_uint256be const value : + std::initializer_list{ + 0x00_bytes32, 0x01_bytes32}) { + msg.value = value; + + for (auto const &calldata_variant : calldata_variants) { + msg.input_data = calldata_variant.data(); + msg.input_size = calldata_variant.size(); + + run_check_call_precompile_test< + typename TestFixture::Trait>( + this->state, msg, EVMC_REJECTED); + } + } + } + } + } + } + + // 2. gas < 100: OOG with message "" + { + evmc_message msg = make_msg(); + msg.gas = 99; + + for (evmc_uint256be const value : std::initializer_list{ + 0x00_bytes32, 0x01_bytes32}) { + msg.value = value; + + for (auto const &calldata_variant : calldata_variants) { + msg.input_data = calldata_variant.data(); + msg.input_size = calldata_variant.size(); + + run_check_call_precompile_test( + this->state, msg, EVMC_OUT_OF_GAS); + } + } + } + + // 3. len(calldata) < 4: Reject with message "method not supported" + { + evmc_message msg = make_msg(); + + std::array short3 = {s[0], s[1], s[2]}; + std::array short2 = {s[0], s[1]}; + std::array short1 = {s[0]}; + std::array short0 = {}; + std::vector> short_calldata_variants = { + {short3.data(), short3.size()}, + {short2.data(), short2.size()}, + {short1.data(), short1.size()}, + {short0.data(), short0.size()}, + {nullptr, 0}, + }; + for (auto const &[data, size] : short_calldata_variants) { + msg.input_data = data; + msg.input_size = size; + + for (evmc_uint256be const value : + std::initializer_list{ + 0x00_bytes32, 0x01_bytes32}) { + msg.value = value; + + run_check_call_precompile_test( + this->state, msg, EVMC_REVERT, "method not supported"); + } + } + } + + // Case 4. calldata[:4] != dippedIntoReserve.selector: Reject with message + // "method not supported" + { + evmc_message msg = make_msg(); + + std::array wrong_selector = {0xFF, 0xFF, 0xFF, 0xFF}; + std::array wrong_too_long = {s[0], s[1], s[2], 0xFF, 0x00}; + std::vector> wrong_calldata = { + {wrong_selector.data(), wrong_selector.size()}, + {wrong_too_long.data(), wrong_too_long.size()}, + }; + + for (evmc_uint256be const value : std::initializer_list{ + 0x00_bytes32, 0x01_bytes32}) { + msg.value = value; + + for (auto const &[data, size] : wrong_calldata) { + msg.input_data = data; + msg.input_size = size; + run_check_call_precompile_test( + this->state, msg, EVMC_REVERT, "method not supported"); + } + } + } + + // Case 5. calldata[:4] == dippedIntoReserve.selector && value > 0: Reject + // with message "value is nonzero" + { + evmc_message msg = make_msg(); + msg.value = 0x01_bytes32; + + std::array selector = {s[0], s[1], s[2], s[3]}; + std::array too_long = {s[0], s[1], s[2], s[3], 0x00}; + std::vector> wrong_calldata = { + {selector.data(), selector.size()}, + {too_long.data(), too_long.size()}, + }; + + for (auto const &[data, size] : wrong_calldata) { + msg.input_data = data; + msg.input_size = size; + run_check_call_precompile_test( + this->state, msg, EVMC_REVERT, "value is nonzero"); + } + } + + // Case 6. calldata[:4] == dippedIntoReserve.selector && len(calldata) > 4: + // Reject with message "input is invalid" + { + evmc_message msg = make_msg(); + std::array too_long = {s[0], s[1], s[2], s[3], 0x00}; + msg.input_data = too_long.data(); + msg.input_size = too_long.size(); + run_check_call_precompile_test( + this->state, msg, EVMC_REVERT, "input is invalid"); + } + + // Case 7: A well-formed call that should be accepted. + { + evmc_message msg = make_msg(); + + init_reserve_balance_context>( + this->state, + Address{msg.sender}, + this->empty_tx, + this->h.base_fee_per_gas_, + this->h.i_, + this->h.chain_ctx_); + + std::array expected_message{}; + + std::string_view expected_message_view{ + reinterpret_cast(expected_message.data()), + expected_message.size(), + }; + + run_check_call_precompile_test( + this->state, msg, EVMC_SUCCESS, expected_message_view); + } +}