diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/AbstractContractCallServiceTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/AbstractContractCallServiceTest.java index b695a12675..c840cee80c 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/AbstractContractCallServiceTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/AbstractContractCallServiceTest.java @@ -16,6 +16,8 @@ package com.hedera.mirror.web3.service; +import static com.hedera.mirror.web3.service.model.CallServiceParameters.CallType.ETH_CALL; +import static com.hedera.mirror.web3.service.model.CallServiceParameters.CallType.ETH_ESTIMATE_GAS; import static com.hedera.mirror.web3.utils.ContractCallTestUtil.ESTIMATE_GAS_ERROR_MESSAGE; import static com.hedera.mirror.web3.utils.ContractCallTestUtil.TRANSACTION_GAS_LIMIT; import static com.hedera.mirror.web3.utils.ContractCallTestUtil.isWithinExpectedGasRange; @@ -211,15 +213,24 @@ protected ContractExecutionParameters getContractExecutionParameters( protected ContractExecutionParameters getContractExecutionParameters( final Bytes data, final Address receiver, final Address payerAddress, final long value) { + return getContractExecutionParameters(data, receiver, payerAddress, value, ETH_CALL); + } + + protected ContractExecutionParameters getContractExecutionParameters( + final Bytes data, + final Address receiverAddress, + final Address senderAddress, + final long value, + final CallType callType) { return ContractExecutionParameters.builder() .block(BlockType.LATEST) .callData(data) - .callType(CallType.ETH_CALL) + .callType(callType) .gas(TRANSACTION_GAS_LIMIT) - .isEstimate(false) + .isEstimate(callType == ETH_ESTIMATE_GAS) .isStatic(false) - .receiver(receiver) - .sender(new HederaEvmAccount(payerAddress)) + .receiver(receiverAddress) + .sender(new HederaEvmAccount(senderAddress)) .value(value) .build(); } diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallServiceTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallServiceTest.java index b4353618dd..3bba7e9e4a 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallServiceTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallServiceTest.java @@ -16,6 +16,7 @@ package com.hedera.mirror.web3.service; +import static com.hedera.hapi.node.base.ResponseCodeEnum.PAYER_ACCOUNT_NOT_FOUND; import static com.hedera.mirror.common.util.DomainUtils.toEvmAddress; import static com.hedera.mirror.web3.evm.utils.EvmTokenUtils.toAddress; import static com.hedera.mirror.web3.exception.BlockNumberNotFoundException.UNKNOWN_BLOCK_NUMBER; @@ -337,14 +338,29 @@ void estimateGasForPureCall() { verifyEthCallAndEstimateGas(functionCall, contract); } + /** + * If we make a contract call with the zero address (0x0) set as the recipient in the contractExecutionParameters, + * Hedera treats this as a contract creation request rather than a function call. + * The contract is then initialized using the contractCallData, which should contain the compiled bytecode of the + * contract. If contractCallData is empty (0x0), the contract will be deployed without any functions(no fallback + * function as well, which is called when the contract is called without specifying a function or when non-existent + * function is specified.) in its bytecode. Respectively, any call to the contract will fail with + * CONTRACT_BYTECODE_EMPTY, indicating that the contract exists, but does not have any executable logic. + */ @Test void estimateGasWithoutReceiver() { - // Given - final var serviceParametersEthCall = - getContractExecutionParameters(Bytes.fromHexString(HEX_PREFIX), Address.ZERO, ETH_CALL); + final var payer = accountEntityPersist(); + final var receiverAddress = Address.ZERO; + + final var contract = testWeb3jService.deployWithoutPersist(ERCTestContract::deploy); + final var contractCallData = Bytes.fromHexString(contract.getContractBinary()); + + final var serviceParametersEthCall = getContractExecutionParameters( + contractCallData, toAddress(payer.toEntityId()), receiverAddress, 0L, ETH_CALL); + final var actualGasUsed = gasUsedAfterExecution(serviceParametersEthCall); - final var serviceParametersEstimateGas = - getContractExecutionParameters(Bytes.fromHexString(HEX_PREFIX), Address.ZERO, ETH_ESTIMATE_GAS); + final var serviceParametersEstimateGas = getContractExecutionParameters( + contractCallData, toAddress(payer.toEntityId()), receiverAddress, 0L, ETH_ESTIMATE_GAS); // When final var result = contractExecutionService.processCall(serviceParametersEstimateGas); @@ -561,13 +577,14 @@ void nonExistingFunctionCallWithFallback() { } @Test - void ethCallWithValueAndNotExistingSenderAlias() { + void ethCallWithValueAndSenderWithoutAlias() { // Given - final var receiver = accountEntityWithEvmAddressPersist(); - final var receiverAddress = getAliasAddressFromEntity(receiver); - final var notExistingSenderAlias = Address.fromHexString("0x6b175474e89094c44da98b954eedeac495271d0f"); - final var serviceParameters = - getContractExecutionParametersWithValue(Bytes.EMPTY, notExistingSenderAlias, receiverAddress, 10L); + final var receiverEntity = accountEntityWithEvmAddressPersist(); + final var receiverAddress = getAliasAddressFromEntity(receiverEntity); + final var payer = accountEntityPersist(); // Account without alias + + final var serviceParameters = getContractExecutionParametersWithValue( + Bytes.EMPTY, toAddress(payer.toEntityId()), receiverAddress, 10L); // When final var result = contractExecutionService.processCall(serviceParameters); @@ -577,6 +594,27 @@ void ethCallWithValueAndNotExistingSenderAlias() { assertGasLimit(serviceParameters); } + @Test + void ethCallWithValueAndNotExistingSenderAddress() { + final var receiverEntity = accountEntityWithEvmAddressPersist(); + final var receiverAddress = getAliasAddressFromEntity(receiverEntity); + final var notExistingAccountAddress = toAddress(EntityId.of(4325)); + + final var serviceParameters = + getContractExecutionParametersWithValue(Bytes.EMPTY, notExistingAccountAddress, receiverAddress, 10L); + + if (mirrorNodeEvmProperties.isModularizedServices()) { + assertThatThrownBy(() -> contractExecutionService.processCall(serviceParameters)) + .isInstanceOf(MirrorEvmTransactionException.class) + .hasMessage(PAYER_ACCOUNT_NOT_FOUND.name()); + } else { + final var result = contractExecutionService.processCall(serviceParameters); + assertThat(result).isEqualTo(HEX_PREFIX); + } + + assertGasLimit(serviceParameters); + } + @Test void invalidFunctionSig() { // Given @@ -1100,7 +1138,8 @@ private Entity systemAccountEntityWithEvmAddressPersist() { @Nested class EVM46Validation { - private static final Address NON_EXISTING_ADDRESS = toAddress(123456789); + private static final Address NON_EXISTING_ADDRESS = + Address.fromHexString("0xa7d9ddbe1f17865597fbd27ec712455208b6b76d"); @Test void callToNonExistingContract() { @@ -1118,8 +1157,12 @@ void callToNonExistingContract() { @Test void transferToNonExistingContract() { // Given - final var serviceParameters = - getContractExecutionParametersWithValue(Bytes.EMPTY, NON_EXISTING_ADDRESS, 1L); + final var payer = accountEntityWithEvmAddressPersist(); + + // The NON_EXISTING_ADDRESS should be a valid EVM alias key(Ethereum-style address derived from an ECDSA + // public key), otherwise INVALID_ALIAS_KEY could be thrown + final var serviceParameters = getContractExecutionParametersWithValue( + Bytes.EMPTY, getAliasAddressFromEntity(payer), NON_EXISTING_ADDRESS, 1L); // When final var result = contractExecutionService.processCall(serviceParameters);