Skip to content

Commit

Permalink
Fix contract deployment with value (0.117) (#9703)
Browse files Browse the repository at this point in the history
fix: Fix contract deployment with value (#9668)

This PR fixes contract deployment when the contract deployment is made with value.

This PR modifies :
Modified ContractCallRequest - change the 'to' validation to check if the call is for contract deployment

BytecodeUtils - Added a new method isInitBytecode which checks if a given data is an init bytecode - needed in ContractCallRequest validation

ContractControllerTest - Added new test and modified a few existing ones based on the new changes to the validation of the to field.

Modified ContractCallAddressThisTest - added tests for deployment of payable contract with and without value
Modified ContractCallServiceERCTokenModificationFunctionsTest - added tests for deployment of non payable contract with and without value.

---------

Signed-off-by: Kristiyan Selveliev <[email protected]>
  • Loading branch information
kselveliev authored Nov 4, 2024
1 parent 3b1bb87 commit aaca64d
Show file tree
Hide file tree
Showing 9 changed files with 360 additions and 85 deletions.
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* </p>
*/
@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.
* <p>
* 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.
*
* <p>
* 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -59,15 +88,16 @@ 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);
}

@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<Bytes> capturedOutputs = new ArrayList<>();
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
}
Loading

0 comments on commit aaca64d

Please sign in to comment.