diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/grantapproval/AbstractGrantApprovalCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/grantapproval/AbstractGrantApprovalCall.java index e196f39346bd..66ffb61d5519 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/grantapproval/AbstractGrantApprovalCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/grantapproval/AbstractGrantApprovalCall.java @@ -16,11 +16,16 @@ package com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.grantapproval; +import static java.util.Objects.requireNonNull; + import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.base.TokenType; +import com.hedera.hapi.node.state.token.AccountApprovalForAllAllowance; import com.hedera.hapi.node.token.CryptoApproveAllowanceTransactionBody; +import com.hedera.hapi.node.token.CryptoDeleteAllowanceTransactionBody; import com.hedera.hapi.node.token.NftAllowance; +import com.hedera.hapi.node.token.NftRemoveAllowance; import com.hedera.hapi.node.token.TokenAllowance; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.contract.impl.exec.gas.SystemContractGasCalculator; @@ -29,6 +34,7 @@ import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater.Enhancement; import edu.umd.cs.findbugs.annotations.NonNull; import java.math.BigInteger; +import java.util.List; public abstract class AbstractGrantApprovalCall extends AbstractHtsCall { protected final VerificationStrategy verificationStrategy; @@ -60,22 +66,60 @@ protected AbstractGrantApprovalCall( } public TransactionBody callGrantApproval() { - return TransactionBody.newBuilder() - .cryptoApproveAllowance(approve(token, spender, amount, tokenType)) + if (tokenType == TokenType.NON_FUNGIBLE_UNIQUE) { + var ownerId = getOwnerId(); + + if (ownerId != null && !isNftApprovalRevocation()) { + List accountApprovalForAllAllowances = enhancement + .nativeOperations() + .getAccount(ownerId.accountNum()) + .approveForAllNftAllowances(); + if (accountApprovalForAllAllowances != null) { + for (var approvedForAll : accountApprovalForAllAllowances) { + if (approvedForAll.tokenId().equals(token)) { + return buildCryptoApproveAllowance(approveDelegate(ownerId, approvedForAll.spenderId())); + } + } + } + } + + return isNftApprovalRevocation() + ? buildCryptoDeleteAllowance(remove(ownerId)) + : buildCryptoApproveAllowance(approve(ownerId)); + } else { + return buildCryptoApproveAllowance(approve(senderId)); + } + } + + private CryptoDeleteAllowanceTransactionBody remove(AccountID ownerId) { + return CryptoDeleteAllowanceTransactionBody.newBuilder() + .nftAllowances(NftRemoveAllowance.newBuilder() + .tokenId(token) + .owner(ownerId) + .serialNumbers(amount.longValue()) + .build()) .build(); } - private CryptoApproveAllowanceTransactionBody approve( - @NonNull final TokenID token, - @NonNull final AccountID spender, - @NonNull final BigInteger amount, - @NonNull final TokenType tokenType) { + private CryptoApproveAllowanceTransactionBody approveDelegate(AccountID ownerId, AccountID delegateSpenderId) { + return CryptoApproveAllowanceTransactionBody.newBuilder() + .nftAllowances(NftAllowance.newBuilder() + .tokenId(token) + .spender(spender) + .delegatingSpender(delegateSpenderId) + .owner(ownerId) + .serialNumbers(amount.longValue()) + .build()) + .build(); + } + + private CryptoApproveAllowanceTransactionBody approve(AccountID ownerId) { return tokenType.equals(TokenType.FUNGIBLE_COMMON) ? CryptoApproveAllowanceTransactionBody.newBuilder() .tokenAllowances(TokenAllowance.newBuilder() .tokenId(token) .spender(spender) - .owner(senderId) + .owner(ownerId) .amount(amount.longValue()) .build()) .build() @@ -83,9 +127,29 @@ private CryptoApproveAllowanceTransactionBody approve( .nftAllowances(NftAllowance.newBuilder() .tokenId(token) .spender(spender) - .owner(senderId) + .owner(ownerId) .serialNumbers(amount.longValue()) .build()) .build(); } + + private TransactionBody buildCryptoDeleteAllowance(CryptoDeleteAllowanceTransactionBody body) { + return TransactionBody.newBuilder().cryptoDeleteAllowance(body).build(); + } + + private TransactionBody buildCryptoApproveAllowance(CryptoApproveAllowanceTransactionBody body) { + return TransactionBody.newBuilder().cryptoApproveAllowance(body).build(); + } + + private AccountID getOwnerId() { + final var nft = enhancement.nativeOperations().getNft(token.tokenNum(), amount.longValue()); + requireNonNull(nft); + return nft.hasOwnerId() + ? nft.ownerId() + : enhancement.nativeOperations().getToken(token.tokenNum()).treasuryAccountId(); + } + + private boolean isNftApprovalRevocation() { + return spender.accountNum() == 0; + } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/grantapproval/ERCGrantApprovalCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/grantapproval/ERCGrantApprovalCall.java index e800efb0d6c5..f4ffb552021e 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/grantapproval/ERCGrantApprovalCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/grantapproval/ERCGrantApprovalCall.java @@ -16,10 +16,14 @@ package com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.grantapproval; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_SPENDER_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.FullResult.revertResult; import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.FullResult.successResult; +import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.HtsSystemContract.HTS_EVM_ADDRESS; import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCall.PricedResult.gasOnly; +import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.asEvmContractId; +import static com.hedera.node.app.service.contract.impl.utils.SystemContractUtils.contractFunctionResultFailedForProto; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ResponseCodeEnum; @@ -28,10 +32,14 @@ import com.hedera.node.app.service.contract.impl.exec.gas.DispatchType; import com.hedera.node.app.service.contract.impl.exec.gas.SystemContractGasCalculator; import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategy; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.FullResult; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.ReturnTypes; import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater.Enhancement; import com.hedera.node.app.spi.workflows.record.SingleTransactionRecordBuilder; +import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; import java.math.BigInteger; +import org.hyperledger.besu.datatypes.Address; public class ERCGrantApprovalCall extends AbstractGrantApprovalCall { @@ -53,7 +61,22 @@ public PricedResult execute() { if (token == null) { return reversionWith(INVALID_TOKEN_ID, gasCalculator.canonicalGasRequirement(DispatchType.APPROVE)); } + final var spenderAccount = enhancement.nativeOperations().getAccount(spender.accountNum()); final var body = callGrantApproval(); + if (spenderAccount == null && spender.accountNum() != 0) { + var gasRequirement = gasCalculator.canonicalGasRequirement(DispatchType.APPROVE); + var revertResult = FullResult.revertResult(INVALID_ALLOWANCE_SPENDER_ID, gasRequirement); + var result = gasOnly(revertResult, INVALID_ALLOWANCE_SPENDER_ID, false); + + var contractID = asEvmContractId(Address.fromHexString(HTS_EVM_ADDRESS)); + var encodedRc = ReturnTypes.encodedRc(INVALID_ALLOWANCE_SPENDER_ID).array(); + var contractFunctionResult = contractFunctionResultFailedForProto( + gasRequirement, INVALID_ALLOWANCE_SPENDER_ID.protoName(), contractID, Bytes.wrap(encodedRc)); + + enhancement.systemOperations().externalizeResult(contractFunctionResult, INVALID_ALLOWANCE_SPENDER_ID); + + return result; + } final var recordBuilder = systemContractOperations() .dispatch(body, verificationStrategy, senderId, SingleTransactionRecordBuilder.class); final var gasRequirement = gasCalculator.gasRequirement(body, DispatchType.APPROVE, senderId); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/utils/SystemContractUtils.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/utils/SystemContractUtils.java index d8a39596917b..75259e46262c 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/utils/SystemContractUtils.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/utils/SystemContractUtils.java @@ -90,6 +90,29 @@ public static ContractFunctionResult contractFunctionResultFailedFor( .build(); } + /** + * Create an error contract function result. + * + * @param gasUsed Report the gas used. + * @param errorMsg The error message to report back to the caller. + * @param contractID The contract ID. + * @param contractCallResult Bytes representation of the contract call result error + * @return The created contract function result when for a failed call. + */ + @NonNull + public static ContractFunctionResult contractFunctionResultFailedForProto( + final long gasUsed, + final String errorMsg, + final ContractID contractID, + final com.hedera.pbj.runtime.io.buffer.Bytes contractCallResult) { + return ContractFunctionResult.newBuilder() + .gasUsed(gasUsed) + .contractID(contractID) + .errorMessage(errorMsg) + .contractCallResult(contractCallResult) + .build(); + } + private static ContractID contractIdFromEvmAddress(final byte[] bytes) { return ContractID.newBuilder() .contractNum(Longs.fromByteArray(Arrays.copyOfRange(bytes, 12, 20))) diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/TestHelpers.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/TestHelpers.java index 95c487da9287..fc7317d1f91e 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/TestHelpers.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/TestHelpers.java @@ -573,6 +573,8 @@ public class TestHelpers { public static final Address OWNER_BESU_ADDRESS = pbjToBesuAddress(OWNER_ADDRESS); public static final AccountID UNAUTHORIZED_SPENDER_ID = AccountID.newBuilder().accountNum(999999L).build(); + public static final AccountID REVOKE_APPROVAL_SPENDER_ID = + AccountID.newBuilder().accountNum(0L).build(); public static final Bytes UNAUTHORIZED_SPENDER_ADDRESS = Bytes.fromHex("b284224b8b83a724438cc3cc7c0d333a2b6b3222"); public static final com.esaulpaugh.headlong.abi.Address UNAUTHORIZED_SPENDER_HEADLONG_ADDRESS = asHeadlongAddress(UNAUTHORIZED_SPENDER_ADDRESS.toByteArray()); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/grantapproval/ClassicGrantApprovalCallTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/grantapproval/ClassicGrantApprovalCallTest.java index b715def87aed..d7c8b3c3347e 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/grantapproval/ClassicGrantApprovalCallTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/grantapproval/ClassicGrantApprovalCallTest.java @@ -27,6 +27,9 @@ import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.TokenType; +import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.state.token.Nft; +import com.hedera.hapi.node.state.token.Token; import com.hedera.node.app.service.contract.impl.exec.gas.SystemContractGasCalculator; import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategy; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.grantapproval.ClassicGrantApprovalCall; @@ -51,6 +54,15 @@ public class ClassicGrantApprovalCallTest extends HtsCallTestBase { @Mock private SystemContractGasCalculator systemContractGasCalculator; + @Mock + private Nft nft; + + @Mock + private Token token; + + @Mock + private Account account; + @Test void fungibleApprove() { subject = new ClassicGrantApprovalCall( @@ -87,6 +99,10 @@ void nftApprove() { TokenType.NON_FUNGIBLE_UNIQUE); given(systemContractOperations.dispatch(any(), any(), any(), any())).willReturn(recordBuilder); given(recordBuilder.status()).willReturn(ResponseCodeEnum.SUCCESS); + given(nativeOperations.getNft(NON_FUNGIBLE_TOKEN_ID.tokenNum(), 100L)).willReturn(nft); + given(nativeOperations.getToken(NON_FUNGIBLE_TOKEN_ID.tokenNum())).willReturn(token); + given(token.treasuryAccountId()).willReturn(OWNER_ID); + given(nativeOperations.getAccount(OWNER_ID.accountNum())).willReturn(account); final var result = subject.execute(frame).fullResult().result(); assertEquals(MessageFrame.State.COMPLETED_SUCCESS, result.getState()); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/grantapproval/ERCGrantApprovalCallTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/grantapproval/ERCGrantApprovalCallTest.java index 5a087c080276..f413496bdd35 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/grantapproval/ERCGrantApprovalCallTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/grantapproval/ERCGrantApprovalCallTest.java @@ -19,15 +19,20 @@ import static com.hedera.node.app.service.contract.impl.test.TestHelpers.FUNGIBLE_TOKEN_ID; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.NON_FUNGIBLE_TOKEN_ID; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.OWNER_ID; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.REVOKE_APPROVAL_SPENDER_ID; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.UNAUTHORIZED_SPENDER_ID; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.asBytesResult; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.TokenType; +import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.state.token.Nft; +import com.hedera.hapi.node.state.token.Token; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.contract.impl.exec.gas.SystemContractGasCalculator; import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategy; @@ -37,7 +42,9 @@ import com.hedera.node.app.service.token.records.CryptoTransferRecordBuilder; import com.hedera.node.app.spi.workflows.record.SingleTransactionRecordBuilder; import java.math.BigInteger; +import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.frame.MessageFrame.State; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -54,6 +61,15 @@ class ERCGrantApprovalCallTest extends HtsCallTestBase { @Mock private CryptoTransferRecordBuilder recordBuilder; + @Mock + private Nft nft; + + @Mock + private Token token; + + @Mock + private Account account; + @Test void erc20approve() { subject = new ERCGrantApprovalCall( @@ -72,6 +88,7 @@ void erc20approve() { eq(SingleTransactionRecordBuilder.class))) .willReturn(recordBuilder); given(recordBuilder.status()).willReturn(ResponseCodeEnum.SUCCESS); + given(nativeOperations.getAccount(anyLong())).willReturn(account); final var result = subject.execute().fullResult().result(); assertEquals(MessageFrame.State.COMPLETED_SUCCESS, result.getState()); @@ -99,6 +116,67 @@ void erc721approve() { eq(SingleTransactionRecordBuilder.class))) .willReturn(recordBuilder); given(recordBuilder.status()).willReturn(ResponseCodeEnum.SUCCESS); + given(nativeOperations.getNft(NON_FUNGIBLE_TOKEN_ID.tokenNum(), 100L)).willReturn(nft); + given(nativeOperations.getToken(NON_FUNGIBLE_TOKEN_ID.tokenNum())).willReturn(token); + given(token.treasuryAccountId()).willReturn(OWNER_ID); + given(nativeOperations.getAccount(anyLong())).willReturn(account); + final var result = subject.execute().fullResult().result(); + + assertEquals(MessageFrame.State.COMPLETED_SUCCESS, result.getState()); + assertEquals( + asBytesResult(GrantApprovalTranslator.ERC_GRANT_APPROVAL_NFT + .getOutputs() + .encodeElements()), + result.getOutput()); + } + + @Test + void erc721approveFailsWithInvalidSpenderAllowance() { + subject = new ERCGrantApprovalCall( + mockEnhancement(), + systemContractGasCalculator, + verificationStrategy, + OWNER_ID, + NON_FUNGIBLE_TOKEN_ID, + UNAUTHORIZED_SPENDER_ID, + BigInteger.valueOf(100L), + TokenType.NON_FUNGIBLE_UNIQUE); + given(nativeOperations.getNft(NON_FUNGIBLE_TOKEN_ID.tokenNum(), 100L)).willReturn(nft); + given(nativeOperations.getToken(NON_FUNGIBLE_TOKEN_ID.tokenNum())).willReturn(token); + given(token.treasuryAccountId()).willReturn(OWNER_ID); + given(nativeOperations.getAccount(anyLong())).willReturn(null).willReturn(account); + final var result = subject.execute().fullResult().result(); + + assertEquals(State.REVERT, result.getState()); + assertEquals( + Bytes.wrap(ResponseCodeEnum.INVALID_ALLOWANCE_SPENDER_ID + .protoName() + .getBytes()), + result.getOutput()); + } + + @Test + void erc721revoke() { + subject = new ERCGrantApprovalCall( + mockEnhancement(), + systemContractGasCalculator, + verificationStrategy, + OWNER_ID, + NON_FUNGIBLE_TOKEN_ID, + REVOKE_APPROVAL_SPENDER_ID, + BigInteger.valueOf(100L), + TokenType.NON_FUNGIBLE_UNIQUE); + given(systemContractOperations.dispatch( + any(TransactionBody.class), + eq(verificationStrategy), + eq(OWNER_ID), + eq(SingleTransactionRecordBuilder.class))) + .willReturn(recordBuilder); + given(recordBuilder.status()).willReturn(ResponseCodeEnum.SUCCESS); + given(nativeOperations.getNft(NON_FUNGIBLE_TOKEN_ID.tokenNum(), 100L)).willReturn(nft); + given(nativeOperations.getToken(NON_FUNGIBLE_TOKEN_ID.tokenNum())).willReturn(token); + given(nativeOperations.getAccount(anyLong())).willReturn(account); + given(token.treasuryAccountId()).willReturn(OWNER_ID); final var result = subject.execute().fullResult().result(); assertEquals(MessageFrame.State.COMPLETED_SUCCESS, result.getState()); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/utils/SystemContractUtilsTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/utils/SystemContractUtilsTest.java index 4be9383023f6..fd948b65d075 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/utils/SystemContractUtilsTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/utils/SystemContractUtilsTest.java @@ -31,6 +31,8 @@ class SystemContractUtilsTest { private static final long gasUsed = 0L; private static final Bytes result = Bytes.EMPTY; + private static final com.hedera.pbj.runtime.io.buffer.Bytes contractCallResult = + com.hedera.pbj.runtime.io.buffer.Bytes.wrap("Contract Call Result"); private static final ContractID contractID = ContractID.newBuilder().contractNum(111).build(); private static final String errorMessage = ResponseCodeEnum.FAIL_INVALID.name(); @@ -64,4 +66,17 @@ void validateFailedContractResults() { final var actual = SystemContractUtils.contractFunctionResultFailedFor(gasUsed, errorMessage, contractID); assertThat(actual).isEqualTo(expected); } + + @Test + void validateFailedContractResultsForProto() { + final var expected = ContractFunctionResult.newBuilder() + .gasUsed(gasUsed) + .errorMessage(errorMessage) + .contractID(contractID) + .contractCallResult(contractCallResult) + .build(); + final var actual = SystemContractUtils.contractFunctionResultFailedForProto( + gasUsed, errorMessage, contractID, contractCallResult); + assertThat(actual).isEqualTo(expected); + } } diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoDeleteAllowanceHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoDeleteAllowanceHandler.java index 6ab784ecfe85..6c03145bd45d 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoDeleteAllowanceHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoDeleteAllowanceHandler.java @@ -88,15 +88,22 @@ public void preHandle(@NonNull final PreHandleContext context) throws PreCheckEx // Every owner whose allowances are being removed should sign (or the payer, if there is no owner) for (final var allowance : op.nftAllowancesOrElse(emptyList())) { if (allowance.hasOwner()) { - context.requireKeyOrThrow(allowance.ownerOrThrow(), INVALID_ALLOWANCE_OWNER_ID); + final var store = context.createStore(ReadableAccountStore.class); + final var ownerId = allowance.ownerOrThrow(); + final var owner = store.getAccountById(ownerId); + final var approvedForAll = owner.approveForAllNftAllowancesOrElse(emptyList()).stream() + .anyMatch(approveForAll -> approveForAll.tokenId().equals(allowance.tokenId()) + && approveForAll.spenderId().equals(context.payer())); + if (!context.payer().equals(ownerId) && !approvedForAll) { + context.requireKeyOrThrow(ownerId, INVALID_ALLOWANCE_OWNER_ID); + } } } } @Override public void handle(@NonNull final HandleContext context) throws HandleException { - final var txn = context.body(); - final var payer = txn.transactionIDOrThrow().accountIDOrThrow(); + final var payer = context.payer(); final var accountStore = context.writableStore(WritableAccountStore.class); // validate payer account exists diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoDeleteAllowanceHandlerTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoDeleteAllowanceHandlerTest.java index d2001cf4a35f..1206eb70aa26 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoDeleteAllowanceHandlerTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoDeleteAllowanceHandlerTest.java @@ -100,6 +100,7 @@ void happyPathDeletesAllowances() { final var txn = cryptoDeleteAllowanceTransaction(payerId); given(handleContext.body()).willReturn(txn); + given(handleContext.payer()).willReturn(payerId); given(expiryValidator.expirationStatus(any(), anyBoolean(), anyLong())).willReturn(OK); assertThat(ownerAccount.approveForAllNftAllowances()).hasSize(1); @@ -124,6 +125,7 @@ void canDeleteAllowancesOnTreasury() { final var txn = cryptoDeleteAllowanceTransaction(payerId); given(handleContext.body()).willReturn(txn); + given(handleContext.payer()).willReturn(payerId); given(expiryValidator.expirationStatus(any(), anyBoolean(), anyLong())).willReturn(OK); assertThat(ownerAccount.approveForAllNftAllowances()).hasSize(1); @@ -175,6 +177,7 @@ void failsDeleteAllowancesOnInvalidTreasury() { writableNftStore.put(nftSl2.copyBuilder().spenderId(spenderId).build()); final var txn = cryptoDeleteAllowanceTransaction(payerId); + given(handleContext.payer()).willReturn(payerId); given(handleContext.body()).willReturn(txn); given(expiryValidator.expirationStatus(any(), anyBoolean(), anyLong())).willReturn(OK); @@ -206,6 +209,7 @@ void doesntThrowIfAllowanceToBeDeletedDoesNotExist() { final var txn = txnWithAllowance(payerId, nftAllowance); given(handleContext.body()).willReturn(txn); + given(handleContext.payer()).willReturn(payerId); given(expiryValidator.expirationStatus(any(), anyBoolean(), anyLong())).willReturn(OK); assertThat(ownerAccount.approveForAllNftAllowances()).hasSize(1); @@ -235,6 +239,7 @@ void considersPayerIfOwnerNotSpecifiedAndFailIfDoesntOwn() { final var txn = txnWithAllowance(payerId, nftAllowance); given(handleContext.body()).willReturn(txn); + given(handleContext.payer()).willReturn(payerId); assertThat(ownerAccount.approveForAllNftAllowances()).hasSize(1); assertThat(writableNftStore.get(nftIdSl1).ownerId()).isEqualTo(ownerId); @@ -261,6 +266,7 @@ void considersPayerIfOwnerNotSpecified() { final var txn = txnWithAllowance(payerId, nftAllowance); given(handleContext.body()).willReturn(txn); + given(handleContext.payer()).willReturn(payerId); assertThat(ownerAccount.approveForAllNftAllowances()).hasSize(1); assertThat(writableNftStore.get(nftIdSl1).ownerId()).isEqualTo(payerId); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileSuite.java index 970c8220fd6d..0b5ce6a97acf 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileSuite.java @@ -1577,6 +1577,7 @@ final HapiSpec someErc721NegativeTransferFromScenariosPass() { recordWith().status(SPENDER_DOES_NOT_HAVE_ALLOWANCE))); } + @HapiTest final HapiSpec someErc721ApproveAndRemoveScenariosPass() { final AtomicReference tokenMirrorAddr = new AtomicReference<>(); final AtomicReference aCivilianMirrorAddr = new AtomicReference<>();