Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// SPDX-License-Identifier: Apache-2.0
package com.hedera.services.bdd.suites.hip1195;

import static com.hedera.services.bdd.junit.hedera.embedded.EmbeddedMode.CONCURRENT;
import static com.hedera.services.bdd.spec.HapiSpec.hapiTest;
import static com.hedera.services.bdd.spec.assertions.AutoAssocAsserts.accountTokenPairs;
import static com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts.resultWith;
import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith;
import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord;
import static com.hedera.services.bdd.spec.transactions.TxnUtils.accountAllowanceHook;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate;
import static com.hedera.services.bdd.suites.HapiSuite.DEFAULT_PAYER;
import static com.hedera.services.bdd.suites.contract.Utils.aaWith;
import static com.hedera.services.bdd.suites.contract.Utils.aaWithPreHook;
import static com.hedera.services.bdd.suites.contract.Utils.asSolidityAddress;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.REJECTED_BY_ACCOUNT_ALLOWANCE_HOOK;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS;

import com.hedera.hapi.node.base.EvmHookCall;
import com.hedera.hapi.node.base.HookCall;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import com.hedera.services.bdd.junit.HapiTest;
import com.hedera.services.bdd.junit.HapiTestLifecycle;
import com.hedera.services.bdd.junit.TargetEmbeddedMode;
import com.hedera.services.bdd.junit.support.TestLifecycle;
import com.hedera.services.bdd.spec.dsl.annotations.Contract;
import com.hedera.services.bdd.spec.dsl.entities.SpecContract;
import com.hederahashgraph.api.proto.java.TokenTransferList;
import com.hederahashgraph.api.proto.java.TransferList;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DynamicTest;

@HapiTestLifecycle
@TargetEmbeddedMode(CONCURRENT)
public class HookTimingBalanceOrderTest {
private static final String OWNER = "owner";
private static final String RECEIVER = "receiver";
private static final String SENDER = "sender";
private static final String FT = "ft";
private static final long HOOK_ID = 1L;
private static final String HOOK_CONTRACT_NUM = "365"; // All EVM hooks execute at 0.0.365

@Contract(contract = "TruePreHook", creationGas = 5000_000L)
static SpecContract TRUE_PRE_ALLOWANCE_HOOK;

@Contract(contract = "TransferTokenHook", creationGas = 5000_000L)
static SpecContract TRANSFER_TOKEN_HOOK;

@BeforeAll
static void beforeAll(@NonNull final TestLifecycle testLifecycle) {
testLifecycle.overrideInClass(Map.of("hooks.hooksEnabled", "true"));
testLifecycle.doAdhoc(TRUE_PRE_ALLOWANCE_HOOK.getInfo());
testLifecycle.doAdhoc(TRANSFER_TOKEN_HOOK.getInfo());
}

@HapiTest
final Stream<DynamicTest> ownerReceivesBeforeSpendingSucceedsAndRecordValid() {
return hapiTest(
cryptoCreate(OWNER).withHook(accountAllowanceHook(HOOK_ID, TRANSFER_TOKEN_HOOK.name())),
tokenCreate(FT).initialSupply(1_000).treasury(OWNER),
cryptoCreate(RECEIVER).maxAutomaticTokenAssociations(10),
cryptoCreate(SENDER).maxAutomaticTokenAssociations(10),
cryptoTransfer((spec, builder) -> {
final var registry = spec.registry();
final var tokenAddr = asSolidityAddress(registry.getTokenID(FT));
final var toAddr = asSolidityAddress(registry.getAccountID(SENDER));
final var amount = 20L;
final var encoded = abiEncodeAddressAddressInt64(tokenAddr, toAddr, amount);
final var hookCall = HookCall.newBuilder()
.hookId(HOOK_ID)
.evmHookCall(EvmHookCall.newBuilder()
.gasLimit(5000_000L)
.data(Bytes.wrap(encoded)))
.build();
builder.setTransfers(TransferList.newBuilder()
.addAccountAmounts(
aaWithPreHook(registry.getAccountID(OWNER), -20, hookCall))
.addAccountAmounts(aaWith(registry.getAccountID(SENDER), +20))
.build())
.addTokenTransfers(TokenTransferList.newBuilder()
.setToken(registry.getTokenID(FT))
.addAllTransfers(List.of(
aaWith(registry.getAccountID(SENDER), -10),
aaWith(registry.getAccountID(RECEIVER), +10)))
.build());
})
.signedBy(DEFAULT_PAYER, SENDER)
.via("preHookReceiveThenSpend"),
// There should be a successful child record for the hook call
getTxnRecord("preHookReceiveThenSpend")
.andAllChildRecords()
.logged()
.hasNonStakingChildRecordCount(2)
.hasChildRecords(
recordWith()
.status(SUCCESS)
.contractCallResult(resultWith().contract(HOOK_CONTRACT_NUM)),
recordWith()
.status(SUCCESS)
.autoAssociated(accountTokenPairs(List.of(Pair.of(SENDER, FT))))));
}

@HapiTest
final Stream<DynamicTest> ownerSpendsBeforeReceivingFailsAtBalanceCheck() {
return hapiTest(
// Register a hook that does NOT fund in pre; any funding would come too late (post)
cryptoCreate(OWNER).withHook(accountAllowanceHook(HOOK_ID, TRANSFER_TOKEN_HOOK.name())),
cryptoCreate(SENDER).maxAutomaticTokenAssociations(10),
cryptoCreate(RECEIVER).maxAutomaticTokenAssociations(10),
// Create the token with SENDER as treasury so OWNER starts with 0 balance
tokenCreate(FT).initialSupply(1_000).treasury(SENDER),
// Ensure OWNER and RECEIVER are associated to the token
tokenAssociate(OWNER, FT),
tokenAssociate(RECEIVER, FT),

// OWNER tries to spend FT before receiving any; pre-hook does not fund -> should fail at balance check
cryptoTransfer((spec, builder) -> {
final var registry = spec.registry();
final var hookCall = HookCall.newBuilder()
.hookId(HOOK_ID)
.evmHookCall(EvmHookCall.newBuilder().gasLimit(5000_000L))
.build();
builder.addTokenTransfers(TokenTransferList.newBuilder()
.setToken(registry.getTokenID(FT))
.addAllTransfers(List.of(
aaWithPreHook(registry.getAccountID(OWNER), -10, hookCall),
aaWith(registry.getAccountID(RECEIVER), +10)))
.build());
})
.signedBy(DEFAULT_PAYER, OWNER)
.hasKnownStatus(REJECTED_BY_ACCOUNT_ALLOWANCE_HOOK)
.via("spendBeforeReceiveFails"),
// Log the record and any children for debugging; no child record assertions since parent fails early
getTxnRecord("spendBeforeReceiveFails")
.andAllChildRecords()
.hasChildRecords(recordWith().status(CONTRACT_REVERT_EXECUTED))
.logged());
}

private static byte[] abiEncodeAddressAddressInt64(byte[] addr1, byte[] addr2, long amount) {
final byte[] out = new byte[96];
// address addr1 in first 32-byte word (left-padded)
System.arraycopy(addr1, 0, out, 12, 20);
// address addr2 in second 32-byte word (left-padded)
System.arraycopy(addr2, 0, out, 32 + 12, 20);
// int64 amount in third 32-byte word (left-padded)
final byte[] amt = ByteBuffer.allocate(8).putLong(amount).array();
System.arraycopy(amt, 0, out, 64 + 24, 8);
return out;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode;
import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFee;
import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor;
import static com.hedera.services.bdd.spec.utilops.EmbeddedVerbs.viewAccount;
import static com.hedera.services.bdd.spec.utilops.EmbeddedVerbs.viewContract;
import static com.hedera.services.bdd.spec.utilops.SidecarVerbs.GLOBAL_WATCHER;
Expand All @@ -44,6 +45,7 @@
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_HOOK_CREATION_SPEC;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.REJECTED_BY_ACCOUNT_ALLOWANCE_HOOK;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TRANSACTION_REQUIRES_ZERO_HOOKS;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TRANSFER_LIST_SIZE_LIMIT_EXCEEDED;
import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE;
import static org.junit.jupiter.api.Assertions.assertEquals;

Expand Down Expand Up @@ -1073,4 +1075,111 @@ final Stream<DynamicTest> cannotDeleteAccountWithHooks() {
cryptoUpdate(OWNER).removingHook(123L),
cryptoDelete(OWNER).payingWith(OWNER));
}

@HapiTest
final Stream<DynamicTest> transfersWorkWithCryptoTransferLimit() {
final var receiverPrefix = "receiver";
return hapiTest(
withOpContext((spec, log) -> {
for (int i = 0; i < 10; i++) {
allRunFor(
spec,
cryptoCreate(receiverPrefix + i)
.withHook(accountAllowanceHook(123L, TRUE_ALLOWANCE_HOOK.name()))
.withHook(accountAllowanceHook(124L, TRUE_PRE_POST_ALLOWANCE_HOOK.name()))
.balance(ONE_HBAR));
}
}),
cryptoCreate(OWNER)
.withHooks(
accountAllowanceHook(123L, TRUE_ALLOWANCE_HOOK.name()),
accountAllowanceHook(124L, TRUE_PRE_POST_ALLOWANCE_HOOK.name())),
cryptoTransfer(
TokenMovement.movingHbar(10).between(OWNER, "receiver0"),
TokenMovement.movingHbar(10).between(OWNER, "receiver1"),
TokenMovement.movingHbar(10).between(OWNER, "receiver2"),
TokenMovement.movingHbar(10).between(OWNER, "receiver3"),
TokenMovement.movingHbar(10).between(OWNER, "receiver4"),
TokenMovement.movingHbar(10).between(OWNER, "receiver5"),
TokenMovement.movingHbar(10).between(OWNER, "receiver6"),
TokenMovement.movingHbar(10).between(OWNER, "receiver7"),
TokenMovement.movingHbar(10).between(OWNER, "receiver8"),
TokenMovement.movingHbar(10).between(OWNER, "receiver9"))
.withPreHookFor(OWNER, 123L, 25_000L, "")
.withPreHookFor("receiver0", 123L, 25_000L, "")
.withPreHookFor("receiver1", 123L, 25_000L, "")
.withPreHookFor("receiver2", 123L, 25_000L, "")
.withPreHookFor("receiver3", 123L, 25_000L, "")
.withPreHookFor("receiver4", 123L, 25_000L, "")
.withPreHookFor("receiver5", 123L, 25_000L, "")
.withPreHookFor("receiver6", 123L, 25_000L, "")
.withPreHookFor("receiver7", 123L, 25_000L, "")
.withPreHookFor("receiver8", 123L, 25_000L, "")
.withPreHookFor("receiver9", 123L, 25_000L, "")
.hasKnownStatus(TRANSFER_LIST_SIZE_LIMIT_EXCEEDED),
cryptoTransfer(
TokenMovement.movingHbar(10).between(OWNER, "receiver0"),
TokenMovement.movingHbar(10).between(OWNER, "receiver1"),
TokenMovement.movingHbar(10).between(OWNER, "receiver2"),
TokenMovement.movingHbar(10).between(OWNER, "receiver3"),
TokenMovement.movingHbar(10).between(OWNER, "receiver4"),
TokenMovement.movingHbar(10).between(OWNER, "receiver5"),
TokenMovement.movingHbar(10).between(OWNER, "receiver6"),
TokenMovement.movingHbar(10).between(OWNER, "receiver7"),
TokenMovement.movingHbar(10).between(OWNER, "receiver8"))
.withPreHookFor(OWNER, 123L, 25_000L, "")
.withPreHookFor("receiver0", 123L, 25_000L, "")
.withPreHookFor("receiver1", 123L, 25_000L, "")
.withPreHookFor("receiver2", 123L, 25_000L, "")
.withPreHookFor("receiver3", 123L, 25_000L, "")
.withPreHookFor("receiver4", 123L, 25_000L, "")
.withPreHookFor("receiver5", 123L, 25_000L, "")
.withPreHookFor("receiver6", 123L, 25_000L, "")
.withPreHookFor("receiver7", 123L, 25_000L, "")
.withPreHookFor("receiver8", 123L, 25_000L, "")
.withPreHookFor("receiver9", 123L, 25_000L, "")
.via("transferTxn"),
getTxnRecord("transferTxn")
.andAllChildRecords()
.hasNonStakingChildRecordCount(10)
.logged(),
cryptoTransfer(
TokenMovement.movingHbar(10).between(OWNER, "receiver0"),
TokenMovement.movingHbar(10).between(OWNER, "receiver1"),
TokenMovement.movingHbar(10).between(OWNER, "receiver2"),
TokenMovement.movingHbar(10).between(OWNER, "receiver3"),
TokenMovement.movingHbar(10).between(OWNER, "receiver4"),
TokenMovement.movingHbar(10).between(OWNER, "receiver5"),
TokenMovement.movingHbar(10).between(OWNER, "receiver6"),
TokenMovement.movingHbar(10).between(OWNER, "receiver7"),
TokenMovement.movingHbar(10).between(OWNER, "receiver8"))
.withPrePostHookFor(OWNER, 124L, 25_000L, "")
.withPrePostHookFor("receiver0", 124L, 25_000L, "")
.withPrePostHookFor("receiver1", 124L, 25_000L, "")
.withPrePostHookFor("receiver2", 124L, 25_000L, "")
.withPrePostHookFor("receiver3", 124L, 25_000L, "")
.withPrePostHookFor("receiver4", 124L, 25_000L, "")
.withPrePostHookFor("receiver5", 124L, 25_000L, "")
.withPrePostHookFor("receiver6", 124L, 25_000L, "")
.withPrePostHookFor("receiver7", 124L, 25_000L, "")
.withPrePostHookFor("receiver8", 124L, 25_000L, "")
.withPrePostHookFor("receiver9", 124L, 25_000L, "")
.via("transferPrePostTxn"),
getTxnRecord("transferPrePostTxn")
.andAllChildRecords()
.hasNonStakingChildRecordCount(20)
.logged());
}

@HapiTest
final Stream<DynamicTest> transfersExceedingChildTxnLimitFails() {
return hapiTest(
cryptoCreate(OWNER).withHooks(accountAllowanceHook(123L, SELF_DESTRUCT_HOOK.name())),
cryptoDelete(OWNER)
.hasKnownStatus(TRANSACTION_REQUIRES_ZERO_HOOKS)
.payingWith(OWNER),
// after removing hook can delete successfully
cryptoUpdate(OWNER).removingHook(123L),
cryptoDelete(OWNER).payingWith(OWNER));
}
}
Loading
Loading