From 751b7b5139e227e6def450f2e7842dcc00d06420 Mon Sep 17 00:00:00 2001 From: Ivan Kavaldzhiev Date: Fri, 3 Oct 2025 21:56:52 +0300 Subject: [PATCH 1/5] Return null in case token num is negative Signed-off-by: Ivan Kavaldzhiev --- .../state/keyvalue/TokenReadableKVState.java | 4 ++++ ...ContractCallAirdropSystemContractTest.java | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/web3/src/main/java/org/hiero/mirror/web3/state/keyvalue/TokenReadableKVState.java b/web3/src/main/java/org/hiero/mirror/web3/state/keyvalue/TokenReadableKVState.java index 2a54444e1b6..16d721d9fae 100644 --- a/web3/src/main/java/org/hiero/mirror/web3/state/keyvalue/TokenReadableKVState.java +++ b/web3/src/main/java/org/hiero/mirror/web3/state/keyvalue/TokenReadableKVState.java @@ -72,6 +72,10 @@ protected TokenReadableKVState( @Override protected Token readFromDataSource(@Nonnull TokenID key) { + if (key.tokenNum() < 0) { + return null; + } + final var timestamp = ContractCallContext.get().getTimestamp(); final var entity = commonEntityAccessor.get(key, timestamp).orElse(null); diff --git a/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java b/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java index ee11214bc6b..ec42a0c262a 100644 --- a/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java +++ b/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java @@ -4,6 +4,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.hiero.mirror.common.domain.entity.EntityType.ACCOUNT; import static org.hiero.mirror.common.domain.entity.EntityType.CONTRACT; import static org.hiero.mirror.web3.evm.utils.EvmTokenUtils.toAddress; @@ -108,6 +109,29 @@ void airdropToken(final EntityType receiverType) { verifyContractCall(functionCall, contract); } + @Test + void airdropTokenWithInvalidTokenAddress() { + // Given + final var contract = testWeb3jService.deployWithValue(Airdrop::deploy, DEFAULT_DEPLOYED_CONTRACT_BALANCE); + final var sender = accountEntityPersist(); + final var receiver = persistAirdropReceiver(EntityType.ACCOUNT, e -> {}); + + final var tokenId = fungibleTokenSetup(sender); + tokenAccountPersist(tokenId, receiver.getId()); + final var invalidTokenAddress = "0xa7d9ddbe1f17865597fbd27ec712455208b6b76d"; + + // When + final var functionCall = contract.send_tokenAirdrop( + invalidTokenAddress, + getAddressFromEntity(sender), + toAddress(receiver).toHexString(), + DEFAULT_TOKEN_AIRDROP_AMOUNT, + DEFAULT_TINYBAR_VALUE); + + // Then + assertThatThrownBy(functionCall::send).isInstanceOf(MirrorEvmTransactionException.class); + } + @ParameterizedTest(name = "Airdrop non-fungible token to a(an) {0} that is already associated to it") @MethodSource("receiverData") void airdropNFT(final EntityType receiverType) { From 4447fc3884acf1449d810616e3b890098d1686fc Mon Sep 17 00:00:00 2001 From: Ivan Kavaldzhiev Date: Mon, 6 Oct 2025 13:52:34 +0300 Subject: [PATCH 2/5] Enhance test with negative cases Signed-off-by: Ivan Kavaldzhiev --- ...ContractCallAirdropSystemContractTest.java | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java b/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java index ec42a0c262a..64b98ed4ce7 100644 --- a/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java +++ b/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java @@ -28,13 +28,17 @@ import org.hiero.mirror.web3.web3j.generated.ClaimAirdrop; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.web3j.protocol.core.RemoteFunctionCall; import org.web3j.protocol.core.methods.response.TransactionReceipt; +@ExtendWith({OutputCaptureExtension.class}) class ContractCallAirdropSystemContractTest extends AbstractContractCallServiceTest { private static final BigInteger DEFAULT_DEPLOYED_CONTRACT_BALANCE = BigInteger.valueOf(100_000_000L); @@ -109,8 +113,9 @@ void airdropToken(final EntityType receiverType) { verifyContractCall(functionCall, contract); } - @Test - void airdropTokenWithInvalidTokenAddress() { + @ParameterizedTest(name = "Airdrop token with invalid token address: {0}") + @CsvSource({"0xa7d9ddbe1f17865597fbd27ec712455208b6b76d", "0.0.-1900", "2.1.-1234"}) + void airdropTokenWithInvalidTokenAddress(String invalidTokenId, CapturedOutput output) { // Given final var contract = testWeb3jService.deployWithValue(Airdrop::deploy, DEFAULT_DEPLOYED_CONTRACT_BALANCE); final var sender = accountEntityPersist(); @@ -118,7 +123,7 @@ void airdropTokenWithInvalidTokenAddress() { final var tokenId = fungibleTokenSetup(sender); tokenAccountPersist(tokenId, receiver.getId()); - final var invalidTokenAddress = "0xa7d9ddbe1f17865597fbd27ec712455208b6b76d"; + final var invalidTokenAddress = parseTokenIdToAddress(invalidTokenId); // When final var functionCall = contract.send_tokenAirdrop( @@ -130,6 +135,7 @@ void airdropTokenWithInvalidTokenAddress() { // Then assertThatThrownBy(functionCall::send).isInstanceOf(MirrorEvmTransactionException.class); + assertThat(output.getAll()).doesNotContain("InvalidEntityException"); } @ParameterizedTest(name = "Airdrop non-fungible token to a(an) {0} that is already associated to it") @@ -892,4 +898,26 @@ private void verifyContractCallWithException(final RemoteFunctionCall functio assertThrows(PrecompileNotSupportedException.class, functionCall::send); } } + + /** + * Parses a token ID to string + * + * @param tokenId the token ID in format "shard.realm.num" or EVM address + * @return the token address string + */ + private String parseTokenIdToAddress(String tokenId) { + if (tokenId.startsWith("0x")) { + return tokenId; + } + + // Parse shard.realm.num format + String[] parts = tokenId.split("\\."); + + long shard = Long.parseLong(parts[0]); + long realm = Long.parseLong(parts[1]); + long num = Long.parseLong(parts[2]); + + // Convert to hex address format + return String.format("0x%08x%016x%016x", (int) shard, realm, num); + } } From 1e6daf52e86e7c6f61214050b237f37b6b44d9f5 Mon Sep 17 00:00:00 2001 From: Ivan Kavaldzhiev Date: Mon, 6 Oct 2025 14:58:55 +0300 Subject: [PATCH 3/5] Add negative test case with very large token number Signed-off-by: Ivan Kavaldzhiev --- .../state/keyvalue/TokenReadableKVState.java | 2 +- ...ContractCallAirdropSystemContractTest.java | 42 ++++++++++--------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/web3/src/main/java/org/hiero/mirror/web3/state/keyvalue/TokenReadableKVState.java b/web3/src/main/java/org/hiero/mirror/web3/state/keyvalue/TokenReadableKVState.java index 16d721d9fae..67c684cf461 100644 --- a/web3/src/main/java/org/hiero/mirror/web3/state/keyvalue/TokenReadableKVState.java +++ b/web3/src/main/java/org/hiero/mirror/web3/state/keyvalue/TokenReadableKVState.java @@ -72,7 +72,7 @@ protected TokenReadableKVState( @Override protected Token readFromDataSource(@Nonnull TokenID key) { - if (key.tokenNum() < 0) { + if (key.tokenNum() < 0 || key.tokenNum() > 274877906943L) { return null; } diff --git a/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java b/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java index 64b98ed4ce7..8270a6ff851 100644 --- a/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java +++ b/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java @@ -114,28 +114,30 @@ void airdropToken(final EntityType receiverType) { } @ParameterizedTest(name = "Airdrop token with invalid token address: {0}") - @CsvSource({"0xa7d9ddbe1f17865597fbd27ec712455208b6b76d", "0.0.-1900", "2.1.-1234"}) + @CsvSource({"0xa7d9ddbe1f17865597fbd27ec712455208b6b76d", "0.0.-1900", "2.1.-1234", "0.0.5901004952499928656"}) void airdropTokenWithInvalidTokenAddress(String invalidTokenId, CapturedOutput output) { - // Given - final var contract = testWeb3jService.deployWithValue(Airdrop::deploy, DEFAULT_DEPLOYED_CONTRACT_BALANCE); - final var sender = accountEntityPersist(); - final var receiver = persistAirdropReceiver(EntityType.ACCOUNT, e -> {}); - - final var tokenId = fungibleTokenSetup(sender); - tokenAccountPersist(tokenId, receiver.getId()); - final var invalidTokenAddress = parseTokenIdToAddress(invalidTokenId); - - // When - final var functionCall = contract.send_tokenAirdrop( - invalidTokenAddress, - getAddressFromEntity(sender), - toAddress(receiver).toHexString(), - DEFAULT_TOKEN_AIRDROP_AMOUNT, - DEFAULT_TINYBAR_VALUE); + if (mirrorNodeEvmProperties.isModularizedServices()) { + // Given + final var contract = testWeb3jService.deployWithValue(Airdrop::deploy, DEFAULT_DEPLOYED_CONTRACT_BALANCE); + final var sender = accountEntityPersist(); + final var receiver = persistAirdropReceiver(EntityType.ACCOUNT, e -> {}); - // Then - assertThatThrownBy(functionCall::send).isInstanceOf(MirrorEvmTransactionException.class); - assertThat(output.getAll()).doesNotContain("InvalidEntityException"); + final var tokenId = fungibleTokenSetup(sender); + tokenAccountPersist(tokenId, receiver.getId()); + final var invalidTokenAddress = parseTokenIdToAddress(invalidTokenId); + + // When + final var functionCall = contract.send_tokenAirdrop( + invalidTokenAddress, + getAddressFromEntity(sender), + toAddress(receiver).toHexString(), + DEFAULT_TOKEN_AIRDROP_AMOUNT, + DEFAULT_TINYBAR_VALUE); + + // Then + assertThatThrownBy(functionCall::send).isInstanceOf(MirrorEvmTransactionException.class); + assertThat(output.getAll()).doesNotContain("InvalidEntityException"); + } } @ParameterizedTest(name = "Airdrop non-fungible token to a(an) {0} that is already associated to it") From 6621903d5673acc9f27f615affdc445f723521fa Mon Sep 17 00:00:00 2001 From: Ivan Kavaldzhiev Date: Tue, 7 Oct 2025 18:09:17 +0300 Subject: [PATCH 4/5] Resolve PR comments Signed-off-by: Ivan Kavaldzhiev --- .../org/hiero/mirror/web3/state/CommonEntityAccessor.java | 7 ++++++- .../mirror/web3/state/keyvalue/TokenReadableKVState.java | 4 ---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/web3/src/main/java/org/hiero/mirror/web3/state/CommonEntityAccessor.java b/web3/src/main/java/org/hiero/mirror/web3/state/CommonEntityAccessor.java index e779bca89dd..373e532502f 100644 --- a/web3/src/main/java/org/hiero/mirror/web3/state/CommonEntityAccessor.java +++ b/web3/src/main/java/org/hiero/mirror/web3/state/CommonEntityAccessor.java @@ -13,6 +13,7 @@ import lombok.RequiredArgsConstructor; import org.hiero.mirror.common.domain.entity.Entity; import org.hiero.mirror.common.domain.entity.EntityId; +import org.hiero.mirror.common.exception.InvalidEntityException; import org.hiero.mirror.web3.repository.EntityRepository; @Named @@ -35,7 +36,11 @@ public class CommonEntityAccessor { } public @Nonnull Optional get(@Nonnull final TokenID tokenID, final Optional timestamp) { - return get(toEntityId(tokenID), timestamp); + try { + return get(toEntityId(tokenID), timestamp); + } catch (final InvalidEntityException e) { + return Optional.empty(); + } } public @Nonnull Optional get(@Nonnull final EntityId entityId, final Optional timestamp) { diff --git a/web3/src/main/java/org/hiero/mirror/web3/state/keyvalue/TokenReadableKVState.java b/web3/src/main/java/org/hiero/mirror/web3/state/keyvalue/TokenReadableKVState.java index 67c684cf461..2a54444e1b6 100644 --- a/web3/src/main/java/org/hiero/mirror/web3/state/keyvalue/TokenReadableKVState.java +++ b/web3/src/main/java/org/hiero/mirror/web3/state/keyvalue/TokenReadableKVState.java @@ -72,10 +72,6 @@ protected TokenReadableKVState( @Override protected Token readFromDataSource(@Nonnull TokenID key) { - if (key.tokenNum() < 0 || key.tokenNum() > 274877906943L) { - return null; - } - final var timestamp = ContractCallContext.get().getTimestamp(); final var entity = commonEntityAccessor.get(key, timestamp).orElse(null); From 48f5ee5509254078135bfed20e204a31f7cc0b63 Mon Sep 17 00:00:00 2001 From: Ivan Kavaldzhiev Date: Tue, 7 Oct 2025 21:05:11 +0300 Subject: [PATCH 5/5] Resolve PR comment Signed-off-by: Ivan Kavaldzhiev --- .../service/ContractCallAirdropSystemContractTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java b/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java index 8270a6ff851..a8d991f8749 100644 --- a/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java +++ b/web3/src/test/java/org/hiero/mirror/web3/service/ContractCallAirdropSystemContractTest.java @@ -15,12 +15,14 @@ import java.util.List; import java.util.function.Consumer; import java.util.stream.Stream; +import org.apache.tuweni.bytes.Bytes; import org.hiero.mirror.common.domain.entity.Entity; import org.hiero.mirror.common.domain.entity.EntityId; import org.hiero.mirror.common.domain.entity.EntityType; import org.hiero.mirror.common.domain.token.FixedFee; import org.hiero.mirror.common.domain.token.FractionalFee; import org.hiero.mirror.common.domain.token.TokenFreezeStatusEnum; +import org.hiero.mirror.common.util.DomainUtils; import org.hiero.mirror.web3.evm.exception.PrecompileNotSupportedException; import org.hiero.mirror.web3.exception.MirrorEvmTransactionException; import org.hiero.mirror.web3.utils.BytecodeUtils; @@ -915,11 +917,8 @@ private String parseTokenIdToAddress(String tokenId) { // Parse shard.realm.num format String[] parts = tokenId.split("\\."); - long shard = Long.parseLong(parts[0]); - long realm = Long.parseLong(parts[1]); long num = Long.parseLong(parts[2]); - // Convert to hex address format - return String.format("0x%08x%016x%016x", (int) shard, realm, num); + return Bytes.wrap(DomainUtils.toEvmAddress(num)).toHexString(); } }