diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoTransferHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoTransferHandler.java index 5854e3435700..82834bb17131 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoTransferHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoTransferHandler.java @@ -510,9 +510,13 @@ private void checkSender( // If the sender account is immutable, then we throw an exception. final var key = senderAccount.key(); if (key == null || !isValid(key)) { - // If the sender account has no key, then fail with INVALID_ACCOUNT_ID. - // NOTE: should change to ACCOUNT_IS_IMMUTABLE - throw new PreCheckException(INVALID_ACCOUNT_ID); + if (isHollow(senderAccount)) { + meta.requireSignatureForHollowAccount(senderAccount); + } else { + // If the sender account has no key, then fail with INVALID_ACCOUNT_ID. + // NOTE: should change to ACCOUNT_IS_IMMUTABLE + throw new PreCheckException(INVALID_ACCOUNT_ID); + } } else if (!nftTransfer.isApproval()) { meta.requireKey(key); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoTransferSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoTransferSuite.java index 4a418830a9d1..55a04663f50b 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoTransferSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoTransferSuite.java @@ -17,6 +17,7 @@ package com.hedera.services.bdd.suites.crypto; import static com.google.protobuf.ByteString.copyFromUtf8; +import static com.hedera.node.app.service.evm.utils.EthSigsUtils.recoverAddressFromPubKey; import static com.hedera.services.bdd.junit.TestTags.CRYPTO; import static com.hedera.services.bdd.spec.HapiPropertySource.accountIdFromHexedMirrorAddress; import static com.hedera.services.bdd.spec.HapiPropertySource.asAccountString; @@ -35,9 +36,11 @@ import static com.hedera.services.bdd.spec.keys.KeyShape.threshOf; import static com.hedera.services.bdd.spec.keys.SigControl.OFF; import static com.hedera.services.bdd.spec.keys.SigControl.ON; +import static com.hedera.services.bdd.spec.keys.TrieSigMapGenerator.uniqueWithFullPrefixesFor; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountDetails; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAliasedAccountInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getReceipt; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; @@ -105,6 +108,8 @@ import static com.hedera.services.bdd.suites.HapiSuite.NODE_REWARD; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_MILLION_HBARS; +import static com.hedera.services.bdd.suites.HapiSuite.SECP_256K1_SHAPE; import static com.hedera.services.bdd.suites.HapiSuite.STAKING_REWARD; import static com.hedera.services.bdd.suites.HapiSuite.TOKEN_TREASURY; import static com.hedera.services.bdd.suites.contract.Utils.aaWith; @@ -112,6 +117,8 @@ import static com.hedera.services.bdd.suites.contract.Utils.captureOneChildCreate2MetaFor; import static com.hedera.services.bdd.suites.contract.Utils.mirrorAddrWith; import static com.hedera.services.bdd.suites.contract.Utils.ocWith; +import static com.hedera.services.bdd.suites.crypto.AutoAccountCreationSuite.A_TOKEN; +import static com.hedera.services.bdd.suites.crypto.AutoAccountCreationSuite.PARTY; import static com.hedera.services.bdd.suites.file.FileUpdateSuite.CIVILIAN; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_FROZEN_FOR_TOKEN; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN; @@ -2297,4 +2304,67 @@ final Stream customFeesCannotCauseOverflow() { .then(cryptoTransfer(moving(1L, "ft").between(PARTY, COUNTERPARTY)) .hasKnownStatus(INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE)); } + + @HapiTest + final Stream createHollowAccountWithNftTransferAndCompleteIt() { + final var tokenA = "tokenA"; + final var hollowAccountKey = "hollowAccountKey"; + final var transferTokenAAndBToHollowAccountTxn = "transferTokenAToHollowAccountTxn"; + final AtomicReference tokenIdA = new AtomicReference<>(); + final AtomicReference treasuryAlias = new AtomicReference<>(); + final AtomicReference hollowAccountAlias = new AtomicReference<>(); + + return defaultHapiSpec("createHollowAccountWithNftTransferAndCompleteIt") + .given( + newKeyNamed(hollowAccountKey).shape(SECP_256K1_SHAPE), + newKeyNamed(MULTI_KEY), + cryptoCreate(TREASURY).balance(10_000 * ONE_MILLION_HBARS), + tokenCreate(tokenA) + .tokenType(NON_FUNGIBLE_UNIQUE) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .treasury(TREASURY), + // Mint NFTs + mintToken(tokenA, List.of(ByteString.copyFromUtf8("metadata1"))), + mintToken(tokenA, List.of(ByteString.copyFromUtf8("metadata2"))), + withOpContext((spec, opLog) -> { + final var registry = spec.registry(); + final var treasuryAccountId = registry.getAccountID(TREASURY); + treasuryAlias.set(ByteString.copyFrom(asSolidityAddress(treasuryAccountId))); + + // Save the alias for the hollow account + final var ecdsaKey = spec.registry() + .getKey(hollowAccountKey) + .getECDSASecp256K1() + .toByteArray(); + final var evmAddressBytes = ByteString.copyFrom(recoverAddressFromPubKey(ecdsaKey)); + hollowAccountAlias.set(evmAddressBytes); + tokenIdA.set(registry.getTokenID(A_TOKEN)); + })) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + // Create hollow account + cryptoTransfer((s, b) -> b.addTokenTransfers(TokenTransferList.newBuilder() + .setToken(tokenIdA.get()) + .addNftTransfers(ocWith( + accountId(treasuryAlias.get()), + accountId(hollowAccountAlias.get()), + 1L)))) + .payingWith(TREASURY) + .signedBy(TREASURY) + .via(transferTokenAAndBToHollowAccountTxn), + // Verify hollow account creation + getAliasedAccountInfo(hollowAccountKey) + .hasToken(relationshipWith(tokenA)) + .has(accountWith().hasEmptyKey()) + .exposingIdTo(id -> spec.registry().saveAccountId(hollowAccountKey, id)), + // Transfer some hbars to the hollow account so that it could pay the next transaction + cryptoTransfer(movingHbar(ONE_MILLION_HBARS).between(TREASURY, hollowAccountKey)), + // Send transfer to complete the hollow account + cryptoTransfer(movingUnique(tokenA, 1L).between(hollowAccountKey, TREASURY)) + .payingWith(hollowAccountKey) + .signedBy(hollowAccountKey, TREASURY) + .sigMapPrefixes(uniqueWithFullPrefixesFor(hollowAccountKey))))) + .then(getAliasedAccountInfo(hollowAccountKey).has(accountWith().key(hollowAccountKey))); + } }