diff --git a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/utils/BytecodeUtils.java b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/utils/BytecodeUtils.java new file mode 100644 index 00000000000..701472f62c4 --- /dev/null +++ b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/utils/BytecodeUtils.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.web3.utils; + +import static com.hedera.mirror.web3.validation.HexValidator.HEX_PREFIX; + +import jakarta.annotation.Nonnull; +import java.util.regex.Pattern; +import lombok.experimental.UtilityClass; + +/** + * A utility class for extracting runtime bytecode from init bytecode of a smart contract. + *

+ * Smart contracts have init bytecode (constructor bytecode) and runtime bytecode (the code executed when the contract + * is called). This class helps in extracting the runtime bytecode from the given init bytecode by searching for + * specific patterns. + *

+ */ +@UtilityClass +public class BytecodeUtils { + + public static final String SKIP_INIT_CODE_CHECK = "HEDERA_MIRROR_WEB3_EVM_SKIPINITCODECHECK"; + private static final String CODECOPY = "39"; + private static final String RETURN = "f3"; + private static final long MINIMUM_INIT_CODE_SIZE = 14L; + private static final String FREE_MEMORY_POINTER = "60806040"; + private static final String FREE_MEMORY_POINTER_2 = "60606040"; + private static final String RUNTIME_CODE_PREFIX = + "6080"; // The pattern to find the start of the runtime code in the init bytecode + + /** + * Compiled regex pattern to match the init bytecode sequence. The pattern checks for a sequence of a free memory + * pointer setup, a CODECOPY operation, and a RETURN operation, in that order. The sequence is matched + * case-insensitively to account for hexadecimal representations. + *

+ * Pattern explanation: - (%s|%s) matches either FREE_MEMORY_POINTER or FREE_MEMORY_POINTER_2, which represent setup + * instructions for the free memory pointer, required for initialization bytecode. - [0-9a-z]+ matches one or more + * valid hexadecimal characters (0-9, a-f) after the free memory pointer setup. - %s matches the CODECOPY opcode, + * which copies code to memory and typically follows the memory pointer setup in init bytecode. - [0-9a-z]+ matches + * one or more valid hexadecimal characters (0-9, a-f) between CODECOPY and RETURN. - %s matches the RETURN opcode, + * signaling the end of the initialization bytecode. + * + *

+ * Example pattern: (60806040|60606040)[0-9a-z]+39[0-9a-z]+f3 This example would match any sequence where either + * "60806040" or "60606040" appears, followed by "39" (CODECOPY) and then "f3" (RETURN), with valid hexadecimal + * characters in between. + */ + private static final Pattern INIT_BYTECODE_PATTERN = Pattern.compile( + String.format( + "(%s|%s)[0-9a-z]+%s[0-9a-z]+%s", FREE_MEMORY_POINTER, FREE_MEMORY_POINTER_2, CODECOPY, RETURN), + Pattern.CASE_INSENSITIVE); + + public static String extractRuntimeBytecode(String initBytecode) { + // Check if the bytecode starts with "0x" and remove it if necessary + if (initBytecode.startsWith(HEX_PREFIX)) { + initBytecode = initBytecode.substring(2); + } + + String runtimeBytecode = getRuntimeBytecode(initBytecode); + + return HEX_PREFIX + runtimeBytecode; // Append "0x" prefix and return + } + + @Nonnull + private static String getRuntimeBytecode(final String initBytecode) { + // Find the first occurrence of "CODECOPY" (39) + int codeCopyIndex = initBytecode.indexOf(CODECOPY); + + if (codeCopyIndex == -1) { + throw new IllegalArgumentException("CODECOPY instruction (39) not found in init bytecode."); + } + + // Find the first occurrence of "6080" after the "CODECOPY" + int runtimeCodePrefixIndex = initBytecode.indexOf(RUNTIME_CODE_PREFIX, codeCopyIndex); + + if (runtimeCodePrefixIndex == -1) { + throw new IllegalArgumentException("Runtime code prefix (6080) not found after CODECOPY."); + } + + // Extract the runtime bytecode starting from the runtimeCodePrefixIndex + return initBytecode.substring(runtimeCodePrefixIndex); + } + + /** + * Checks if a given data string is likely init bytecode. + * + * @param data the data string to check. + * @return true if it is init bytecode, false otherwise. + */ + public static boolean isInitBytecode(final String data) { + if (data == null || data.length() < MINIMUM_INIT_CODE_SIZE) { + return false; + } + + return INIT_BYTECODE_PATTERN.matcher(data).find(); + } + + public static boolean isValidInitBytecode(final String data) { + return shouldSkipBytecodeCheck() || BytecodeUtils.isInitBytecode(data); + } + + private static boolean shouldSkipBytecodeCheck() { + return Boolean.parseBoolean(System.getenv(SKIP_INIT_CODE_CHECK)); + } +} diff --git a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/viewmodel/ContractCallRequest.java b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/viewmodel/ContractCallRequest.java index b82562a0efe..2dfe035a8b3 100644 --- a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/viewmodel/ContractCallRequest.java +++ b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/viewmodel/ContractCallRequest.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.hedera.mirror.web3.convert.BlockTypeDeserializer; import com.hedera.mirror.web3.convert.BlockTypeSerializer; +import com.hedera.mirror.web3.utils.BytecodeUtils; import com.hedera.mirror.web3.validation.Hex; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.Min; @@ -63,6 +64,7 @@ private boolean hasFrom() { @AssertTrue(message = "must not be empty") private boolean hasTo() { - return value <= 0 || from == null || StringUtils.isNotEmpty(to); + boolean isValidToField = value <= 0 || from == null || StringUtils.isNotEmpty(to); + return BytecodeUtils.isValidInitBytecode(data) || isValidToField; } } diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/controller/ContractControllerTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/controller/ContractControllerTest.java index 44d0ac7d301..0863b29981c 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/controller/ContractControllerTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/controller/ContractControllerTest.java @@ -45,6 +45,15 @@ import com.hedera.mirror.web3.viewmodel.BlockType; import com.hedera.mirror.web3.viewmodel.ContractCallRequest; import com.hedera.mirror.web3.viewmodel.GenericErrorResponse; +import com.hedera.mirror.web3.web3j.generated.DynamicEthCalls; +import com.hedera.mirror.web3.web3j.generated.ERCTestContractHistorical; +import com.hedera.mirror.web3.web3j.generated.EthCall; +import com.hedera.mirror.web3.web3j.generated.EvmCodes; +import com.hedera.mirror.web3.web3j.generated.EvmCodesHistorical; +import com.hedera.mirror.web3.web3j.generated.ExchangeRatePrecompileHistorical; +import com.hedera.mirror.web3.web3j.generated.NestedCallsHistorical; +import com.hedera.mirror.web3.web3j.generated.PrecompileTestContractHistorical; +import com.hedera.mirror.web3.web3j.generated.TestAddressThis; import io.github.bucket4j.Bucket; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; @@ -79,6 +88,7 @@ class ContractControllerTest { private static final String CALL_URI = "/api/v1/contracts/call"; private static final String ONE_BYTE_HEX = "80"; private static final long THROTTLE_GAS_LIMIT = 10_000_000L; + private static final String INIT_CODE = "0x6080604052348015600f57600080fd5b5060a38061001c6000396000f3"; @Resource private MockMvc mockMvc; @@ -117,9 +127,9 @@ private ResultActions contractCall(ContractCallRequest request) { .content(convert(request))); } - @NullAndEmptySource - @ValueSource(strings = {"0x00000000000000000000000000000000000007e7"}) @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"0x00000000000000000000000000000000000007e7", "0x00000000000000000000000000000000000004e2"}) void estimateGas(String to) throws Exception { final var request = request(); request.setEstimate(true); @@ -128,6 +138,28 @@ void estimateGas(String to) throws Exception { contractCall(request).andExpect(status().isOk()); } + @ParameterizedTest + @ValueSource( + strings = { + DynamicEthCalls.BINARY, + ERCTestContractHistorical.BINARY, + EthCall.BINARY, + EvmCodes.BINARY, + EvmCodesHistorical.BINARY, + ExchangeRatePrecompileHistorical.BINARY, + NestedCallsHistorical.BINARY, + PrecompileTestContractHistorical.BINARY, + TestAddressThis.BINARY + }) + void estimateGasContractDeploy(final String data) throws Exception { + final var request = request(); + request.setEstimate(true); + request.setValue(0); + request.setTo(null); + request.setData(data); + contractCall(request).andExpect(status().isOk()); + } + @ValueSource(longs = {2000, -2000, 16_000_000L, 0}) @ParameterizedTest void estimateGasWithInvalidGasParameter(long gas) throws Exception { @@ -446,7 +478,7 @@ void callSuccessWithNullAndEmptyData(String data) throws Exception { void callSuccessOnContractCreateWithMissingFrom() throws Exception { final var request = request(); request.setFrom(null); - request.setTo(null); + request.setData(INIT_CODE); request.setValue(0); request.setEstimate(false); diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallAddressThisTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallAddressThisTest.java index 431763e869a..09bc96ecb8e 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallAddressThisTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallAddressThisTest.java @@ -25,30 +25,59 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hedera.mirror.web3.utils.BytecodeUtils; +import com.hedera.mirror.web3.viewmodel.BlockType; +import com.hedera.mirror.web3.viewmodel.ContractCallRequest; import com.hedera.mirror.web3.web3j.generated.TestAddressThis; import com.hedera.mirror.web3.web3j.generated.TestNestedAddressThis; import com.hedera.node.app.service.evm.contracts.execution.HederaEvmTransactionProcessingResult; import jakarta.annotation.Resource; +import java.math.BigInteger; import java.util.ArrayList; import java.util.List; +import lombok.SneakyThrows; import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.datatypes.Address; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; import org.testcontainers.shaded.org.apache.commons.lang3.StringUtils; +@AutoConfigureMockMvc class ContractCallAddressThisTest extends AbstractContractCallServiceTest { + private static final String CALL_URI = "/api/v1/contracts/call"; + @Resource protected ContractExecutionService contractCallService; + @Resource + private MockMvc mockMvc; + + @Resource + private ObjectMapper objectMapper; + @SpyBean private ContractExecutionService contractExecutionService; + @SneakyThrows + private ResultActions contractCall(ContractCallRequest request) { + return mockMvc.perform(post(CALL_URI) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(convert(request))); + } + @Test void deployAddressThisContract() { - final var contract = testWeb3jService.deploy(TestAddressThis::deploy); + final var contract = testWeb3jService.deployWithValue(TestAddressThis::deploy, BigInteger.valueOf(1000)); final var serviceParameters = testWeb3jService.serviceParametersForTopLevelContractCreate( contract.getContractBinary(), ETH_ESTIMATE_GAS, Address.ZERO); final long actualGas = 57764L; @@ -59,7 +88,7 @@ void deployAddressThisContract() { @Test void addressThisFromFunction() { - final var contract = testWeb3jService.deploy(TestAddressThis::deploy); + final var contract = testWeb3jService.deployWithValue(TestAddressThis::deploy, BigInteger.valueOf(1000)); final var functionCall = contract.send_testAddressThisFunction(); verifyEthCallAndEstimateGas(functionCall, contract); } @@ -67,7 +96,8 @@ void addressThisFromFunction() { @Test void addressThisEthCallWithoutEvmAlias() throws Exception { // Given - final var contract = testWeb3jService.deployWithoutPersist(TestAddressThis::deploy); + final var contract = + testWeb3jService.deployWithoutPersistWithValue(TestAddressThis::deploy, BigInteger.valueOf(1000)); addressThisContractPersist( testWeb3jService.getContractRuntime(), Address.fromHexString(contract.getContractAddress())); final List capturedOutputs = new ArrayList<>(); @@ -88,6 +118,43 @@ void addressThisEthCallWithoutEvmAlias() throws Exception { assertThat(successfulResponse).isEqualTo(capturedOutputs.getFirst().toHexString()); } + @Test + void contractDeployWithoutValue() throws Exception { + // Given + final var contract = testWeb3jService.deployWithValue(TestAddressThis::deploy, BigInteger.valueOf(1000)); + final var request = new ContractCallRequest(); + request.setBlock(BlockType.LATEST); + request.setData(contract.getContractBinary()); + request.setFrom(Address.ZERO.toHexString()); + // When + contractCall(request) + // Then + .andExpect(status().isOk()) + .andExpect(result -> { + final var response = result.getResponse().getContentAsString(); + assertThat(response).contains(BytecodeUtils.extractRuntimeBytecode(contract.getContractBinary())); + }); + } + + @Test + void contractDeployWithValue() throws Exception { + // Given + final var contract = testWeb3jService.deployWithValue(TestAddressThis::deploy, BigInteger.valueOf(1000)); + final var request = new ContractCallRequest(); + request.setBlock(BlockType.LATEST); + request.setData(contract.getContractBinary()); + request.setFrom(Address.ZERO.toHexString()); + request.setValue(1000); + // When + contractCall(request) + // Then + .andExpect(status().isOk()) + .andExpect(result -> { + final var response = result.getResponse().getContentAsString(); + assertThat(response).contains(BytecodeUtils.extractRuntimeBytecode(contract.getContractBinary())); + }); + } + @Test void deployNestedAddressThisContract() { final var contract = testWeb3jService.deploy(TestNestedAddressThis::deploy); @@ -116,4 +183,9 @@ private void addressThisContractPersist(byte[] runtimeBytecode, Address contract .persist(); domainBuilder.recordFile().customize(f -> f.bytes(runtimeBytecode)).persist(); } + + @SneakyThrows + private String convert(Object object) { + return objectMapper.writeValueAsString(object); + } } diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallServiceERCTokenModificationFunctionsTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallServiceERCTokenModificationFunctionsTest.java index 24078c2c242..8e2534df9a6 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallServiceERCTokenModificationFunctionsTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallServiceERCTokenModificationFunctionsTest.java @@ -24,20 +24,49 @@ import static com.hedera.mirror.web3.utils.ContractCallTestUtil.SPENDER_ALIAS; import static com.hedera.mirror.web3.utils.ContractCallTestUtil.SPENDER_PUBLIC_KEY; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.protobuf.ByteString; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.token.Token; import com.hedera.mirror.common.domain.token.TokenKycStatusEnum; import com.hedera.mirror.common.domain.token.TokenTypeEnum; +import com.hedera.mirror.web3.utils.BytecodeUtils; +import com.hedera.mirror.web3.viewmodel.BlockType; +import com.hedera.mirror.web3.viewmodel.ContractCallRequest; import com.hedera.mirror.web3.web3j.generated.ERCTestContract; import com.hedera.mirror.web3.web3j.generated.RedirectTestContract; +import jakarta.annotation.Resource; import java.math.BigInteger; +import lombok.SneakyThrows; import org.hyperledger.besu.datatypes.Address; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +@AutoConfigureMockMvc class ContractCallServiceERCTokenModificationFunctionsTest extends AbstractContractCallServiceTest { + private static final String CALL_URI = "/api/v1/contracts/call"; + + @Resource + private MockMvc mockMvc; + + @Resource + private ObjectMapper objectMapper; + + @SneakyThrows + private ResultActions contractCall(ContractCallRequest request) { + return mockMvc.perform(post(CALL_URI) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(convert(request))); + } + @Test void approveFungibleToken() { // Given @@ -92,6 +121,39 @@ void deleteAllowanceNFT() { verifyEthCallAndEstimateGas(functionCall, contract); } + @Test + void contractDeployNonPayableWithoutValue() throws Exception { + // Given + final var contract = testWeb3jService.deploy(ERCTestContract::deploy); + final var request = new ContractCallRequest(); + request.setBlock(BlockType.LATEST); + request.setData(contract.getContractBinary()); + request.setFrom(Address.ZERO.toHexString()); + // When + contractCall(request) + // Then + .andExpect(status().isOk()) + .andExpect(result -> { + final var response = result.getResponse().getContentAsString(); + assertThat(response).contains(BytecodeUtils.extractRuntimeBytecode(contract.getContractBinary())); + }); + } + + @Test + void contractDeployNonPayableWithValue() throws Exception { + // Given + final var contract = testWeb3jService.deploy(ERCTestContract::deploy); + final var request = new ContractCallRequest(); + request.setBlock(BlockType.LATEST); + request.setData(contract.getContractBinary()); + request.setFrom(Address.ZERO.toHexString()); + request.setValue(10); + // When + contractCall(request) + // Then + .andExpect(status().isBadRequest()); + } + @Test void approveFungibleTokenWithAlias() { // Given @@ -739,4 +801,9 @@ protected void fungibleTokenAllowancePersist( .owner(owner.getId())) .persist(); } + + @SneakyThrows + private String convert(Object object) { + return objectMapper.writeValueAsString(object); + } } diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/utils/RuntimeBytecodeExtractorTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/utils/BytecodeUtilsTest.java similarity index 56% rename from hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/utils/RuntimeBytecodeExtractorTest.java rename to hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/utils/BytecodeUtilsTest.java index b175c144e51..d9233ade4c9 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/utils/RuntimeBytecodeExtractorTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/utils/BytecodeUtilsTest.java @@ -24,18 +24,26 @@ import com.hedera.mirror.web3.service.ContractExecutionService; import com.hedera.mirror.web3.web3j.TestWeb3jService; import com.hedera.mirror.web3.web3j.TestWeb3jService.Web3jTestConfiguration; +import com.hedera.mirror.web3.web3j.generated.DynamicEthCalls; +import com.hedera.mirror.web3.web3j.generated.ERCTestContractHistorical; import com.hedera.mirror.web3.web3j.generated.EthCall; import com.hedera.mirror.web3.web3j.generated.EvmCodes; +import com.hedera.mirror.web3.web3j.generated.EvmCodesHistorical; import com.hedera.mirror.web3.web3j.generated.ExchangeRatePrecompileHistorical; +import com.hedera.mirror.web3.web3j.generated.NestedCallsHistorical; +import com.hedera.mirror.web3.web3j.generated.PrecompileTestContractHistorical; +import com.hedera.mirror.web3.web3j.generated.TestAddressThis; import lombok.RequiredArgsConstructor; import org.hyperledger.besu.datatypes.Address; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.context.annotation.Import; @Import(Web3jTestConfiguration.class) @RequiredArgsConstructor -class RuntimeBytecodeExtractorTest extends Web3IntegrationTest { +class BytecodeUtilsTest extends Web3IntegrationTest { private final ContractExecutionService contractExecutionService; private final TestWeb3jService testWeb3jService; @@ -49,7 +57,7 @@ void setUp() { void testExtractRuntimeBytecodeEvmCodes() { final var serviceParameters = testWeb3jService.serviceParametersForTopLevelContractCreate(EvmCodes.BINARY, ETH_CALL, Address.ZERO); - assertThat(RuntimeBytecodeExtractor.extractRuntimeBytecode(EvmCodes.BINARY)) + assertThat(BytecodeUtils.extractRuntimeBytecode(EvmCodes.BINARY)) .isEqualTo(contractExecutionService.processCall(serviceParameters)); } @@ -57,7 +65,7 @@ void testExtractRuntimeBytecodeEvmCodes() { void testExtractRuntimeBytecodeEthCall() { final var serviceParameters = testWeb3jService.serviceParametersForTopLevelContractCreate(EthCall.BINARY, ETH_CALL, Address.ZERO); - assertThat(RuntimeBytecodeExtractor.extractRuntimeBytecode(EthCall.BINARY)) + assertThat(BytecodeUtils.extractRuntimeBytecode(EthCall.BINARY)) .isEqualTo(contractExecutionService.processCall(serviceParameters)); } @@ -65,7 +73,7 @@ void testExtractRuntimeBytecodeEthCall() { void testExtractRuntimeBytecodeExchangeRateHistorical() { final var serviceParameters = testWeb3jService.serviceParametersForTopLevelContractCreate( ExchangeRatePrecompileHistorical.BINARY, ETH_CALL, Address.ZERO); - assertThat(RuntimeBytecodeExtractor.extractRuntimeBytecode(ExchangeRatePrecompileHistorical.BINARY)) + assertThat(BytecodeUtils.extractRuntimeBytecode(ExchangeRatePrecompileHistorical.BINARY)) .isEqualTo(contractExecutionService.processCall(serviceParameters)); } @@ -73,7 +81,7 @@ void testExtractRuntimeBytecodeExchangeRateHistorical() { void testExtractRuntimeBytecodeMissingCODECOPY() { String initBytecode = "6080abcdef"; // No CODECOPY present final var exception = assertThrows(RuntimeException.class, () -> { - RuntimeBytecodeExtractor.extractRuntimeBytecode(initBytecode); + BytecodeUtils.extractRuntimeBytecode(initBytecode); }); assertThat(exception.getMessage()).isEqualTo("CODECOPY instruction (39) not found in init bytecode."); } @@ -82,8 +90,46 @@ void testExtractRuntimeBytecodeMissingCODECOPY() { void testExtractRuntimeBytecodeMissingRuntimePrefix() { String initBytecode = "395ff3fe"; // CODECOPY present but no runtime code prefix RuntimeException thrown = assertThrows(RuntimeException.class, () -> { - RuntimeBytecodeExtractor.extractRuntimeBytecode(initBytecode); + BytecodeUtils.extractRuntimeBytecode(initBytecode); }); assertThat(thrown.getMessage()).isEqualTo("Runtime code prefix (6080) not found after CODECOPY."); } + + @ParameterizedTest + @ValueSource( + strings = { + DynamicEthCalls.BINARY, + ERCTestContractHistorical.BINARY, + EthCall.BINARY, + EvmCodes.BINARY, + EvmCodesHistorical.BINARY, + ExchangeRatePrecompileHistorical.BINARY, + NestedCallsHistorical.BINARY, + PrecompileTestContractHistorical.BINARY, + TestAddressThis.BINARY + }) + void testIsInitBytecode(final String data) { + assertThat(BytecodeUtils.isInitBytecode(data)).isTrue(); + } + + @ParameterizedTest + @ValueSource( + strings = { + "", + " ", + "0x", + "0x39", // Only CODECOPY, missing everything else + "0xf30039", // Contains RETURN and CODECOPY, but in the wrong order + "608060", // Starts with a partial free memory pointer setup + "608060403900000", // Free memory pointer setup + CODECOPY but missing RETURN + "396080604000000", // CODECOPY before free memory pointer setup + "606060f3", // Free memory pointer setup + RETURN but missing CODECOPY + "60404039f2", // Free memory pointer setup + CODECOPY, but invalid opcode instead of RETURN + "0x0039608040f3", // CODECOPY at start, free memory pointer setup and RETURN out of order + "60806040f360", // Free memory pointer setup followed by RETURN and CODECOPY out of order + "0x608060f34039", // Free memory pointer setup, RETURN before CODECOPY + }) + void testIsInitBytecodeFalse(final String data) { + assertThat(BytecodeUtils.isInitBytecode(data)).isFalse(); + } } diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/utils/RuntimeBytecodeExtractor.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/utils/RuntimeBytecodeExtractor.java deleted file mode 100644 index aadf0c7508e..00000000000 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/utils/RuntimeBytecodeExtractor.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.mirror.web3.utils; - -import static com.hedera.mirror.web3.validation.HexValidator.HEX_PREFIX; - -import jakarta.annotation.Nonnull; -import lombok.experimental.UtilityClass; - -/** - * A utility class for extracting runtime bytecode from init bytecode of a smart contract. - *

- * Smart contracts have init bytecode (constructor bytecode) and runtime bytecode (the code - * executed when the contract is called). This class helps in extracting the runtime bytecode from the - * given init bytecode by searching for specific patterns. - *

- */ -@UtilityClass -public class RuntimeBytecodeExtractor { - - private static final String CODECOPY = "39"; - private static final String RUNTIME_CODE_PREFIX = - "6080"; // The pattern to find the start of the runtime code in the init bytecode - - public static String extractRuntimeBytecode(String initBytecode) { - // Check if the bytecode starts with "0x" and remove it if necessary - if (initBytecode.startsWith(HEX_PREFIX)) { - initBytecode = initBytecode.substring(2); - } - - String runtimeBytecode = getRuntimeBytecode(initBytecode); - - return HEX_PREFIX + runtimeBytecode; // Append "0x" prefix and return - } - - @Nonnull - private static String getRuntimeBytecode(final String initBytecode) { - // Find the first occurrence of "CODECOPY" (39) - int codeCopyIndex = initBytecode.indexOf(CODECOPY); - - if (codeCopyIndex == -1) { - throw new RuntimeException("CODECOPY instruction (39) not found in init bytecode."); - } - - // Find the first occurrence of "6080" after the "CODECOPY" - int runtimeCodePrefixIndex = initBytecode.indexOf(RUNTIME_CODE_PREFIX, codeCopyIndex); - - if (runtimeCodePrefixIndex == -1) { - throw new RuntimeException("Runtime code prefix (6080) not found after CODECOPY."); - } - - // Extract the runtime bytecode starting from the runtimeCodePrefixIndex - return initBytecode.substring(runtimeCodePrefixIndex); - } -} diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/web3j/TestWeb3jService.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/web3j/TestWeb3jService.java index 9c800c79077..ca144549f75 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/web3j/TestWeb3jService.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/web3j/TestWeb3jService.java @@ -30,7 +30,7 @@ import com.hedera.mirror.web3.service.ContractExecutionService; import com.hedera.mirror.web3.service.model.CallServiceParameters; import com.hedera.mirror.web3.service.model.ContractExecutionParameters; -import com.hedera.mirror.web3.utils.RuntimeBytecodeExtractor; +import com.hedera.mirror.web3.utils.BytecodeUtils; import com.hedera.mirror.web3.viewmodel.BlockType; import com.hedera.node.app.service.evm.store.models.HederaEvmAccount; import com.hederahashgraph.api.proto.java.Key.KeyCase; @@ -132,6 +132,12 @@ public T deployWithoutPersist(Deployer deployer) { return deployer.deploy(web3j, credentials, contractGasProvider).send(); } + @SneakyThrows(Exception.class) + public T deployWithoutPersistWithValue(DeployerWithValue deployer, BigInteger value) { + persistContract = false; + return deployer.deploy(web3j, credentials, contractGasProvider, value).send(); + } + @SneakyThrows(Exception.class) public T deployWithValue(DeployerWithValue deployer, BigInteger value) { return deployer.deploy(web3j, credentials, contractGasProvider, value).send(); @@ -170,7 +176,7 @@ private EthSendTransaction sendTopLevelContractCreate( serviceParametersForTopLevelContractCreate(rawTransaction.getData(), ETH_CALL, sender); runtimeCode = contractExecutionService.processCall(serviceParameters); } else { - runtimeCode = RuntimeBytecodeExtractor.extractRuntimeBytecode(rawTransaction.getData()); + runtimeCode = BytecodeUtils.extractRuntimeBytecode(rawTransaction.getData()); } try { final var contractInstance = deployInternal(runtimeCode, persistContract); diff --git a/hedera-mirror-web3/src/test/solidity/TestAddressThis.sol b/hedera-mirror-web3/src/test/solidity/TestAddressThis.sol index 9e78f0447bf..f4a04559cde 100644 --- a/hedera-mirror-web3/src/test/solidity/TestAddressThis.sol +++ b/hedera-mirror-web3/src/test/solidity/TestAddressThis.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.18; contract TestAddressThis { - constructor() { + constructor() payable { address test = address(this); if (test == address(0)) { revert("Zero address.");