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: add additional tests for HIP-904 #17773

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Changes from 4 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 @@ -16,23 +16,33 @@

package com.hedera.services.bdd.suites.contract.precompile.airdrops;

import static com.hedera.node.app.hapi.utils.EthSigsUtils.recoverAddressFromPubKey;
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.recordWith;
import static com.hedera.services.bdd.spec.dsl.entities.SpecTokenKey.FEE_SCHEDULE_KEY;
import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAutoCreatedAccountBalance;
import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoUpdate;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenClaimAirdrop;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenFeeScheduleUpdate;
import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fractionalFeeNetOfTransfers;
import static com.hedera.services.bdd.spec.transactions.token.HapiTokenClaimAirdrop.pendingAirdrop;
import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving;
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.contract.Utils.accountId;
import static com.hedera.services.bdd.suites.contract.Utils.asAddress;
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 com.google.protobuf.ByteString;
import com.hedera.services.bdd.junit.HapiTest;
import com.hedera.services.bdd.junit.HapiTestLifecycle;
import com.hedera.services.bdd.junit.OrderedInIsolation;
Expand All @@ -48,6 +58,7 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.List;
import java.util.OptionalLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DynamicTest;
Expand Down Expand Up @@ -292,4 +303,264 @@ public Stream<DynamicTest> airdropToAccountWithFreeAutoAssocSlots(
receiver.getBalance().andAssert(balance -> balance.hasTokenBalance(nft.name(), 1L)));
}));
}

@Order(7)
@HapiTest
@DisplayName("Contract account airdrops multiple tokens to contract alias with unlimited association slots")
public Stream<DynamicTest> airdropToAccountWithUnlimitedAutoAssocSlots(
@NonNull @Contract(contract = "EmptyOne", creationGas = 100_000_000L, maxAutoAssociations = -1)
final SpecContract receiver,
@NonNull @Contract(contract = "EmptyOne", creationGas = 100_000_000L) final SpecContract sender,
@NonNull @FungibleToken(initialSupply = 1_000_000L) final SpecFungibleToken token1,
@NonNull @FungibleToken(initialSupply = 1_000_000L) final SpecFungibleToken token2) {
return hapiTest(withOpContext((spec, opLog) -> {
allRunFor(
spec,
sender.authorizeContract(airdropContract),
sender.associateTokens(token1, token2),
token1.treasury().transferUnitsTo(sender, 1_000L, token1),
token2.treasury().transferUnitsTo(sender, 1_000L, token2));
allRunFor(spec, checkForEmptyBalance(receiver, List.of(token1, token2), List.of()));
allRunFor(
spec,
airdropContract
.call(
"tokenNAmountAirdrops",
prepareTokenAddresses(spec, List.of(token1, token2)),
prepareContractAddresses(spec, List.of(sender, sender)),
prepareContractAddresses(spec, List.of(receiver, receiver)),
10L)
.sending(450_000_000L)
.gas(1_750_000L)
.via("AirdropTxn"),
receiver.getBalance().andAssert(balance -> balance.hasTokenBalance(token1.name(), 10L)),
receiver.getBalance().andAssert(balance -> balance.hasTokenBalance(token2.name(), 10L)),
getTxnRecord("AirdropTxn").hasChildRecords(recordWith().pendingAirdropsCount(0)));
}));
}

@Order(8)
@HapiTest
@DisplayName("Contract account airdrops a multiple tokens to an account alias without free association slots")
public Stream<DynamicTest> airdropToAccountWithNoFreeAutoAssocSlots(
@NonNull @Account(maxAutoAssociations = 0, tinybarBalance = 100_000_000L) final SpecAccount receiver,
@NonNull @Contract(contract = "EmptyOne", creationGas = 100_000_000L) final SpecContract sender,
@NonNull @FungibleToken(initialSupply = 1_000L) final SpecFungibleToken token1,
@NonNull @FungibleToken(initialSupply = 1_000L) final SpecFungibleToken token2) {
return hapiTest(withOpContext((spec, opLog) -> {
allRunFor(
spec,
sender.authorizeContract(airdropContract),
sender.associateTokens(token1, token2),
token1.treasury().transferUnitsTo(sender, 1_000L, token1),
token2.treasury().transferUnitsTo(sender, 1_000L, token2));
allRunFor(spec, checkForEmptyBalance(receiver, List.of(token1, token2), List.of()));
allRunFor(
spec,
airdropContract
.call(
"tokenNAmountAirdrops",
prepareTokenAddresses(spec, List.of(token1, token2)),
prepareContractAddresses(spec, List.of(sender, sender)),
prepareAccountAddresses(spec, List.of(receiver, receiver)),
10L)
.sending(450_000_000L)
.gas(1_750_000L)
.via("AirdropTxn"),
receiver.getBalance().andAssert(balance -> balance.hasTokenBalance(token1.name(), 0L)),
receiver.getBalance().andAssert(balance -> balance.hasTokenBalance(token2.name(), 0L)),
getTxnRecord("AirdropTxn").hasChildRecords(recordWith().pendingAirdropsCount(2)),
getTxnRecord("AirdropTxn")
.hasChildRecords(recordWith()
.pendingAirdrops(includingFungiblePendingAirdrop(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to get the record again, you can just chain the pending checks

moving(10L, token1.name()).between(sender.name(), receiver.name()),
moving(10L, token2.name()).between(sender.name(), receiver.name())))));
}));
}

@Order(9)
@HapiTest
@DisplayName("Contract account airdrops a multiple tokens to an address with no account on it.")
public Stream<DynamicTest> airdropToAddressWithNoAccount(
@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) final SpecFungibleToken token) {

final var hollowAccountKey = "hollowAccountKey";
final AtomicReference<ByteString> hollowAccountAlias = new AtomicReference<>();

return hapiTest(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only airdrops to a hollow account. You can check Create2OperationSuite to get a reference on how to do the create2 part with a hapi test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the description and the method name here were wrong, this test does not need create2 contract.

newKeyNamed(hollowAccountKey).shape(KeyShape.SECP256K1),
sender.getBalance(),
withOpContext((spec, opLog) -> {
final var ecdsaKey = spec.registry()
.getKey(hollowAccountKey)
.getECDSASecp256K1()
.toByteArray();
final var evmAddressBytes = ByteString.copyFrom(recoverAddressFromPubKey(ecdsaKey));
hollowAccountAlias.set(evmAddressBytes);
contractCall(
"Airdrop",
"tokenAirdrop",
token,
sender.addressOn(spec.targetNetworkOrThrow()),
asAddress(accountId(hollowAccountAlias.get())),
10L)
.sending(85_000_000L)
.gas(1_500_000L)
.via("AirdropTxn");
getAutoCreatedAccountBalance(hollowAccountKey).hasTokenBalance(token.name(), 10L);
}));
}

@Order(10)
@HapiTest
@DisplayName(
"Contract airdrops a token to an account, then the receiver claims the airdrop, then the sender airdrops the same token again ")
public Stream<DynamicTest> airdropToAccountAgainAfterReceiverClaims(
@NonNull @Account(maxAutoAssociations = 0, tinybarBalance = 100_000_000L) final SpecAccount receiver,
@NonNull @Contract(contract = "EmptyOne", creationGas = 100_000_000L) final SpecContract sender,
@NonNull @FungibleToken(initialSupply = 1_000L) final SpecFungibleToken token) {
return hapiTest(withOpContext((spec, opLog) -> {
allRunFor(
spec,
sender.authorizeContract(airdropContract),
sender.associateTokens(token),
token.treasury().transferUnitsTo(sender, 10L, token));
allRunFor(
spec,
airdropContract
.call("tokenAirdrop", token, sender, receiver, 5L)
.sending(85_000_000L)
.gas(1_500_000L)
.via("AirdropTxn"),
receiver.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 0L)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also verify the PendingAirdrop from the record

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check is added.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also add the including*PendingAirdrop

getTxnRecord("AirdropTxn").hasChildRecords(recordWith().pendingAirdropsCount(1)),
sender.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 10L)));
allRunFor(
spec,
tokenClaimAirdrop(pendingAirdrop(sender.name(), receiver.name(), token.name()))
.payingWith(receiver.name()),
receiver.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 5L)),
sender.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 5L)));
allRunFor(
spec,
airdropContract
.call("tokenAirdrop", token, sender, receiver, 3L)
.sending(85_000_000L)
.gas(1_500_000L),
receiver.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 8L)),
sender.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 2L)));
}));
}

@Order(11)
@HapiTest
@DisplayName("Contract airdrops multiple times that FT to an account, then only one pending transaction is created")
public Stream<DynamicTest> airdropFTToAccountMultipleTimes(
@NonNull @Account(maxAutoAssociations = 0, tinybarBalance = 100_000_000L) final SpecAccount receiver,
@NonNull @Contract(contract = "EmptyOne", creationGas = 100_000_000L) final SpecContract sender,
@NonNull @FungibleToken(initialSupply = 1_000L) final SpecFungibleToken token) {
return hapiTest(withOpContext((spec, opLog) -> {
allRunFor(
spec,
sender.authorizeContract(airdropContract),
sender.associateTokens(token),
token.treasury().transferUnitsTo(sender, 10L, token));
allRunFor(
spec,
airdropContract
.call("tokenAirdrop", token, sender, receiver, 1L)
.sending(85_000_000L)
.gas(1_500_000L)
.via("AirdropTxn"),
airdropContract
.call("tokenAirdrop", token, sender, receiver, 1L)
.sending(85_000_000L)
.gas(1_500_000L)
.via("AirdropTxn"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the transaction ids should be different

airdropContract
.call("tokenAirdrop", token, sender, receiver, 1L)
.sending(85_000_000L)
.gas(1_500_000L)
.via("AirdropTxn"),
getTxnRecord("AirdropTxn").hasChildRecords(recordWith().pendingAirdropsCount(1)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

receiver.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 0L)));
allRunFor(
spec,
tokenClaimAirdrop(pendingAirdrop(sender.name(), receiver.name(), token.name()))
.payingWith(receiver.name()),
receiver.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 3L)));
}));
}

@Order(12)
@HapiTest
@DisplayName(
"Contract airdrops a FT to an account, then associate the receiver, then airdrops the same token again")
public Stream<DynamicTest> airdropFTToAccountThenAssociateTheReceiverAndAirdropAgain(
@NonNull @Account(maxAutoAssociations = 0, tinybarBalance = 100_000_000L) final SpecAccount receiver,
@NonNull @Contract(contract = "EmptyOne", creationGas = 100_000_000L) final SpecContract sender,
@NonNull @FungibleToken(initialSupply = 1_000L) final SpecFungibleToken token) {
return hapiTest(withOpContext((spec, opLog) -> {
allRunFor(
spec,
sender.authorizeContract(airdropContract),
sender.associateTokens(token),
token.treasury().transferUnitsTo(sender, 10L, token));
allRunFor(
spec,
airdropContract
.call("tokenAirdrop", token, sender, receiver, 1L)
.sending(85_000_000L)
.gas(1_500_000L)
.via("AirdropTxn"),
getTxnRecord("AirdropTxn").hasChildRecords(recordWith().pendingAirdropsCount(1)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here

receiver.associateTokens(token),
airdropContract
.call("tokenAirdrop", token, sender, receiver, 1L)
.sending(85_000_000L)
.gas(1_500_000L)
.via("AirdropTxn"),
getTxnRecord("AirdropTxn").hasChildRecords(recordWith().pendingAirdropsCount(0)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use different transaction ids as this might result in race conditions and leaky tests

receiver.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 1L)));
}));
}

@Order(13)
@HapiTest
@DisplayName("Multiple contracts airdrop tokens to multiple accounts.")
public Stream<DynamicTest> multipleContractsAirdropTokensToMultipleAccounts(
@NonNull @Account(maxAutoAssociations = 1, tinybarBalance = 100_000_000L) final SpecAccount receiver1,
@NonNull @Account(maxAutoAssociations = 1, tinybarBalance = 100_000_000L) final SpecAccount receiver2,
@NonNull @Contract(contract = "EmptyOne", creationGas = 100_000_000L) final SpecContract sender1,
@NonNull @Contract(contract = "EmptyConstructor", creationGas = 100_000_000L) final SpecContract sender2,
@NonNull @FungibleToken(initialSupply = 1_000L) final SpecFungibleToken token) {
return hapiTest(withOpContext((spec, opLog) -> {
allRunFor(
spec,
sender1.authorizeContract(airdropContract),
sender2.authorizeContract(airdropContract),
sender1.associateTokens(token),
sender2.associateTokens(token),
token.treasury().transferUnitsTo(sender1, 10L, token),
token.treasury().transferUnitsTo(sender2, 10L, token));
allRunFor(
spec,
airdropContract
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it should be only one airdrop with multiple senders and receivers.
You can check the multiAirdrops() tests in AirdropToContractSystemContractTest

.call("tokenAirdrop", token, sender1, receiver1, 1L)
.sending(85_000_000L)
.gas(1_500_000L)
.via("AirdropTxn"),
airdropContract
.call("tokenAirdrop", token, sender2, receiver2, 1L)
.sending(85_000_000L)
.gas(1_500_000L),
getTxnRecord("AirdropTxn").hasChildRecords(recordWith().pendingAirdropsCount(0)),
receiver1.getBalance().andAssert(balance -> balance.hasTinyBars(100_000_000L)),
receiver2.getBalance().andAssert(balance -> balance.hasTinyBars(100_000_000L)),
receiver1.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 1L)),
receiver2.getBalance().andAssert(balance -> balance.hasTokenBalance(token.name(), 1L)));
}));
}
}