diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/gas/DispatchType.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/gas/DispatchType.java index 1f859583e304..98526a90afd3 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/gas/DispatchType.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/gas/DispatchType.java @@ -62,7 +62,9 @@ public enum DispatchType { UPDATE_TOKEN_CUSTOM_FEES(HederaFunctionality.TOKEN_FEE_SCHEDULE_UPDATE, DEFAULT), TOKEN_AIRDROP(HederaFunctionality.TOKEN_AIRDROP, DEFAULT), TOKEN_CLAIM_AIRDROP(HederaFunctionality.TOKEN_CLAIM_AIRDROP, DEFAULT), - TOKEN_CANCEL_AIRDROP(HederaFunctionality.TOKEN_CANCEL_AIRDROP, DEFAULT); + TOKEN_CANCEL_AIRDROP(HederaFunctionality.TOKEN_CANCEL_AIRDROP, DEFAULT), + TOKEN_REJECT_FT(HederaFunctionality.TOKEN_REJECT, TOKEN_FUNGIBLE_COMMON), + TOKEN_REJECT_NFT(HederaFunctionality.TOKEN_REJECT, TOKEN_NON_FUNGIBLE_UNIQUE); private final HederaFunctionality functionality; private final SubType subtype; diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/HtsTranslatorsModule.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/HtsTranslatorsModule.java index a5473916fc25..f3a3598c56bb 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/HtsTranslatorsModule.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/HtsTranslatorsModule.java @@ -46,6 +46,7 @@ import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.nfttokeninfo.NftTokenInfoTranslator; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.ownerof.OwnerOfTranslator; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.pauses.PausesTranslator; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.rejecttokens.RejectTokensTranslator; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.setapproval.SetApprovalForAllTranslator; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.symbol.SymbolTranslator; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.tokenexpiry.TokenExpiryTranslator; @@ -468,4 +469,13 @@ static CallTranslator provideTokenCancelAirdropTranslator( @NonNull final TokenCancelAirdropTranslator translator) { return translator; } + + @Provides + @Singleton + @IntoSet + @Named("HtsTranslators") + static CallTranslator provideTokenRejectsTranslator( + @NonNull final RejectTokensTranslator translator) { + return translator; + } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/rejecttokens/RejectTokensDecoder.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/rejecttokens/RejectTokensDecoder.java new file mode 100644 index 000000000000..89893d03f56e --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/rejecttokens/RejectTokensDecoder.java @@ -0,0 +1,123 @@ +/* + * 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.node.app.service.contract.impl.exec.systemcontracts.hts.rejecttokens; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED; +import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.asTokenId; +import static com.hedera.node.app.spi.workflows.HandleException.validateFalse; + +import com.esaulpaugh.headlong.abi.Address; +import com.esaulpaugh.headlong.abi.Tuple; +import com.hedera.hapi.node.base.NftID; +import com.hedera.hapi.node.token.TokenReference; +import com.hedera.hapi.node.token.TokenRejectTransactionBody; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCallAttempt; +import com.hedera.node.config.data.LedgerConfig; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class RejectTokensDecoder { + + // Tuple indexes + // Indexes for TOKEN_REJECT + // rejectTokens(address,address[],(address,int64)[]) + private static final int OWNER_ADDRESS_INDEX = 0; + private static final int FUNGIBLE_ADDRESS_INDEX = 1; + private static final int NFT_IDS_INDEX = 2; + // Indexes for NftID struct (address,int64) + private static final int NFT_ID_ADDRESS_INDEX = 0; + private static final int NFT_ID_SERIAL_INDEX = 1; + + // Indexes for HRC_TOKEN_REJECT_NFT + // rejectTokenNFTs(int64[]) + private static final int HRC_NFT_SERIAL_INDEX = 0; + + @Inject + public RejectTokensDecoder() { + // Dagger2 + } + + public TransactionBody decodeTokenRejects(@NonNull final HtsCallAttempt attempt) { + final var call = RejectTokensTranslator.TOKEN_REJECT.decodeCall(attempt.inputBytes()); + final var maxRejections = + attempt.configuration().getConfigData(LedgerConfig.class).tokenRejectsMaxLen(); + final Address[] ftAddresses = call.get(FUNGIBLE_ADDRESS_INDEX); + final Tuple[] nftIds = call.get(NFT_IDS_INDEX); + validateFalse(ftAddresses.length + nftIds.length > maxRejections, TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED); + final var owner = (Address) call.get(OWNER_ADDRESS_INDEX); + final var ownerId = attempt.addressIdConverter().convert(owner); + var referenceList = new ArrayList(); + for (Address ftAddress : ftAddresses) { + final var tokenReference = TokenReference.newBuilder() + .fungibleToken(asTokenId(ftAddress)) + .build(); + referenceList.add(tokenReference); + } + for (Tuple nftId : nftIds) { + final var nftIdAddress = (Address) nftId.get(NFT_ID_ADDRESS_INDEX); + final var nftIdSerial = (long) nftId.get(NFT_ID_SERIAL_INDEX); + final var nftReference = TokenReference.newBuilder() + .nft(NftID.newBuilder() + .tokenId(asTokenId(nftIdAddress)) + .serialNumber(nftIdSerial) + .build()) + .build(); + referenceList.add(nftReference); + } + + return TransactionBody.newBuilder() + .tokenReject( + TokenRejectTransactionBody.newBuilder().owner(ownerId).rejections(referenceList)) + .build(); + } + + public TransactionBody decodeHrcTokenRejectFT(@NonNull final HtsCallAttempt attempt) { + final var token = attempt.redirectTokenId(); + final var sender = attempt.senderId(); + final var tokenReference = + TokenReference.newBuilder().fungibleToken(token).build(); + return TransactionBody.newBuilder() + .tokenReject( + TokenRejectTransactionBody.newBuilder().owner(sender).rejections(tokenReference)) + .build(); + } + + public TransactionBody decodeHrcTokenRejectNFT(@NonNull final HtsCallAttempt attempt) { + final var maxRejections = + attempt.configuration().getConfigData(LedgerConfig.class).tokenRejectsMaxLen(); + final var call = RejectTokensTranslator.HRC_TOKEN_REJECT_NFT.decodeCall(attempt.inputBytes()); + final var serials = (long[]) call.get(HRC_NFT_SERIAL_INDEX); + validateFalse(serials.length > maxRejections, TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED); + final var token = attempt.redirectTokenId(); + final var sender = attempt.senderId(); + var referenceList = new ArrayList(); + for (long serial : serials) { + final var tokenReference = TokenReference.newBuilder() + .nft(NftID.newBuilder().tokenId(token).serialNumber(serial).build()) + .build(); + referenceList.add(tokenReference); + } + return TransactionBody.newBuilder() + .tokenReject( + TokenRejectTransactionBody.newBuilder().owner(sender).rejections(referenceList)) + .build(); + } +} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/rejecttokens/RejectTokensTranslator.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/rejecttokens/RejectTokensTranslator.java new file mode 100644 index 000000000000..e90d627e9029 --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/rejecttokens/RejectTokensTranslator.java @@ -0,0 +1,121 @@ +/* + * 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.node.app.service.contract.impl.exec.systemcontracts.hts.rejecttokens; + +import com.esaulpaugh.headlong.abi.Function; +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.contract.impl.exec.gas.DispatchGasCalculator; +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.systemcontracts.common.AbstractCallTranslator; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.common.Call; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.DispatchForResponseCodeHtsCall; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCallAttempt; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.ReturnTypes; +import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater; +import com.hedera.node.config.data.ContractsConfig; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import javax.inject.Inject; + +public class RejectTokensTranslator extends AbstractCallTranslator { + public static final Function TOKEN_REJECT = + new Function("rejectTokens(address,address[],(address,int64)[])", ReturnTypes.INT_64); + public static final Function HRC_TOKEN_REJECT_FT = new Function("rejectTokenFT()", ReturnTypes.INT_64); + public static final Function HRC_TOKEN_REJECT_NFT = new Function("rejectTokenNFTs(int64[])", ReturnTypes.INT_64); + + private final RejectTokensDecoder decoder; + private final Map gasCalculators = new HashMap<>(); + + @Inject + public RejectTokensTranslator(@NonNull final RejectTokensDecoder decoder) { + this.decoder = decoder; + gasCalculators.put(TOKEN_REJECT, RejectTokensTranslator::gasRequirement); + gasCalculators.put(HRC_TOKEN_REJECT_FT, RejectTokensTranslator::gasRequirementHRCFungible); + gasCalculators.put(HRC_TOKEN_REJECT_NFT, RejectTokensTranslator::gasRequirementHRCNft); + } + + @Override + public boolean matches(@NonNull final HtsCallAttempt attempt) { + final var rejectEnabled = + attempt.configuration().getConfigData(ContractsConfig.class).systemContractRejectTokensEnabled(); + return attempt.isTokenRedirect() + ? attempt.isSelectorIfConfigEnabled(rejectEnabled, HRC_TOKEN_REJECT_FT, HRC_TOKEN_REJECT_NFT) + : attempt.isSelectorIfConfigEnabled(TOKEN_REJECT, rejectEnabled); + } + + @Override + public Call callFrom(@NonNull final HtsCallAttempt attempt) { + final var gasRequirement = gasCalculators.entrySet().stream() + .filter(entry -> attempt.isSelector(entry.getKey())) + .map(Entry::getValue) + .findFirst(); + return new DispatchForResponseCodeHtsCall(attempt, bodyFor(attempt), gasRequirement.get()); + } + + public static long gasRequirementHRCFungible( + @NonNull final TransactionBody body, + @NonNull final SystemContractGasCalculator systemContractGasCalculator, + @NonNull final HederaWorldUpdater.Enhancement enhancement, + @NonNull final AccountID payerId) { + return systemContractGasCalculator.gasRequirement(body, DispatchType.TOKEN_REJECT_FT, payerId); + } + + public static long gasRequirementHRCNft( + @NonNull final TransactionBody body, + @NonNull final SystemContractGasCalculator systemContractGasCalculator, + @NonNull final HederaWorldUpdater.Enhancement enhancement, + @NonNull final AccountID payerId) { + return systemContractGasCalculator.gasRequirement(body, DispatchType.TOKEN_REJECT_NFT, payerId); + } + + public static long gasRequirement( + @NonNull final TransactionBody body, + @NonNull final SystemContractGasCalculator systemContractGasCalculator, + @NonNull final HederaWorldUpdater.Enhancement enhancement, + @NonNull final AccountID payerId) { + final var accumulatedCanonicalPricing = body.tokenReject().rejections().stream() + .map(rejection -> { + if (rejection.hasFungibleToken()) { + return systemContractGasCalculator.canonicalPriceInTinycents(DispatchType.TOKEN_REJECT_FT); + } else { + return systemContractGasCalculator.canonicalPriceInTinycents(DispatchType.TOKEN_REJECT_NFT); + } + }) + .reduce(0L, Long::sum); + return systemContractGasCalculator.gasRequirementWithTinycents(body, payerId, accumulatedCanonicalPricing); + } + + private TransactionBody bodyFor(@NonNull HtsCallAttempt attempt) { + return attempt.isSelector(TOKEN_REJECT) ? bodyForClassic(attempt) : bodyForHRC(attempt); + } + + private TransactionBody bodyForClassic(@NonNull final HtsCallAttempt attempt) { + return decoder.decodeTokenRejects(attempt); + } + + private TransactionBody bodyForHRC(@NonNull final HtsCallAttempt attempt) { + if (attempt.isSelector(HRC_TOKEN_REJECT_FT)) { + return decoder.decodeHrcTokenRejectFT(attempt); + } else { + return decoder.decodeHrcTokenRejectNFT(attempt); + } + } +} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/rejecttokens/RejectTokensDecoderTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/rejecttokens/RejectTokensDecoderTest.java new file mode 100644 index 000000000000..54dd9465083f --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/rejecttokens/RejectTokensDecoderTest.java @@ -0,0 +1,249 @@ +/* + * 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.node.app.service.contract.impl.test.exec.systemcontracts.hts.rejecttokens; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.FUNGIBLE_TOKEN_HEADLONG_ADDRESS; +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_HEADLONG_ADDRESS; +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_HEADLONG_ADDRESS; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.SENDER_ID; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.asHeadlongAddress; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.lenient; + +import com.esaulpaugh.headlong.abi.Address; +import com.esaulpaugh.headlong.abi.Tuple; +import com.hedera.hapi.node.base.NftID; +import com.hedera.hapi.node.token.TokenReference; +import com.hedera.hapi.node.token.TokenRejectTransactionBody; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.AddressIdConverter; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCallAttempt; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.rejecttokens.RejectTokensDecoder; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.rejecttokens.RejectTokensTranslator; +import com.hedera.node.app.spi.workflows.HandleException; +import com.hedera.node.config.data.LedgerConfig; +import com.swirlds.config.api.Configuration; +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class RejectTokensDecoderTest { + + @Mock + private HtsCallAttempt attempt; + + @Mock + private AddressIdConverter addressIdConverter; + + @Mock + private Configuration configuration; + + @Mock + private LedgerConfig ledgerConfig; + + private RejectTokensDecoder subject; + + @BeforeEach + void setup() { + subject = new RejectTokensDecoder(); + + lenient().when(attempt.addressIdConverter()).thenReturn(addressIdConverter); + } + + @Test + void decodeHtsCall() { + // given: + given(attempt.configuration()).willReturn(configuration); + given(configuration.getConfigData(LedgerConfig.class)).willReturn(ledgerConfig); + given(ledgerConfig.tokenRejectsMaxLen()).willReturn(10); + given(addressIdConverter.convert(asHeadlongAddress(SENDER_ID.accountNum()))) + .willReturn(SENDER_ID); + final var encoded = Bytes.wrapByteBuffer(RejectTokensTranslator.TOKEN_REJECT.encodeCall(Tuple.of( + asHeadlongAddress(SENDER_ID.accountNum()), + new Address[] {FUNGIBLE_TOKEN_HEADLONG_ADDRESS}, + new Tuple[] {}))); + + // when + given(attempt.inputBytes()).willReturn(encoded.toArrayUnsafe()); + + final var expected = TokenRejectTransactionBody.newBuilder() + .owner(SENDER_ID) + .rejections(TokenReference.newBuilder() + .fungibleToken(FUNGIBLE_TOKEN_ID) + .build()) + .owner(SENDER_ID) + .build(); + + // then + final var decoded = subject.decodeTokenRejects(attempt); + + assertNotNull(decoded); + assertEquals(expected, decoded.tokenReject()); + } + + @Test + void decodeHtsCallNFT() { + // given: + given(attempt.configuration()).willReturn(configuration); + given(configuration.getConfigData(LedgerConfig.class)).willReturn(ledgerConfig); + given(ledgerConfig.tokenRejectsMaxLen()).willReturn(10); + given(addressIdConverter.convert(asHeadlongAddress(SENDER_ID.accountNum()))) + .willReturn(SENDER_ID); + + final var encoded = Bytes.wrapByteBuffer(RejectTokensTranslator.TOKEN_REJECT.encodeCall( + Tuple.of(asHeadlongAddress(SENDER_ID.accountNum()), new Address[] {}, new Tuple[] { + Tuple.of(NON_FUNGIBLE_TOKEN_HEADLONG_ADDRESS, 1L) + }))); + + // when + given(attempt.inputBytes()).willReturn(encoded.toArrayUnsafe()); + + final var expected = TokenRejectTransactionBody.newBuilder() + .owner(SENDER_ID) + .rejections(TokenReference.newBuilder() + .nft(NftID.newBuilder() + .tokenId(NON_FUNGIBLE_TOKEN_ID) + .serialNumber(1L) + .build()) + .build()) + .owner(SENDER_ID) + .build(); + + // then + final var decoded = subject.decodeTokenRejects(attempt); + + assertNotNull(decoded); + assertEquals(expected, decoded.tokenReject()); + } + + @Test + void decodeFailsIfReferencesExceedLimits() { + // given: + given(attempt.configuration()).willReturn(configuration); + given(configuration.getConfigData(LedgerConfig.class)).willReturn(ledgerConfig); + given(ledgerConfig.tokenRejectsMaxLen()).willReturn(10); + + final var encoded = Bytes.wrapByteBuffer(RejectTokensTranslator.TOKEN_REJECT.encodeCall(Tuple.of( + OWNER_HEADLONG_ADDRESS, + new Address[] { + FUNGIBLE_TOKEN_HEADLONG_ADDRESS, + FUNGIBLE_TOKEN_HEADLONG_ADDRESS, + FUNGIBLE_TOKEN_HEADLONG_ADDRESS, + FUNGIBLE_TOKEN_HEADLONG_ADDRESS, + FUNGIBLE_TOKEN_HEADLONG_ADDRESS, + FUNGIBLE_TOKEN_HEADLONG_ADDRESS + }, + new Tuple[] { + Tuple.of(NON_FUNGIBLE_TOKEN_HEADLONG_ADDRESS, 1L), + Tuple.of(NON_FUNGIBLE_TOKEN_HEADLONG_ADDRESS, 1L), + Tuple.of(NON_FUNGIBLE_TOKEN_HEADLONG_ADDRESS, 1L), + Tuple.of(NON_FUNGIBLE_TOKEN_HEADLONG_ADDRESS, 1L), + Tuple.of(NON_FUNGIBLE_TOKEN_HEADLONG_ADDRESS, 1L), + Tuple.of(NON_FUNGIBLE_TOKEN_HEADLONG_ADDRESS, 1L), + }))); + + // when + given(attempt.inputBytes()).willReturn(encoded.toArrayUnsafe()); + + // then + assertThatExceptionOfType(HandleException.class) + .isThrownBy(() -> subject.decodeTokenRejects(attempt)) + .withMessage(TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED.toString()); + } + + @Test + void decodeHRCFungible() { + // given: + given(attempt.senderId()).willReturn(SENDER_ID); + + // when + given(attempt.redirectTokenId()).willReturn(FUNGIBLE_TOKEN_ID); + + final var expected = TokenRejectTransactionBody.newBuilder() + .rejections(TokenReference.newBuilder() + .fungibleToken(FUNGIBLE_TOKEN_ID) + .build()) + .owner(SENDER_ID) + .build(); + + // then + final var decoded = subject.decodeHrcTokenRejectFT(attempt); + + assertNotNull(decoded); + assertEquals(expected, decoded.tokenReject()); + } + + @Test + void decodeHRCNft() { + // given: + given(attempt.configuration()).willReturn(configuration); + given(configuration.getConfigData(LedgerConfig.class)).willReturn(ledgerConfig); + given(ledgerConfig.tokenRejectsMaxLen()).willReturn(10); + + final var encoded = + Bytes.wrapByteBuffer(RejectTokensTranslator.HRC_TOKEN_REJECT_NFT.encodeCall(Tuple.of(new long[] {1L}))); + + // when + given(attempt.inputBytes()).willReturn(encoded.toArrayUnsafe()); + given(attempt.senderId()).willReturn(SENDER_ID); + given(attempt.redirectTokenId()).willReturn(NON_FUNGIBLE_TOKEN_ID); + + final var expected = TokenRejectTransactionBody.newBuilder() + .rejections(TokenReference.newBuilder() + .nft(NftID.newBuilder() + .tokenId(NON_FUNGIBLE_TOKEN_ID) + .serialNumber(1L) + .build()) + .build()) + .owner(SENDER_ID) + .build(); + + // then + final var decoded = subject.decodeHrcTokenRejectNFT(attempt); + + assertNotNull(decoded); + assertEquals(expected, decoded.tokenReject()); + } + + @Test + void decodeFailsWhenHRCNftExceedsLimits() { + // given: + given(attempt.configuration()).willReturn(configuration); + given(configuration.getConfigData(LedgerConfig.class)).willReturn(ledgerConfig); + given(ledgerConfig.tokenRejectsMaxLen()).willReturn(2); + + final var encoded = Bytes.wrapByteBuffer( + RejectTokensTranslator.HRC_TOKEN_REJECT_NFT.encodeCall(Tuple.of(new long[] {1L, 2L, 3L}))); + + // when + given(attempt.inputBytes()).willReturn(encoded.toArrayUnsafe()); + + // then + assertThatExceptionOfType(HandleException.class) + .isThrownBy(() -> subject.decodeHrcTokenRejectNFT(attempt)) + .withMessage(TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED.toString()); + } +} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/rejecttokens/RejectTokensTranslatorTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/rejecttokens/RejectTokensTranslatorTest.java new file mode 100644 index 000000000000..62036e369899 --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/rejecttokens/RejectTokensTranslatorTest.java @@ -0,0 +1,360 @@ +/* + * 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.node.app.service.contract.impl.test.exec.systemcontracts.hts.rejecttokens; + +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.SENDER_ID; +import static com.hedera.node.app.service.contract.impl.test.exec.systemcontracts.CallAttemptHelpers.prepareHtsAttemptWithSelectorAndCustomConfig; +import static com.hedera.node.app.service.contract.impl.test.exec.systemcontracts.CallAttemptHelpers.prepareHtsAttemptWithSelectorForRedirectWithConfig; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.token.TokenReference; +import com.hedera.hapi.node.token.TokenRejectTransactionBody; +import com.hedera.hapi.node.transaction.TransactionBody; +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.HederaNativeOperations; +import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategies; +import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategy; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.AddressIdConverter; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.DispatchForResponseCodeHtsCall; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCallAttempt; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.burn.BurnTranslator; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.rejecttokens.RejectTokensDecoder; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.rejecttokens.RejectTokensTranslator; +import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater.Enhancement; +import com.hedera.node.config.data.ContractsConfig; +import com.swirlds.config.api.Configuration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class RejectTokensTranslatorTest { + + @Mock + private RejectTokensDecoder decoder; + + @Mock + private HtsCallAttempt attempt; + + @Mock + private ContractsConfig contractsConfig; + + @Mock + private Configuration configuration; + + @Mock + private Enhancement enhancement; + + @Mock + private AddressIdConverter addressIdConverter; + + @Mock + private VerificationStrategies verificationStrategies; + + @Mock + private VerificationStrategy verificationStrategy; + + @Mock + private SystemContractGasCalculator gasCalculator; + + @Mock + private HederaNativeOperations nativeOperations; + + @Mock + private TransactionBody transactionBody; + + @Mock + private AccountID payerId; + + private RejectTokensTranslator subject; + + @BeforeEach + void setUp() { + subject = new RejectTokensTranslator(decoder); + } + + @Test + void matchesHTSWithInvalidSig() { + // given: + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.systemContractRejectTokensEnabled()).willReturn(true); + attempt = prepareHtsAttemptWithSelectorAndCustomConfig( + BurnTranslator.BURN_TOKEN_V1, + subject, + enhancement, + addressIdConverter, + verificationStrategies, + gasCalculator, + configuration); + + // when: + boolean matches = subject.matches(attempt); + + // then: + assertFalse(matches); + } + + @Test + void matchesHTSWithConfigEnabled() { + // given: + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.systemContractRejectTokensEnabled()).willReturn(true); + attempt = prepareHtsAttemptWithSelectorAndCustomConfig( + RejectTokensTranslator.TOKEN_REJECT, + subject, + enhancement, + addressIdConverter, + verificationStrategies, + gasCalculator, + configuration); + + // when: + boolean matches = subject.matches(attempt); + + // then: + assertTrue(matches); + } + + @Test + void matchesHTSWithConfigDisabled() { + // given: + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.systemContractRejectTokensEnabled()).willReturn(false); + attempt = prepareHtsAttemptWithSelectorAndCustomConfig( + RejectTokensTranslator.TOKEN_REJECT, + subject, + enhancement, + addressIdConverter, + verificationStrategies, + gasCalculator, + configuration); + + // when: + boolean matches = subject.matches(attempt); + + // then: + assertFalse(matches); + } + + @Test + void matchesFungibleHRCWithConfigEnabled() { + // given: + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.systemContractRejectTokensEnabled()).willReturn(true); + given(enhancement.nativeOperations()).willReturn(nativeOperations); + attempt = prepareHtsAttemptWithSelectorForRedirectWithConfig( + RejectTokensTranslator.HRC_TOKEN_REJECT_FT, + subject, + enhancement, + addressIdConverter, + verificationStrategies, + gasCalculator, + configuration); + + // when: + boolean matches = subject.matches(attempt); + + // then: + assertTrue(matches); + } + + @Test + void matchesFungibleHRCWithConfigDisabled() { + // given: + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.systemContractRejectTokensEnabled()).willReturn(false); + given(enhancement.nativeOperations()).willReturn(nativeOperations); + attempt = prepareHtsAttemptWithSelectorForRedirectWithConfig( + RejectTokensTranslator.HRC_TOKEN_REJECT_FT, + subject, + enhancement, + addressIdConverter, + verificationStrategies, + gasCalculator, + configuration); + + // when: + boolean matches = subject.matches(attempt); + + // then: + assertFalse(matches); + } + + @Test + void matchesNftHRCWithConfigEnabled() { + // given: + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.systemContractRejectTokensEnabled()).willReturn(true); + given(enhancement.nativeOperations()).willReturn(nativeOperations); + attempt = prepareHtsAttemptWithSelectorForRedirectWithConfig( + RejectTokensTranslator.HRC_TOKEN_REJECT_NFT, + subject, + enhancement, + addressIdConverter, + verificationStrategies, + gasCalculator, + configuration); + + // when: + boolean matches = subject.matches(attempt); + + // then: + assertTrue(matches); + } + + @Test + void matchesNftHRCWithConfigDisabled() { + // given: + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.systemContractRejectTokensEnabled()).willReturn(false); + given(enhancement.nativeOperations()).willReturn(nativeOperations); + attempt = prepareHtsAttemptWithSelectorForRedirectWithConfig( + RejectTokensTranslator.HRC_TOKEN_REJECT_NFT, + subject, + enhancement, + addressIdConverter, + verificationStrategies, + gasCalculator, + configuration); + + // when: + boolean matches = subject.matches(attempt); + + // then: + assertFalse(matches); + } + + @Test + void gasRequirementCalculatesCorrectly() { + long expectedGas = 1000L; + final var body = TokenRejectTransactionBody.newBuilder() + .rejections(TokenReference.newBuilder() + .fungibleToken(FUNGIBLE_TOKEN_ID) + .build()) + .owner(SENDER_ID) + .build(); + given(gasCalculator.canonicalPriceInTinycents(DispatchType.TOKEN_REJECT_FT)) + .willReturn(expectedGas); + given(transactionBody.tokenReject()).willReturn(body); + given(gasCalculator.gasRequirementWithTinycents(transactionBody, payerId, expectedGas)) + .willReturn(expectedGas); + long result = RejectTokensTranslator.gasRequirement(transactionBody, gasCalculator, enhancement, payerId); + + assertEquals(expectedGas, result); + } + + @Test + void gasRequirementHRCFungible() { + long expectedGas = 1000L; + given(gasCalculator.gasRequirement(transactionBody, DispatchType.TOKEN_REJECT_FT, payerId)) + .willReturn(expectedGas); + long result = + RejectTokensTranslator.gasRequirementHRCFungible(transactionBody, gasCalculator, enhancement, payerId); + + assertEquals(expectedGas, result); + } + + @Test + void gasRequirementHRCNft() { + long expectedGas = 1000L; + given(gasCalculator.gasRequirement(transactionBody, DispatchType.TOKEN_REJECT_NFT, payerId)) + .willReturn(expectedGas); + long result = RejectTokensTranslator.gasRequirementHRCNft(transactionBody, gasCalculator, enhancement, payerId); + + assertEquals(expectedGas, result); + } + + @Test + void callFromHtsTokenReject() { + // given: + given(addressIdConverter.convertSender(any())).willReturn(SENDER_ID); + given(verificationStrategies.activatingOnlyContractKeysFor(any(), anyBoolean(), any())) + .willReturn(verificationStrategy); + attempt = prepareHtsAttemptWithSelectorAndCustomConfig( + RejectTokensTranslator.TOKEN_REJECT, + subject, + enhancement, + addressIdConverter, + verificationStrategies, + gasCalculator, + configuration); + + // when: + var call = subject.callFrom(attempt); + + // then: + assertEquals(DispatchForResponseCodeHtsCall.class, call.getClass()); + verify(decoder).decodeTokenRejects(attempt); + } + + @Test + void callFromHRCCancelFTAirdrop() { + // given: + given(addressIdConverter.convertSender(any())).willReturn(SENDER_ID); + given(verificationStrategies.activatingOnlyContractKeysFor(any(), anyBoolean(), any())) + .willReturn(verificationStrategy); + attempt = prepareHtsAttemptWithSelectorAndCustomConfig( + RejectTokensTranslator.HRC_TOKEN_REJECT_FT, + subject, + enhancement, + addressIdConverter, + verificationStrategies, + gasCalculator, + configuration); + + // when: + var call = subject.callFrom(attempt); + + // then: + assertEquals(DispatchForResponseCodeHtsCall.class, call.getClass()); + verify(decoder).decodeHrcTokenRejectFT(attempt); + } + + @Test + void callFromHRCCancelNFTAirdrop() { + // given: + given(addressIdConverter.convertSender(any())).willReturn(SENDER_ID); + given(verificationStrategies.activatingOnlyContractKeysFor(any(), anyBoolean(), any())) + .willReturn(verificationStrategy); + attempt = prepareHtsAttemptWithSelectorAndCustomConfig( + RejectTokensTranslator.HRC_TOKEN_REJECT_NFT, + subject, + enhancement, + addressIdConverter, + verificationStrategies, + gasCalculator, + configuration); + + // when: + var call = subject.callFrom(attempt); + + // then: + assertEquals(DispatchForResponseCodeHtsCall.class, call.getClass()); + verify(decoder).decodeHrcTokenRejectNFT(attempt); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/dsl/contracts/TokenRedirectContract.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/dsl/contracts/TokenRedirectContract.java index dc6f0e98e0e3..fd047c5e1b23 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/dsl/contracts/TokenRedirectContract.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/dsl/contracts/TokenRedirectContract.java @@ -25,6 +25,7 @@ public enum TokenRedirectContract { // TODO: Update this to HRC904 once all tests are merged HRC904CLAIM("HRC904TokenClaim"), HRC904CANCEL("HRC904TokenCancel"), + HRC904REJECT("HRC904Reject"), ERC20("ERC20ABI"), ERC721("ERC721ABI"); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/HRCTokenRejectTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/HRCTokenRejectTest.java new file mode 100644 index 000000000000..227402cb2f18 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/HRCTokenRejectTest.java @@ -0,0 +1,134 @@ +/* + * 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.services.bdd.suites.contract.precompile; + +import static com.hedera.services.bdd.junit.TestTags.SMART_CONTRACT; +import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.dsl.contracts.TokenRedirectContract.HRC904REJECT; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_TOKEN_BALANCE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_OWNER_ID; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; + +import com.hedera.services.bdd.junit.HapiTest; +import com.hedera.services.bdd.junit.HapiTestLifecycle; +import com.hedera.services.bdd.junit.support.TestLifecycle; +import com.hedera.services.bdd.spec.dsl.annotations.Account; +import com.hedera.services.bdd.spec.dsl.annotations.FungibleToken; +import com.hedera.services.bdd.spec.dsl.annotations.NonFungibleToken; +import com.hedera.services.bdd.spec.dsl.entities.SpecAccount; +import com.hedera.services.bdd.spec.dsl.entities.SpecFungibleToken; +import com.hedera.services.bdd.spec.dsl.entities.SpecNonFungibleToken; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Tag; + +@Tag(SMART_CONTRACT) +@HapiTestLifecycle +public class HRCTokenRejectTest { + + @Account(tinybarBalance = 100_000_000_000L) + static SpecAccount sender; + + @BeforeAll + public static void setUp(@NonNull TestLifecycle lifecycle) { + lifecycle.doAdhoc(sender.getInfo()); + } + + @HapiTest + @DisplayName("HRC rejectTokenFT works") + public Stream hrcFungibleWorks(@FungibleToken(initialSupply = 1000) SpecFungibleToken token) { + return hapiTest( + sender.associateTokens(token), + token.treasury().transferUnitsTo(sender, 10L, token), + token.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 990L)), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 10L)), + token.call(HRC904REJECT, "rejectTokenFT").with(call -> call.payingWith(sender.name())), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 0L)), + token.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 1000L))); + } + + @HapiTest + @DisplayName("HRC rejectTokenNFTs works") + public Stream hrcNftWorks(@NonFungibleToken(numPreMints = 1) SpecNonFungibleToken nft) { + return hapiTest( + sender.associateTokens(nft), + nft.treasury().transferNFTsTo(sender, nft, 1L), + nft.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 0L)), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 1L)), + nft.call(HRC904REJECT, "rejectTokenNFTs", new long[] {1L}).with(call -> call.payingWith(sender.name())), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 0L)), + nft.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 1L))); + } + + @HapiTest + @DisplayName("HRC rejectTokenNFTs works for max allowed serials") + public Stream hrcNftWorksForMultipleSerials( + @NonFungibleToken(numPreMints = 10) SpecNonFungibleToken nft) { + return hapiTest( + sender.associateTokens(nft), + nft.treasury().transferNFTsTo(sender, nft, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L), + nft.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 0L)), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 10L)), + nft.call(HRC904REJECT, "rejectTokenNFTs", new long[] {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L}) + .with(call -> call.payingWith(sender.name())) + .gas(1_000_000L), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 0L)), + nft.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 10L))); + } + + @HapiTest + @DisplayName("HRC rejectTokenNFTs fails if account has no nft balance") + public Stream hrcNftFailsIfAccountHasNoBalance( + @NonFungibleToken(numPreMints = 1) SpecNonFungibleToken nft) { + return hapiTest( + sender.associateTokens(nft), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 0L)), + nft.call(HRC904REJECT, "rejectTokenNFTs", new long[] {1L}) + .with(call -> call.payingWith(sender.name())) + .andAssert(txn -> txn.hasKnownStatuses(SUCCESS, INVALID_OWNER_ID))); + } + + @HapiTest + @DisplayName("HRC rejectTokenFT fails if account has no token balance") + public Stream hrcFungibleFailsIfAccountHasNoBalance(@FungibleToken SpecFungibleToken token) { + return hapiTest( + sender.associateTokens(token), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 0L)), + token.call(HRC904REJECT, "rejectTokenFT") + .with(call -> call.payingWith(sender.name())) + .andAssert(txn -> txn.hasKnownStatuses(SUCCESS, INSUFFICIENT_TOKEN_BALANCE))); + } + + @HapiTest + @DisplayName("HRC rejectTokenNFTs fails if serials exceed limit") + public Stream hrcNftFailsForMultipleSerials( + @NonFungibleToken(numPreMints = 11) SpecNonFungibleToken nft) { + return hapiTest( + sender.associateTokens(nft), + nft.treasury().transferNFTsTo(sender, nft, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L), + nft.treasury().transferNFTsTo(sender, nft, 11L), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 11L)), + nft.call(HRC904REJECT, "rejectTokenNFTs", new long[] {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L}) + .with(call -> call.payingWith(sender.name())) + .gas(1_000_000L) + .andAssert(txn -> txn.hasKnownStatuses(CONTRACT_REVERT_EXECUTED))); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenRejectSystemContractTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenRejectSystemContractTest.java new file mode 100644 index 000000000000..f8d040c6d8c1 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenRejectSystemContractTest.java @@ -0,0 +1,258 @@ +/* + * 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.services.bdd.suites.contract.precompile; + +import static com.hedera.services.bdd.junit.TestTags.SMART_CONTRACT; +import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_TOKEN_BALANCE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_NFT_ID; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_TOKEN_ID; + +import com.esaulpaugh.headlong.abi.Address; +import com.hedera.services.bdd.junit.HapiTest; +import com.hedera.services.bdd.junit.HapiTestLifecycle; +import com.hedera.services.bdd.junit.support.TestLifecycle; +import com.hedera.services.bdd.spec.dsl.annotations.Account; +import com.hedera.services.bdd.spec.dsl.annotations.Contract; +import com.hedera.services.bdd.spec.dsl.annotations.FungibleToken; +import com.hedera.services.bdd.spec.dsl.annotations.NonFungibleToken; +import com.hedera.services.bdd.spec.dsl.entities.SpecAccount; +import com.hedera.services.bdd.spec.dsl.entities.SpecContract; +import com.hedera.services.bdd.spec.dsl.entities.SpecFungibleToken; +import com.hedera.services.bdd.spec.dsl.entities.SpecNonFungibleToken; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Tag; + +@Tag(SMART_CONTRACT) +@HapiTestLifecycle +public class TokenRejectSystemContractTest { + + @Contract(contract = "TokenReject", creationGas = 1_000_000L) + static SpecContract tokenReject; + + @Account(tinybarBalance = 1_000_000_000L) + static SpecAccount sender; + + @BeforeAll + public static void setup(final @NonNull TestLifecycle lifecycle) { + lifecycle.doAdhoc(sender.authorizeContract(tokenReject)); + } + + @HapiTest + @DisplayName("Reject fungible token") + public Stream tokenRejectSystemContractTest( + @FungibleToken(initialSupply = 1000) SpecFungibleToken token) { + return hapiTest(withOpContext((spec, opLog) -> { + allRunFor( + spec, + sender.associateTokens(token), + token.treasury().transferUnitsTo(sender, 100, token), + token.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 900L))); + final var tokenAddress = token.addressOn(spec.targetNetworkOrThrow()); + allRunFor( + spec, + tokenReject.call("rejectTokens", sender, new Address[] {tokenAddress}, new Address[0]), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 0L)), + token.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 1000L))); + })); + } + + @HapiTest + @DisplayName("Reject non-fungible token") + public Stream tokenRejectSystemContractNftTest( + @NonFungibleToken(numPreMints = 1) SpecNonFungibleToken nft) { + return hapiTest(withOpContext((spec, opLog) -> { + allRunFor( + spec, + sender.associateTokens(nft), + nft.treasury().transferNFTsTo(sender, nft, 1L), + nft.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 0L))); + final var tokenAddress = nft.addressOn(spec.targetNetworkOrThrow()); + allRunFor( + spec, + tokenReject.call("rejectTokens", sender, new Address[] {}, new Address[] {tokenAddress}), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 0L)), + nft.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 1L))); + })); + } + + @HapiTest + @DisplayName("Reject multiple tokens") + public Stream tokenRejectForMultipleTokens( + @FungibleToken SpecFungibleToken token1, + @FungibleToken SpecFungibleToken token2, + @FungibleToken SpecFungibleToken token3, + @NonFungibleToken(numPreMints = 1) SpecNonFungibleToken nft1, + @NonFungibleToken(numPreMints = 1) SpecNonFungibleToken nft2, + @NonFungibleToken(numPreMints = 1) SpecNonFungibleToken nft3) { + return hapiTest(withOpContext((spec, opLog) -> { + allRunFor( + spec, + sender.associateTokens(token1, token2, token3, nft1, nft2, nft3), + token1.treasury().transferUnitsTo(sender, 100, token1), + token2.treasury().transferUnitsTo(sender, 100, token2), + token3.treasury().transferUnitsTo(sender, 100, token3), + nft1.treasury().transferNFTsTo(sender, nft1, 1L), + nft2.treasury().transferNFTsTo(sender, nft2, 1L), + nft3.treasury().transferNFTsTo(sender, nft3, 1L)); + final var token1Address = token1.addressOn(spec.targetNetworkOrThrow()); + final var token2Address = token2.addressOn(spec.targetNetworkOrThrow()); + final var token3Address = token3.addressOn(spec.targetNetworkOrThrow()); + final var nft1Address = nft1.addressOn(spec.targetNetworkOrThrow()); + final var nft2Address = nft2.addressOn(spec.targetNetworkOrThrow()); + final var nft3Address = nft3.addressOn(spec.targetNetworkOrThrow()); + allRunFor( + spec, + tokenReject + .call( + "rejectTokens", + sender, + new Address[] {token1Address, token2Address, token3Address}, + new Address[] {nft1Address, nft2Address, nft3Address}) + .gas(1_000_000L), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(token1.name(), 0L)), + token1.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(token1.name(), 100L)), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(token2.name(), 0L)), + token2.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(token2.name(), 100L)), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(token3.name(), 0L)), + token3.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(token3.name(), 100L)), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(nft1.name(), 0L)), + nft1.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(nft1.name(), 1L)), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(nft2.name(), 0L)), + nft2.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(nft2.name(), 1L)), + sender.getBalance().andAssert(balance -> balance.hasTokenBalance(nft3.name(), 0L)), + nft3.treasury().getBalance().andAssert(balance -> balance.hasTokenBalance(nft3.name(), 1L))); + })); + } + + @HapiTest + @DisplayName("Fails to reject tokens if limits exceeded") + public Stream failsIfLimitsExceeded( + @FungibleToken SpecFungibleToken token1, + @FungibleToken SpecFungibleToken token2, + @FungibleToken SpecFungibleToken token3, + @FungibleToken SpecFungibleToken token4, + @FungibleToken SpecFungibleToken token5, + @FungibleToken SpecFungibleToken token6, + @FungibleToken SpecFungibleToken token7, + @FungibleToken SpecFungibleToken token8, + @FungibleToken SpecFungibleToken token9, + @FungibleToken SpecFungibleToken token10, + @FungibleToken SpecFungibleToken token11) { + return hapiTest(withOpContext((spec, opLog) -> { + allRunFor( + spec, + sender.associateTokens( + token1, token2, token3, token4, token5, token6, token7, token8, token9, token10, token11), + token1.treasury().transferUnitsTo(sender, 100, token1), + token2.treasury().transferUnitsTo(sender, 100, token2), + token3.treasury().transferUnitsTo(sender, 100, token3), + token4.treasury().transferUnitsTo(sender, 100, token4), + token5.treasury().transferUnitsTo(sender, 100, token5), + token6.treasury().transferUnitsTo(sender, 100, token6), + token7.treasury().transferUnitsTo(sender, 100, token7), + token8.treasury().transferUnitsTo(sender, 100, token8), + token9.treasury().transferUnitsTo(sender, 100, token9), + token10.treasury().transferUnitsTo(sender, 100, token10), + token11.treasury().transferUnitsTo(sender, 100, token11)); + final var token1Address = token1.addressOn(spec.targetNetworkOrThrow()); + final var token2Address = token2.addressOn(spec.targetNetworkOrThrow()); + final var token3Address = token3.addressOn(spec.targetNetworkOrThrow()); + final var token4Address = token4.addressOn(spec.targetNetworkOrThrow()); + final var token5Address = token5.addressOn(spec.targetNetworkOrThrow()); + final var token6Address = token6.addressOn(spec.targetNetworkOrThrow()); + final var token7Address = token7.addressOn(spec.targetNetworkOrThrow()); + final var token8Address = token8.addressOn(spec.targetNetworkOrThrow()); + final var token9Address = token9.addressOn(spec.targetNetworkOrThrow()); + final var token10Address = token10.addressOn(spec.targetNetworkOrThrow()); + final var token11Address = token11.addressOn(spec.targetNetworkOrThrow()); + allRunFor( + spec, + tokenReject + .call( + "rejectTokens", + sender, + new Address[] { + token1Address, + token2Address, + token3Address, + token4Address, + token5Address, + token6Address, + token7Address, + token8Address, + token9Address, + token10Address, + token11Address + }, + new Address[0]) + .gas(1_000_000L) + .andAssert(txn -> txn.hasKnownStatus(CONTRACT_REVERT_EXECUTED))); + })); + } + + @HapiTest + @DisplayName("Fails to reject tokens if there are no associated tokens") + public Stream failsIfNoAssociatedTokens(@FungibleToken SpecFungibleToken token) { + return hapiTest(withOpContext((spec, opLog) -> { + allRunFor(spec, sender.associateTokens(token)); + final var tokenAddress = token.addressOn(spec.targetNetworkOrThrow()); + allRunFor( + spec, + tokenReject + .call("rejectTokens", sender, new Address[] {tokenAddress}, new Address[0]) + .gas(1_000_000L) + .andAssert( + txn -> txn.hasKnownStatuses(CONTRACT_REVERT_EXECUTED, INSUFFICIENT_TOKEN_BALANCE))); + })); + } + + @HapiTest + @DisplayName("Fails if token is invalid") + public Stream failsIfTokenIsInvalid() { + return hapiTest(withOpContext((spec, opLog) -> { + final var senderAddress = sender.addressOn(spec.targetNetworkOrThrow()); + allRunFor( + spec, + tokenReject + .call("rejectTokens", sender, new Address[] {senderAddress}, new Address[0]) + .gas(1_000_000L) + .andAssert(txn -> txn.hasKnownStatuses(CONTRACT_REVERT_EXECUTED, INVALID_TOKEN_ID))); + })); + } + + @HapiTest + @DisplayName("Fails if NFT is invalid") + public Stream failsIfNFTIsInvalid() { + return hapiTest(withOpContext((spec, opLog) -> { + final var senderAddress = sender.addressOn(spec.targetNetworkOrThrow()); + allRunFor( + spec, + tokenReject + .call("rejectTokens", sender, new Address[] {}, new Address[] {senderAddress}) + .gas(1_000_000L) + .andAssert(txn -> txn.hasKnownStatuses(CONTRACT_REVERT_EXECUTED, INVALID_NFT_ID))); + })); + } +} diff --git a/hedera-node/test-clients/src/main/resources/contract/contracts/HRC904Reject/HRC904Reject.bin b/hedera-node/test-clients/src/main/resources/contract/contracts/HRC904Reject/HRC904Reject.bin new file mode 100644 index 000000000000..e593fa57b65c --- /dev/null +++ b/hedera-node/test-clients/src/main/resources/contract/contracts/HRC904Reject/HRC904Reject.bin @@ -0,0 +1 @@ +608060405234801561001057600080fd5b50600436106100365760003560e01c806376c6b3911461003b578063a869c78a14610059575b600080fd5b610043610089565b60405161005091906101a2565b60405180910390f35b610073600480360381019061006e9190610356565b610101565b60405161008091906101a2565b60405180910390f35b60003073ffffffffffffffffffffffffffffffffffffffff166376c6b3916040518163ffffffff1660e01b81526004016020604051808303816000875af11580156100d8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fc91906103b4565b905090565b60003073ffffffffffffffffffffffffffffffffffffffff1663a869c78a836040518263ffffffff1660e01b815260040161013c919061049f565b6020604051808303816000875af115801561015b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061017f91906103b4565b9050919050565b60008160070b9050919050565b61019c81610186565b82525050565b60006020820190506101b76000830184610193565b92915050565b6000604051905090565b600080fd5b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61021f826101d6565b810181811067ffffffffffffffff8211171561023e5761023d6101e7565b5b80604052505050565b60006102516101bd565b905061025d8282610216565b919050565b600067ffffffffffffffff82111561027d5761027c6101e7565b5b602082029050602081019050919050565b600080fd5b61029c81610186565b81146102a757600080fd5b50565b6000813590506102b981610293565b92915050565b60006102d26102cd84610262565b610247565b905080838252602082019050602084028301858111156102f5576102f461028e565b5b835b8181101561031e578061030a88826102aa565b8452602084019350506020810190506102f7565b5050509392505050565b600082601f83011261033d5761033c6101d1565b5b813561034d8482602086016102bf565b91505092915050565b60006020828403121561036c5761036b6101c7565b5b600082013567ffffffffffffffff81111561038a576103896101cc565b5b61039684828501610328565b91505092915050565b6000815190506103ae81610293565b92915050565b6000602082840312156103ca576103c96101c7565b5b60006103d88482850161039f565b91505092915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b61041681610186565b82525050565b6000610428838361040d565b60208301905092915050565b6000602082019050919050565b600061044c826103e1565b61045681856103ec565b9350610461836103fd565b8060005b83811015610492578151610479888261041c565b975061048483610434565b925050600181019050610465565b5085935050505092915050565b600060208201905081810360008301526104b98184610441565b90509291505056fea264697066735822122090079297d3821301f7590ff582a2d38263cf93cb98de3e7f4d3246266bd4951364736f6c63430008120033 \ No newline at end of file diff --git a/hedera-node/test-clients/src/main/resources/contract/contracts/HRC904Reject/HRC904Reject.json b/hedera-node/test-clients/src/main/resources/contract/contracts/HRC904Reject/HRC904Reject.json new file mode 100644 index 000000000000..e3c00f29a926 --- /dev/null +++ b/hedera-node/test-clients/src/main/resources/contract/contracts/HRC904Reject/HRC904Reject.json @@ -0,0 +1,34 @@ +[ + { + "inputs": [], + "name": "rejectTokenFT", + "outputs": [ + { + "internalType": "int64", + "name": "responseCode", + "type": "int64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int64[]", + "name": "serialNumbers", + "type": "int64[]" + } + ], + "name": "rejectTokenNFTs", + "outputs": [ + { + "internalType": "int64", + "name": "responseCode", + "type": "int64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/hedera-node/test-clients/src/main/resources/contract/contracts/HRC904Reject/HRC904Reject.sol b/hedera-node/test-clients/src/main/resources/contract/contracts/HRC904Reject/HRC904Reject.sol new file mode 100644 index 000000000000..823039d25ea0 --- /dev/null +++ b/hedera-node/test-clients/src/main/resources/contract/contracts/HRC904Reject/HRC904Reject.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +interface IHRC904TokenReject { + function rejectTokenFT() external returns (int64 responseCode); + function rejectTokenNFTs(int64[] memory serialNumbers) external returns (int64 responseCode); +} + +contract HRC904TokenReject is IHRC904TokenReject { + function rejectTokenFT() public returns (int64 responseCode) { + return HRC904TokenReject(this).rejectTokenFT(); + } + + function rejectTokenNFTs(int64[] memory serialNumbers) public returns (int64 responseCode) { + return HRC904TokenReject(this).rejectTokenNFTs(serialNumbers); + } +} \ No newline at end of file diff --git a/hedera-node/test-clients/src/main/resources/contract/contracts/TokenReject/TokenReject.bin b/hedera-node/test-clients/src/main/resources/contract/contracts/TokenReject/TokenReject.bin new file mode 100644 index 000000000000..5090e0452fc4 --- /dev/null +++ b/hedera-node/test-clients/src/main/resources/contract/contracts/TokenReject/TokenReject.bin @@ -0,0 +1 @@ +608060405234801561001057600080fd5b5061108d806100206000396000f3fe608060405234801561001057600080fd5b506004361061004c5760003560e01c806315dacbea146100515780632d03d39014610081578063618dc65e146100b15780639b23d3d9146100e2575b600080fd5b61006b600480360381019061006691906107cf565b610112565b6040516100789190610852565b60405180910390f35b61009b600480360381019061009691906109c6565b610230565b6040516100a89190610852565b60405180910390f35b6100cb60048036038101906100c69190610b06565b61035f565b6040516100d9929190610bfa565b60405180910390f35b6100fc60048036038101906100f791906107cf565b6104bb565b6040516101099190610852565b60405180910390f35b600080600061016773ffffffffffffffffffffffffffffffffffffffff166315dacbea60e01b8888888860405160240161014f9493929190610c48565b604051602081830303815290604052907bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040516101b99190610cc9565b6000604051808303816000865af19150503d80600081146101f6576040519150601f19603f3d011682016040523d82523d6000602084013e6101fb565b606091505b50915091508161020c576015610221565b808060200190518101906102209190610d19565b5b60030b92505050949350505050565b600080825167ffffffffffffffff81111561024e5761024d610883565b5b60405190808252806020026020018201604052801561028757816020015b6102746106f4565b81526020019060019003908161026c5790505b50905060005b83518110156103365761029e6106f4565b8482815181106102b1576102b0610d46565b5b6020026020010151816000019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff16815250506001816020019060070b908160070b815250508083838151811061031757610316610d46565b5b602002602001018190525050808061032e90610da4565b91505061028d565b506103428585836105d9565b9150601660030b8260070b1461035757600080fd5b509392505050565b6000606060008061016773ffffffffffffffffffffffffffffffffffffffff1663618dc65e60e01b878760405160240161039a929190610dec565b604051602081830303815290604052907bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040516104049190610cc9565b6000604051808303816000865af19150503d8060008114610441576040519150601f19603f3d011682016040523d82523d6000602084013e610446565b606091505b50915091507f4af4780e06fe8cb9df64b0794fa6f01399af979175bb988e35e0e57e594567bc828260405161047c929190610e37565b60405180910390a1816104a0576015604051806020016040528060008152506104a4565b6016815b8160030b9150809450819550505050509250929050565b600080600061016773ffffffffffffffffffffffffffffffffffffffff16639b23d3d960e01b888888886040516024016104f89493929190610c48565b604051602081830303815290604052907bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040516105629190610cc9565b6000604051808303816000865af19150503d806000811461059f576040519150601f19603f3d011682016040523d82523d6000602084013e6105a4565b606091505b5091509150816105b55760156105ca565b808060200190518101906105c99190610d19565b5b60030b92505050949350505050565b600080600061016773ffffffffffffffffffffffffffffffffffffffff1663ebd595e060e01b87878760405160240161061493929190611012565b604051602081830303815290604052907bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161067e9190610cc9565b6000604051808303816000865af19150503d80600081146106bb576040519150601f19603f3d011682016040523d82523d6000602084013e6106c0565b606091505b5091509150816106d15760156106e6565b808060200190518101906106e59190610d19565b5b60030b925050509392505050565b6040518060400160405280600073ffffffffffffffffffffffffffffffffffffffff168152602001600060070b81525090565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006107668261073b565b9050919050565b6107768161075b565b811461078157600080fd5b50565b6000813590506107938161076d565b92915050565b6000819050919050565b6107ac81610799565b81146107b757600080fd5b50565b6000813590506107c9816107a3565b92915050565b600080600080608085870312156107e9576107e8610731565b5b60006107f787828801610784565b945050602061080887828801610784565b935050604061081987828801610784565b925050606061082a878288016107ba565b91505092959194509250565b60008160070b9050919050565b61084c81610836565b82525050565b60006020820190506108676000830184610843565b92915050565b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6108bb82610872565b810181811067ffffffffffffffff821117156108da576108d9610883565b5b80604052505050565b60006108ed610727565b90506108f982826108b2565b919050565b600067ffffffffffffffff82111561091957610918610883565b5b602082029050602081019050919050565b600080fd5b600061094261093d846108fe565b6108e3565b905080838252602082019050602084028301858111156109655761096461092a565b5b835b8181101561098e578061097a8882610784565b845260208401935050602081019050610967565b5050509392505050565b600082601f8301126109ad576109ac61086d565b5b81356109bd84826020860161092f565b91505092915050565b6000806000606084860312156109df576109de610731565b5b60006109ed86828701610784565b935050602084013567ffffffffffffffff811115610a0e57610a0d610736565b5b610a1a86828701610998565b925050604084013567ffffffffffffffff811115610a3b57610a3a610736565b5b610a4786828701610998565b9150509250925092565b600080fd5b600067ffffffffffffffff821115610a7157610a70610883565b5b610a7a82610872565b9050602081019050919050565b82818337600083830152505050565b6000610aa9610aa484610a56565b6108e3565b905082815260208101848484011115610ac557610ac4610a51565b5b610ad0848285610a87565b509392505050565b600082601f830112610aed57610aec61086d565b5b8135610afd848260208601610a96565b91505092915050565b60008060408385031215610b1d57610b1c610731565b5b6000610b2b85828601610784565b925050602083013567ffffffffffffffff811115610b4c57610b4b610736565b5b610b5885828601610ad8565b9150509250929050565b6000819050919050565b610b7581610b62565b82525050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610bb5578082015181840152602081019050610b9a565b60008484015250505050565b6000610bcc82610b7b565b610bd68185610b86565b9350610be6818560208601610b97565b610bef81610872565b840191505092915050565b6000604082019050610c0f6000830185610b6c565b8181036020830152610c218184610bc1565b90509392505050565b610c338161075b565b82525050565b610c4281610799565b82525050565b6000608082019050610c5d6000830187610c2a565b610c6a6020830186610c2a565b610c776040830185610c2a565b610c846060830184610c39565b95945050505050565b600081905092915050565b6000610ca382610b7b565b610cad8185610c8d565b9350610cbd818560208601610b97565b80840191505092915050565b6000610cd58284610c98565b915081905092915050565b60008160030b9050919050565b610cf681610ce0565b8114610d0157600080fd5b50565b600081519050610d1381610ced565b92915050565b600060208284031215610d2f57610d2e610731565b5b6000610d3d84828501610d04565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610daf82610799565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610de157610de0610d75565b5b600182019050919050565b6000604082019050610e016000830185610c2a565b8181036020830152610e138184610bc1565b90509392505050565b60008115159050919050565b610e3181610e1c565b82525050565b6000604082019050610e4c6000830185610e28565b8181036020830152610e5e8184610bc1565b90509392505050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b610e9c8161075b565b82525050565b6000610eae8383610e93565b60208301905092915050565b6000602082019050919050565b6000610ed282610e67565b610edc8185610e72565b9350610ee783610e83565b8060005b83811015610f18578151610eff8882610ea2565b9750610f0a83610eba565b925050600181019050610eeb565b5085935050505092915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b610f5a81610836565b82525050565b604082016000820151610f766000850182610e93565b506020820151610f896020850182610f51565b50505050565b6000610f9b8383610f60565b60408301905092915050565b6000602082019050919050565b6000610fbf82610f25565b610fc98185610f30565b9350610fd483610f41565b8060005b83811015611005578151610fec8882610f8f565b9750610ff783610fa7565b925050600181019050610fd8565b5085935050505092915050565b60006060820190506110276000830186610c2a565b81810360208301526110398185610ec7565b9050818103604083015261104d8184610fb4565b905094935050505056fea2646970667358221220d250baf2969ae38cbdd5b139677c6d86b83ae55c63a9c887b987371f75cb30a464736f6c63430008120033 \ No newline at end of file diff --git a/hedera-node/test-clients/src/main/resources/contract/contracts/TokenReject/TokenReject.json b/hedera-node/test-clients/src/main/resources/contract/contracts/TokenReject/TokenReject.json new file mode 100644 index 000000000000..943654a29e89 --- /dev/null +++ b/hedera-node/test-clients/src/main/resources/contract/contracts/TokenReject/TokenReject.json @@ -0,0 +1,147 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "", + "type": "bool" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "CallResponseEvent", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "bytes", + "name": "encodedFunctionSelector", + "type": "bytes" + } + ], + "name": "redirectForToken", + "outputs": [ + { + "internalType": "int256", + "name": "responseCode", + "type": "int256" + }, + { + "internalType": "bytes", + "name": "response", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "rejectingAddress", + "type": "address" + }, + { + "internalType": "address[]", + "name": "ftAddresses", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "nftAddresses", + "type": "address[]" + } + ], + "name": "rejectTokens", + "outputs": [ + { + "internalType": "int64", + "name": "responseCode", + "type": "int64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "int64", + "name": "responseCode", + "type": "int64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "serialNumber", + "type": "uint256" + } + ], + "name": "transferFromNFT", + "outputs": [ + { + "internalType": "int64", + "name": "responseCode", + "type": "int64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/hedera-node/test-clients/src/main/resources/contract/contracts/TokenReject/TokenReject.sol b/hedera-node/test-clients/src/main/resources/contract/contracts/TokenReject/TokenReject.sol new file mode 100644 index 000000000000..8d8fb722afab --- /dev/null +++ b/hedera-node/test-clients/src/main/resources/contract/contracts/TokenReject/TokenReject.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.9.0; +pragma experimental ABIEncoderV2; + +import "./HederaTokenService.sol"; + +contract TokenReject is HederaTokenService { + + function rejectTokens(address rejectingAddress, address[] memory ftAddresses, address[] memory nftAddresses) public returns(int64 responseCode) { + IHederaTokenService.NftID[] memory nftIDs = new IHederaTokenService.NftID[](nftAddresses.length); + for (uint i; i < nftAddresses.length; i++) + { + IHederaTokenService.NftID memory nftId; + nftId.nft = nftAddresses[i]; + nftId.serial = 1; + nftIDs[i] = nftId; + } + responseCode = rejectTokens(rejectingAddress, ftAddresses, nftIDs); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } +} \ No newline at end of file