Skip to content

Commit

Permalink
feat: HIP-904 Reject Tokens System Contract implementation (#16118)
Browse files Browse the repository at this point in the history
Signed-off-by: Stanimir Stoyanov <stanimir.stoyanov@limechain.tech>
  • Loading branch information
stoyanov-st authored Oct 28, 2024
1 parent 4c13304 commit 9cc0ab8
Show file tree
Hide file tree
Showing 15 changed files with 1,483 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -468,4 +469,13 @@ static CallTranslator<HtsCallAttempt> provideTokenCancelAirdropTranslator(
@NonNull final TokenCancelAirdropTranslator translator) {
return translator;
}

@Provides
@Singleton
@IntoSet
@Named("HtsTranslators")
static CallTranslator<HtsCallAttempt> provideTokenRejectsTranslator(
@NonNull final RejectTokensTranslator translator) {
return translator;
}
}
Original file line number Diff line number Diff line change
@@ -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<TokenReference>();
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<TokenReference>();
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();
}
}
Original file line number Diff line number Diff line change
@@ -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<HtsCallAttempt> {
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<Function, DispatchGasCalculator> 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);
}
}
}
Loading

0 comments on commit 9cc0ab8

Please sign in to comment.