Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: HIP-904 negative cases for airdrops from contract #17772

Merged
merged 4 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,35 @@

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.assertions.TransactionRecordAsserts.includingFungiblePendingAirdrop;
import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.includingNftPendingAirdrop;
import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith;
import static com.hedera.services.bdd.spec.dsl.entities.SpecTokenKey.ADMIN_KEY;
import static com.hedera.services.bdd.spec.dsl.entities.SpecTokenKey.FEE_SCHEDULE_KEY;
import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractDelete;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoUpdate;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenFeeScheduleUpdate;
import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFee;
import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fractionalFeeNetOfTransfers;
import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.movingUnique;
import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor;
import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed;
import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext;
import static com.hedera.services.bdd.suites.HapiSuite.GENESIS;
import static com.hedera.services.bdd.suites.contract.precompile.airdrops.SystemContractAirdropHelper.checkForBalances;
import static com.hedera.services.bdd.suites.contract.precompile.airdrops.SystemContractAirdropHelper.checkForEmptyBalance;
import static com.hedera.services.bdd.suites.contract.precompile.airdrops.SystemContractAirdropHelper.prepareAccountAddresses;
import static com.hedera.services.bdd.suites.contract.precompile.airdrops.SystemContractAirdropHelper.prepareContractAddresses;
import static com.hedera.services.bdd.suites.contract.precompile.airdrops.SystemContractAirdropHelper.prepareTokenAddresses;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_HAS_PENDING_AIRDROPS;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_REPEATED_IN_ACCOUNT_AMOUNTS;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_TOKEN_BALANCE;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.PENDING_NFT_AIRDROP_ALREADY_EXISTS;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_ID_REPEATED_IN_TOKEN_LIST;

import com.hedera.services.bdd.junit.HapiTest;
import com.hedera.services.bdd.junit.HapiTestLifecycle;
Expand All @@ -45,12 +60,14 @@
import com.hedera.services.bdd.spec.dsl.entities.SpecFungibleToken;
import com.hedera.services.bdd.spec.dsl.entities.SpecNonFungibleToken;
import com.hedera.services.bdd.spec.keys.KeyShape;
import com.hedera.services.bdd.spec.transactions.token.TokenMovement;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.List;
import java.util.OptionalLong;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Tag;

Expand Down Expand Up @@ -292,4 +309,206 @@ public Stream<DynamicTest> airdropToAccountWithFreeAutoAssocSlots(
receiver.getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 1L)));
}));
}

@Nested
class AirdropFromContractNegativeCases {

@HapiTest
@DisplayName("Airdrop token with custom fees while the sender cannot pay the fees")
public Stream<DynamicTest> airdropFromContractWhileTheSenderCannotPayTheCustomFees(
@NonNull @Account(maxAutoAssociations = 10, tinybarBalance = 100L) final SpecAccount receiver,
@Contract(contract = "EmptyOne", creationGas = 10_000_000L) final SpecContract sender,
@NonNull
@FungibleToken(
initialSupply = 1_000_000L,
name = "airdropToken",
keys = {ADMIN_KEY, FEE_SCHEDULE_KEY})
final SpecFungibleToken token,
@NonNull @FungibleToken(initialSupply = 1_000_000L) final SpecFungibleToken tokenForFee) {
return hapiTest(withOpContext((spec, opLog) -> {
allRunFor(
spec,
sender.associateTokens(token, tokenForFee),
tokenAssociate(GENESIS, tokenForFee.name()),
sender.authorizeContract(airdropContract),
token.treasury().transferUnitsTo(sender, 500_000L, token),
tokenFeeScheduleUpdate(token.name()).withCustom(fixedHtsFee(5L, tokenForFee.name(), GENESIS)));
allRunFor(
spec,
airdropContract
.call("tokenAirdrop", token, sender, receiver, 10L)
.sending(85_000_000L)
.gas(1_500_000L)
.via("AirdropTxn")
.andAssert(txn -> txn.hasKnownStatuses(
CONTRACT_REVERT_EXECUTED, INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE)));
}));
}

@HapiTest
@DisplayName("Airdrop nft to the same account twice should fail with PENDING_NFT_AIRDROP_ALREADY_EXISTS")
public Stream<DynamicTest> airdropNftToTheSameAccountTwice(
@NonNull @Account(maxAutoAssociations = 10, tinybarBalance = 100L) final SpecAccount associatedReceiver,
@NonNull @Account(maxAutoAssociations = 0, tinybarBalance = 100L)
final SpecAccount notAssociatedReceiver,
@Contract(contract = "EmptyOne", creationGas = 10_000_000L) final SpecContract sender,
@NonNull @NonFungibleToken(numPreMints = 1) final SpecNonFungibleToken nft) {
return hapiTest(withOpContext((spec, opLog) -> {
allRunFor(
spec,
sender.authorizeContract(airdropContract),
sender.associateTokens(nft),
nft.treasury().transferNFTsTo(sender, nft, 1L),
associatedReceiver.associateTokens(nft),
notAssociatedReceiver
.getBalance()
.andAssert(balance -> balance.hasTokenBalance(nft.name(), 0L)));
allRunFor(
spec,
// Airdrop the same nft serial to the same account
// when the account is already associated with the nft we don't get a pending airdrop
// so when we try to do so with a single airdrop it will fail with
// TOKEN_ID_REPEATED_IN_TOKEN_LIST
airdropContract
.call(
"nftNAmountAirdrops",
prepareTokenAddresses(spec, nft, nft, nft),
prepareContractAddresses(spec, sender, sender, sender),
prepareAccountAddresses(
spec, associatedReceiver, associatedReceiver, associatedReceiver),
new long[] {1L, 1L, 1L})
.sending(85_000_000L)
.gas(1_500_000L)
.andAssert(txn -> txn.hasKnownStatuses(
CONTRACT_REVERT_EXECUTED, TOKEN_ID_REPEATED_IN_TOKEN_LIST)));
allRunFor(
spec,
// We validate the same case but with an account that is not associated with the nft
airdropContract
.call(
"nftNAmountAirdrops",
prepareTokenAddresses(spec, nft, nft, nft),
prepareContractAddresses(spec, sender, sender, sender),
prepareAccountAddresses(
spec,
notAssociatedReceiver,
notAssociatedReceiver,
notAssociatedReceiver),
new long[] {1L, 1L, 1L})
.sending(85_000_000L)
.gas(1_500_000L)
.andAssert(txn -> txn.hasKnownStatuses(
CONTRACT_REVERT_EXECUTED, TOKEN_ID_REPEATED_IN_TOKEN_LIST)));
allRunFor(
spec,
// Now we airdrop a single to the same account to go to the pending airdrop list
airdropContract
.call("nftAirdrop", nft, sender, notAssociatedReceiver, 1L)
.sending(85_000_000L)
.gas(1_500_000L)
.via("AirdropToPendingState"),
getTxnRecord("AirdropToPendingState")
.hasChildRecords(recordWith()
.pendingAirdrops(includingNftPendingAirdrop(movingUnique(nft.name(), 1L)
.between(sender.name(), notAssociatedReceiver.name())))));
allRunFor(
spec,
// Now we try to airdrop the same nft serial to the same account that should fail with
// PENDING_NFT_AIRDROP_ALREADY_EXISTS
airdropContract
.call("nftAirdrop", nft, sender, notAssociatedReceiver, 1L)
.sending(85_000_000L)
.gas(1_500_000L)
.andAssert(txn -> txn.hasKnownStatuses(
CONTRACT_REVERT_EXECUTED, PENDING_NFT_AIRDROP_ALREADY_EXISTS)));
}));
}

@HapiTest
@DisplayName(
"Airdrop token amount of Long.MAX_VALUE then try to airdrop 1 more token to the same receiver should fail")
public Stream<DynamicTest> airdropMaxLongPlusOneShouldFail(
@NonNull @FungibleToken(initialSupply = Long.MAX_VALUE) final SpecFungibleToken token,
@NonNull @Contract(contract = "EmptyOne", creationGas = 100_000_000L) final SpecContract sender,
@NonNull @Account(maxAutoAssociations = 1, tinybarBalance = 100L) final SpecAccount receiver) {
return hapiTest(withOpContext((spec, opLog) -> {
allRunFor(
spec,
sender.authorizeContract(airdropContract),
sender.associateTokens(token),
token.treasury().transferUnitsTo(sender, Long.MAX_VALUE, token));
allRunFor(
spec,
airdropContract
.call("tokenAirdrop", token, sender, receiver, Long.MAX_VALUE)
.sending(85_000_000L)
.gas(1_500_000L));
allRunFor(
spec,
receiver.getBalance()
.andAssert(balance -> balance.hasTokenBalance(token.name(), Long.MAX_VALUE)));
allRunFor(
spec,
airdropContract
.call("tokenAirdrop", token, sender, receiver, 1L)
.sending(85_000_000L)
.gas(1_500_000L)
.andAssert(txn ->
txn.hasKnownStatuses(CONTRACT_REVERT_EXECUTED, INSUFFICIENT_TOKEN_BALANCE)));
}));
}

@HapiTest
@DisplayName("Contract tries to airdrop a token to itself")
public Stream<DynamicTest> airdropTokenToItself(
@NonNull @FungibleToken(initialSupply = 1_000_000L) final SpecFungibleToken token,
@NonNull @Contract(contract = "EmptyOne", creationGas = 10_000_000L) final SpecContract sender) {
return hapiTest(withOpContext((spec, opLog) -> {
allRunFor(
spec,
sender.authorizeContract(airdropContract),
sender.associateTokens(token),
token.treasury().transferUnitsTo(sender, 1_000L, token));
allRunFor(
spec,
airdropContract
.call("tokenAirdrop", token, sender, sender, 10L)
.sending(85_000_000L)
.gas(1_500_000L)
.andAssert(txn -> txn.hasKnownStatuses(
CONTRACT_REVERT_EXECUTED, ACCOUNT_REPEATED_IN_ACCOUNT_AMOUNTS)));
}));
}

@HapiTest
@DisplayName("Contract airdrops to a pending state then tries to SELFDESTRUCT")
public Stream<DynamicTest> contractAirdropsThenSelfdestructs(
@NonNull @FungibleToken(initialSupply = 100) final SpecFungibleToken token,
@NonNull @Contract(contract = "EmptyOne", creationGas = 100_000_000L) final SpecContract contract,
@NonNull @Account(maxAutoAssociations = 0, tinybarBalance = 100L) final SpecAccount receiver) {
return hapiTest(withOpContext((spec, opLog) -> {
allRunFor(
spec,
contract.authorizeContract(airdropContract),
contract.associateTokens(token),
token.treasury().transferUnitsTo(contract, 100, token));
allRunFor(
spec,
airdropContract
.call("tokenAirdrop", token, contract, receiver, 10L)
.sending(85_000_000L)
.gas(1_500_000L)
.via("pendingAirdropTxn"),
getTxnRecord("pendingAirdropTxn")
.hasChildRecords(recordWith()
.pendingAirdrops(
includingFungiblePendingAirdrop(TokenMovement.moving(10L, token.name())
.between(contract.name(), receiver.name())))));
allRunFor(
spec,
contractDelete(contract.name())
.hasKnownStatusFrom(CONTRACT_REVERT_EXECUTED, ACCOUNT_HAS_PENDING_AIRDROPS));
}));
}
}
}
Loading
Loading