diff --git a/hedera-node/hedera-app/src/test/resources/bootstrap.properties b/hedera-node/hedera-app/src/test/resources/bootstrap.properties index 26a77f913097..678f2fe5a253 100644 --- a/hedera-node/hedera-app/src/test/resources/bootstrap.properties +++ b/hedera-node/hedera-app/src/test/resources/bootstrap.properties @@ -61,7 +61,7 @@ balances.compressOnCreation=true cache.records.ttl=180 contracts.allowAutoAssociations=false contracts.allowSystemUseOfHapiSigs=TokenAssociateToAccount,TokenDissociateFromAccount,TokenFreezeAccount,TokenUnfreezeAccount,TokenGrantKycToAccount,TokenRevokeKycFromAccount,TokenAccountWipe,TokenBurn,TokenDelete,TokenMint,TokenUnpause,TokenPause,TokenCreate,TokenUpdate,ContractCall,CryptoTransfer -contracts.maxNumWithHapiSigsAccess=10_000_000 +contracts.maxNumWithHapiSigsAccess=0 contracts.withSpecialHapiSigsAccess= contracts.allowCreate2=true contracts.chainId=295 diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ContractsConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ContractsConfig.java index 1ab6d7bd1f13..77d51bf80192 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ContractsConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ContractsConfig.java @@ -35,7 +35,7 @@ public record ContractsConfig( @ConfigProperty(defaultValue = "false") boolean allowAutoAssociations, // @ConfigProperty(defaultValue = // "TokenAssociateToAccount,TokenDissociateFromAccount,TokenFreezeAccount,TokenUnfreezeAccount,TokenGrantKycToAccount,TokenRevokeKycFromAccount,TokenAccountWipe,TokenBurn,TokenDelete,TokenMint,TokenUnpause,TokenPause,TokenCreate,TokenUpdate,ContractCall,CryptoTransfer") Set allowSystemUseOfHapiSigs, - @ConfigProperty(defaultValue = "10000000") long maxNumWithHapiSigsAccess, + @ConfigProperty(defaultValue = "0") long maxNumWithHapiSigsAccess, // @ConfigProperty(defaultValue = "") Set
withSpecialHapiSigsAccess, @ConfigProperty(defaultValue = "false") boolean enforceCreationThrottle, @ConfigProperty(defaultValue = "15000000") long maxGasPerSec, diff --git a/hedera-node/hedera-mono-service/src/main/resources/bootstrap.properties b/hedera-node/hedera-mono-service/src/main/resources/bootstrap.properties index 616b837ceadc..c6eaaf471ed8 100644 --- a/hedera-node/hedera-mono-service/src/main/resources/bootstrap.properties +++ b/hedera-node/hedera-mono-service/src/main/resources/bootstrap.properties @@ -61,7 +61,7 @@ balances.compressOnCreation=true cache.records.ttl=180 contracts.allowAutoAssociations=false contracts.allowSystemUseOfHapiSigs=TokenAssociateToAccount,TokenDissociateFromAccount,TokenFreezeAccount,TokenUnfreezeAccount,TokenGrantKycToAccount,TokenRevokeKycFromAccount,TokenAccountWipe,TokenBurn,TokenDelete,TokenMint,TokenUnpause,TokenPause,TokenCreate,TokenUpdate,ContractCall,CryptoTransfer -contracts.maxNumWithHapiSigsAccess=10_000_000 +contracts.maxNumWithHapiSigsAccess=0 contracts.withSpecialHapiSigsAccess= contracts.allowCreate2=true contracts.chainId=295 diff --git a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/context/properties/BootstrapPropertiesTest.java b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/context/properties/BootstrapPropertiesTest.java index 59f2a43df5f9..0b0780b4b314 100644 --- a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/context/properties/BootstrapPropertiesTest.java +++ b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/context/properties/BootstrapPropertiesTest.java @@ -344,7 +344,7 @@ class BootstrapPropertiesTest { entry(AUTO_RENEW_GRANT_FREE_RENEWALS, false), entry(CONTRACTS_ALLOW_CREATE2, true), entry(CONTRACTS_ALLOW_AUTO_ASSOCIATIONS, false), - entry(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, 10_000_000L), + entry(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, 0L), entry(CONTRACTS_WITH_SPECIAL_HAPI_SIGS_ACCESS, Set.
of()), entry( CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, diff --git a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/ERC20PrecompilesTest.java b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/ERC20PrecompilesTest.java index 15157c76db39..3d08b72401b7 100644 --- a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/ERC20PrecompilesTest.java +++ b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/ERC20PrecompilesTest.java @@ -165,6 +165,7 @@ import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import java.util.function.Consumer; import org.apache.commons.lang3.tuple.Pair; import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.datatypes.Address; @@ -177,6 +178,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -326,6 +329,22 @@ class ERC20PrecompilesTest { private static final int CENTS_RATE = 12; private static final int HBAR_RATE = 1; + private enum WithHapiBlockLimit { + LOW, + HIGH + }; + + private static final Map> setHapiBlockLimitGivens = Map.of( + WithHapiBlockLimit.LOW, + props -> { + given(props.maxNumWithHapiSigsAccess()).willReturn(0L); + }, + WithHapiBlockLimit.HIGH, + props -> { + given(props.maxNumWithHapiSigsAccess()).willReturn(Long.MAX_VALUE); + given(props.systemContractsWithTopLevelSigsAccess()).willReturn(Set.of(CryptoTransfer)); + }); + private HTSPrecompiledContract subject; private MockedStatic entityIdUtils; private MockedStatic ercTransferPrecompile; @@ -1280,15 +1299,14 @@ void transferFrom() throws InvalidProtocolBufferException { verify(worldUpdater).manageInProgressRecord(recordsHistorian, mockRecordBuilder, mockSynthBodyBuilder); } - @Test - void transferFromHapiFungible() throws InvalidProtocolBufferException { + @ParameterizedTest + @EnumSource + void transferFromHapiFungible(final WithHapiBlockLimit limit) throws InvalidProtocolBufferException { final var pretendArguments = Bytes.of(Integers.toBytes(ABI_ID_TRANSFER_FROM)); givenMinimalFrameContext(Bytes.EMPTY); givenLedgers(); givenPricingUtilsContext(); - given(dynamicProperties.maxNumWithHapiSigsAccess()).willReturn(Long.MAX_VALUE); - given(dynamicProperties.systemContractsWithTopLevelSigsAccess()).willReturn(Set.of(CryptoTransfer)); - + setHapiBlockLimitGivens.get(limit).accept(dynamicProperties); given(frame.getContractAddress()).willReturn(contractAddr); given(syntheticTxnFactory.createCryptoTransfer(Collections.singletonList(TOKEN_TRANSFER_FROM_WRAPPER))) .willReturn(mockSynthBodyBuilder); @@ -1354,11 +1372,11 @@ void transferFromHapiFungible() throws InvalidProtocolBufferException { .build()); } - @Test - void transferFromNFTHapi() throws InvalidProtocolBufferException { + @ParameterizedTest + @EnumSource + void transferFromNFTHapi(final WithHapiBlockLimit limit) throws InvalidProtocolBufferException { final var pretendArguments = Bytes.of(Integers.toBytes(ABI_ID_TRANSFER_FROM_NFT)); - given(dynamicProperties.maxNumWithHapiSigsAccess()).willReturn(Long.MAX_VALUE); - given(dynamicProperties.systemContractsWithTopLevelSigsAccess()).willReturn(Set.of(CryptoTransfer)); + setHapiBlockLimitGivens.get(limit).accept(dynamicProperties); givenMinimalFrameContext(Bytes.EMPTY); givenLedgers(); givenPricingUtilsContext(); @@ -1485,12 +1503,13 @@ void transferFails() throws InvalidProtocolBufferException { assertEquals(invalidFullPrefix, result); } - @Test - void onlyFallsBackToApprovalWithoutTopLevelSigs() throws InvalidProtocolBufferException { + @ParameterizedTest + @EnumSource + void onlyFallsBackToApprovalWithoutTopLevelSigs(final WithHapiBlockLimit limit) + throws InvalidProtocolBufferException { final Bytes nestedPretendArguments = Bytes.of(Integers.toBytes(ABI_ID_ERC_TRANSFER)); final Bytes pretendArguments = givenMinimalFrameContext(nestedPretendArguments); - given(dynamicProperties.maxNumWithHapiSigsAccess()).willReturn(Long.MAX_VALUE); - given(dynamicProperties.systemContractsWithTopLevelSigsAccess()).willReturn(Set.of(CryptoTransfer)); + setHapiBlockLimitGivens.get(limit).accept(dynamicProperties); givenLedgers(); givenPricingUtilsContext(); diff --git a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/ERC721PrecompilesTest.java b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/ERC721PrecompilesTest.java index 680e8353d7ff..54e88628039e 100644 --- a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/ERC721PrecompilesTest.java +++ b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/ERC721PrecompilesTest.java @@ -160,9 +160,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.function.Consumer; import org.apache.commons.lang3.tuple.Pair; import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.datatypes.Address; @@ -174,6 +176,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -316,6 +320,22 @@ class ERC721PrecompilesTest { private static final int CENTS_RATE = 12; private static final int HBAR_RATE = 1; + private enum WithHapiBlockLimit { + LOW, + HIGH + }; + + private static final Map> setHapiBlockLimitGivens = Map.of( + WithHapiBlockLimit.LOW, + props -> { + given(props.maxNumWithHapiSigsAccess()).willReturn(0L); + }, + WithHapiBlockLimit.HIGH, + props -> { + given(props.maxNumWithHapiSigsAccess()).willReturn(Long.MAX_VALUE); + given(props.systemContractsWithTopLevelSigsAccess()).willReturn(Set.of(CryptoTransfer)); + }); + private HTSPrecompiledContract subject; private MockedStatic entityIdUtils; private MockedStatic isApprovedForAllPrecompile; @@ -1280,12 +1300,12 @@ void ownerOfRevertsWithMissingNft() { assertEquals(missingNftResult, result); } - @Test - void transferFrom() throws InvalidProtocolBufferException { + @ParameterizedTest + @EnumSource + void transferFrom(final WithHapiBlockLimit limit) throws InvalidProtocolBufferException { final Bytes nestedPretendArguments = Bytes.of(Integers.toBytes(ABI_ID_ERC_TRANSFER_FROM)); final Bytes pretendArguments = givenMinimalFrameContext(nestedPretendArguments); - given(dynamicProperties.maxNumWithHapiSigsAccess()).willReturn(Long.MAX_VALUE); - given(dynamicProperties.systemContractsWithTopLevelSigsAccess()).willReturn(Set.of(CryptoTransfer)); + setHapiBlockLimitGivens.get(limit).accept(dynamicProperties); givenLedgers(); givenPricingUtilsContext(); @@ -1362,14 +1382,14 @@ void transferFrom() throws InvalidProtocolBufferException { verify(frame).addLog(log); } - @Test - void transferFromFailsForInvalidSig() throws InvalidProtocolBufferException { + @ParameterizedTest + @EnumSource + void transferFromFailsForInvalidSig(final WithHapiBlockLimit limit) throws InvalidProtocolBufferException { final Bytes nestedPretendArguments = Bytes.of(Integers.toBytes(ABI_ID_ERC_TRANSFER_FROM)); final Bytes pretendArguments = givenMinimalFrameContext(nestedPretendArguments); givenLedgers(); - given(dynamicProperties.maxNumWithHapiSigsAccess()).willReturn(Long.MAX_VALUE); + setHapiBlockLimitGivens.get(limit).accept(dynamicProperties); givenPricingUtilsContext(); - given(dynamicProperties.systemContractsWithTopLevelSigsAccess()).willReturn(Set.of(CryptoTransfer)); given(frame.getContractAddress()).willReturn(contractAddr); given(syntheticTxnFactory.createCryptoTransfer(Collections.singletonList(nftTransferList))) diff --git a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/TransferPrecompilesTest.java b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/TransferPrecompilesTest.java index 2eecf88563be..1c7e6176b709 100644 --- a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/TransferPrecompilesTest.java +++ b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/TransferPrecompilesTest.java @@ -170,8 +170,10 @@ import java.util.Deque; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.UnaryOperator; import org.apache.commons.lang3.tuple.Pair; @@ -187,6 +189,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -349,6 +353,22 @@ class TransferPrecompilesTest { private static final Bytes TRANSFER_NFTS_INPUT = Bytes.fromHexString( "0x2c4ba191000000000000000000000000000000000000000000000000000000000000047a000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000047700000000000000000000000000000000000000000000000000000000000004770000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000047c000000000000000000000000000000000000000000000010000000000000047c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000000ea"); + private enum WithHapiBlockLimit { + LOW, + HIGH + }; + + private static final Map> setHapiBlockLimitGivens = Map.of( + WithHapiBlockLimit.LOW, + props -> { + given(props.maxNumWithHapiSigsAccess()).willReturn(0L); + }, + WithHapiBlockLimit.HIGH, + props -> { + given(props.maxNumWithHapiSigsAccess()).willReturn(Long.MAX_VALUE); + given(props.systemContractsWithTopLevelSigsAccess()).willReturn(Set.of(CryptoTransfer)); + }); + private HTSPrecompiledContract subject; private MockedStatic transferPrecompile; final Predicate accoundIdExists = acc -> true; @@ -1720,15 +1740,15 @@ void hbarNFTTransferHappyPathWorks() throws InvalidProtocolBufferException { verify(worldUpdater).manageInProgressRecord(recordsHistorian, mockRecordBuilder, mockSynthBodyBuilder); } - @Test - void transferFailsAndCatchesProperly() throws InvalidProtocolBufferException { + @ParameterizedTest + @EnumSource + void transferFailsAndCatchesProperly(final WithHapiBlockLimit limit) throws InvalidProtocolBufferException { final Bytes pretendArguments = Bytes.of(Integers.toBytes(ABI_ID_TRANSFER_TOKEN)); givenMinimalFrameContext(); givenLedgers(); givenPricingUtilsContext(); - given(dynamicProperties.maxNumWithHapiSigsAccess()).willReturn(Long.MAX_VALUE); - given(dynamicProperties.systemContractsWithTopLevelSigsAccess()).willReturn(Set.of(CryptoTransfer)); + setHapiBlockLimitGivens.get(limit).accept(dynamicProperties); given(infrastructureFactory.newSideEffects()).willReturn(sideEffects); given(infrastructureFactory.newImpliedTransfersMarshal(any())).willReturn(impliedTransfersMarshal); given(worldUpdater.permissivelyUnaliased(any())) diff --git a/hedera-node/hedera-mono-service/src/test/resources/bootstrap.properties b/hedera-node/hedera-mono-service/src/test/resources/bootstrap.properties index bab000848734..2e9a5427934d 100644 --- a/hedera-node/hedera-mono-service/src/test/resources/bootstrap.properties +++ b/hedera-node/hedera-mono-service/src/test/resources/bootstrap.properties @@ -61,7 +61,7 @@ balances.compressOnCreation=true cache.records.ttl=180 contracts.allowAutoAssociations=false contracts.allowSystemUseOfHapiSigs=TokenAssociateToAccount,TokenDissociateFromAccount,TokenFreezeAccount,TokenUnfreezeAccount,TokenGrantKycToAccount,TokenRevokeKycFromAccount,TokenAccountWipe,TokenBurn,TokenDelete,TokenMint,TokenUnpause,TokenPause,TokenCreate,TokenUpdate,ContractCall,CryptoTransfer -contracts.maxNumWithHapiSigsAccess=10_000_000 +contracts.maxNumWithHapiSigsAccess=0 contracts.withSpecialHapiSigsAccess= contracts.allowCreate2=true contracts.chainId=295 diff --git a/hedera-node/hedera-mono-service/src/test/resources/bootstrap/standard.properties b/hedera-node/hedera-mono-service/src/test/resources/bootstrap/standard.properties index 000d3a06edeb..0b673a91ac8a 100644 --- a/hedera-node/hedera-mono-service/src/test/resources/bootstrap/standard.properties +++ b/hedera-node/hedera-mono-service/src/test/resources/bootstrap/standard.properties @@ -61,7 +61,7 @@ balances.compressOnCreation=true cache.records.ttl=180 contracts.allowAutoAssociations=false contracts.allowSystemUseOfHapiSigs=TokenAssociateToAccount,TokenDissociateFromAccount,TokenFreezeAccount,TokenUnfreezeAccount,TokenGrantKycToAccount,TokenRevokeKycFromAccount,TokenAccountWipe,TokenBurn,TokenDelete,TokenMint,TokenUnpause,TokenPause,TokenCreate,TokenUpdate,ContractCall,CryptoTransfer -contracts.maxNumWithHapiSigsAccess=10_000_000 +contracts.maxNumWithHapiSigsAccess=0 contracts.withSpecialHapiSigsAccess= contracts.allowCreate2=true contracts.chainId=295 diff --git a/hedera-node/test-clients/src/eet/java/EndToEndTests.java b/hedera-node/test-clients/src/eet/java/EndToEndTests.java index c6402e3b9ebe..4c0b32f5e09f 100644 --- a/hedera-node/test-clients/src/eet/java/EndToEndTests.java +++ b/hedera-node/test-clients/src/eet/java/EndToEndTests.java @@ -135,7 +135,6 @@ Collection contractPrecompile2() { new DynamicContainer[] { // extractSpecsFromSuite(CryptoTransferHTSSuite::new), // extractSpecsFromSuite(DelegatePrecompileSuite::new), - // extractSpecsFromSuite(DissociatePrecompileSuite::new), // extractSpecsFromSuite(DynamicGasCostSuite::new), // extractSpecsFromSuite(MixedHTSPrecompileTestsSuite::new) }); diff --git a/hedera-node/test-clients/src/itest/java/AllIntegrationTests.java b/hedera-node/test-clients/src/itest/java/AllIntegrationTests.java index 3e4fa2d010fd..37bd3258d6d8 100644 --- a/hedera-node/test-clients/src/itest/java/AllIntegrationTests.java +++ b/hedera-node/test-clients/src/itest/java/AllIntegrationTests.java @@ -48,11 +48,20 @@ class AllIntegrationTests extends IntegrationTestBase { private static final String TEST_CONTAINER_NODE0_STREAMS = "build/network/itest/records/node_0"; + @Tag("integration") + @Order(0) + @TestFactory + Collection globalPrerequisiteSpecsBySuite() { + return Arrays.stream(SequentialSuites.globalPrerequisiteSuites()) + .map(this::extractSpecsFromSuite) + .toList(); + } + @Tag("integration") @Order(1) @TestFactory Collection sequentialSpecsBySuite() { - return Arrays.stream(SequentialSuites.all()) + return Arrays.stream(SequentialSuites.sequentialSuites()) .map(this::extractSpecsFromSuite) .toList(); } diff --git a/hedera-node/test-clients/src/itest/java/ConcurrentSuites.java b/hedera-node/test-clients/src/itest/java/ConcurrentSuites.java index 7851292b4f84..c82ba70a313a 100644 --- a/hedera-node/test-clients/src/itest/java/ConcurrentSuites.java +++ b/hedera-node/test-clients/src/itest/java/ConcurrentSuites.java @@ -60,7 +60,6 @@ import com.hedera.services.bdd.suites.contract.precompile.FreezeUnfreezeTokenPrecompileSuite; import com.hedera.services.bdd.suites.contract.precompile.GrantRevokeKycSuite; import com.hedera.services.bdd.suites.contract.precompile.LazyCreateThroughPrecompileSuite; -import com.hedera.services.bdd.suites.contract.precompile.MixedHTSPrecompileTestsSuite; import com.hedera.services.bdd.suites.contract.precompile.PauseUnpauseTokenAccountPrecompileSuite; import com.hedera.services.bdd.suites.contract.precompile.PrngPrecompileSuite; import com.hedera.services.bdd.suites.contract.precompile.SigningReqsSuite; @@ -179,7 +178,6 @@ static Supplier[] all() { FreezeUnfreezeTokenPrecompileSuite::new, GrantRevokeKycSuite::new, LazyCreateThroughPrecompileSuite::new, - MixedHTSPrecompileTestsSuite::new, PauseUnpauseTokenAccountPrecompileSuite::new, PrngPrecompileSuite::new, TokenAndTypeCheckSuite::new, @@ -234,7 +232,6 @@ static Supplier[] ethereumSuites() { DefaultTokenStatusSuite::new, DelegatePrecompileSuite::new, DeleteTokenPrecompileSuite::new, - DissociatePrecompileSuite::new, CreatePrecompileSuite::new, ERCPrecompileSuite::new, FreezeUnfreezeTokenPrecompileSuite::new, diff --git a/hedera-node/test-clients/src/itest/java/SequentialSuites.java b/hedera-node/test-clients/src/itest/java/SequentialSuites.java index 2f25cbcc309a..20590aa1c15c 100644 --- a/hedera-node/test-clients/src/itest/java/SequentialSuites.java +++ b/hedera-node/test-clients/src/itest/java/SequentialSuites.java @@ -21,21 +21,31 @@ import com.hedera.services.bdd.suites.leaky.FeatureFlagSuite; import com.hedera.services.bdd.suites.leaky.LeakyContractTestsSuite; import com.hedera.services.bdd.suites.leaky.LeakyCryptoTestsSuite; +import com.hedera.services.bdd.suites.leaky.LeakySecurityModelV1Suite; import com.hedera.services.bdd.suites.regression.TargetNetworkPrep; import com.hedera.services.bdd.suites.throttling.PrivilegedOpsSuite; import java.util.function.Supplier; +import org.apache.commons.lang3.ArrayUtils; public class SequentialSuites { - @SuppressWarnings("unchecked") static Supplier[] all() { + return ArrayUtils.addAll(globalPrerequisiteSuites(), sequentialSuites()); + } + + @SuppressWarnings("unchecked") + static Supplier[] globalPrerequisiteSuites() { + return (Supplier[]) new Supplier[] {TargetNetworkPrep::new, FeatureFlagSuite::new}; + } + + @SuppressWarnings("unchecked") + static Supplier[] sequentialSuites() { return (Supplier[]) new Supplier[] { - TargetNetworkPrep::new, - FeatureFlagSuite::new, SpecialAccountsAreExempted::new, PrivilegedOpsSuite::new, TraceabilitySuite::new, LeakyContractTestsSuite::new, LeakyCryptoTestsSuite::new, + LeakySecurityModelV1Suite::new, Create2OperationSuite::new, }; } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnUtils.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnUtils.java index 2b2d12c7e61e..9c6554cb7fea 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnUtils.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnUtils.java @@ -55,6 +55,7 @@ import com.hedera.services.bdd.spec.queries.contract.HapiGetContractInfo; import com.hedera.services.bdd.spec.queries.file.HapiGetFileInfo; import com.hedera.services.bdd.spec.transactions.contract.HapiContractCall; +import com.hedera.services.bdd.suites.contract.Utils; import com.hederahashgraph.api.proto.java.AccountAmount; import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.ContractID; @@ -612,7 +613,9 @@ public static String toReadableString(final Transaction grpcTransaction) throws } public static String bytecodePath(final String contractName) { - return String.format("src/main/resource/contract/contracts/%s/%s.bin", contractName, contractName); + // TODO: Quick fix for https://github.com/hashgraph/hedera-services/issues/6821, a better solution + // will be provided when the issue is resolved + return Utils.getResourcePath(contractName, ".bin"); } public static ByteString literalInitcodeFor(final String contract) { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/SuiteRunner.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/SuiteRunner.java index 9251cb6482d2..f6d722ba4b40 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/SuiteRunner.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/SuiteRunner.java @@ -64,8 +64,6 @@ import com.hedera.services.bdd.suites.contract.precompile.ContractMintHTSSuite; import com.hedera.services.bdd.suites.contract.precompile.CryptoTransferHTSSuite; import com.hedera.services.bdd.suites.contract.precompile.DelegatePrecompileSuite; -import com.hedera.services.bdd.suites.contract.precompile.DissociatePrecompileSuite; -import com.hedera.services.bdd.suites.contract.precompile.MixedHTSPrecompileTestsSuite; import com.hedera.services.bdd.suites.contract.records.LogsSuite; import com.hedera.services.bdd.suites.contract.records.RecordsSuite; import com.hedera.services.bdd.suites.crypto.AutoAccountCreationSuite; @@ -437,8 +435,6 @@ public class SuiteRunner { put("ContractMintHTSSuite", aof(ContractMintHTSSuite::new)); put("CryptoTransferHTSSuite", aof(CryptoTransferHTSSuite::new)); put("DelegatePrecompileSuite", aof(DelegatePrecompileSuite::new)); - put("DissociatePrecompileSuite", aof(DissociatePrecompileSuite::new)); - put("MixedHTSPrecompileTestsSuite", aof(MixedHTSPrecompileTestsSuite::new)); /* Functional tests - AUTORENEW */ put("AccountAutoRenewalSuite", aof(AccountAutoRenewalSuite::new)); /* Functional tests - MIXED (record emphasis) */ diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/Utils.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/Utils.java index 0b783710901a..57c24240e677 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/Utils.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/Utils.java @@ -20,25 +20,34 @@ import static com.hedera.services.bdd.spec.HapiPropertySource.asDotDelimitedLongArray; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.assertionsHold; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.contract.Utils.FunctionType.CONSTRUCTOR; +import static com.hederahashgraph.api.proto.java.HederaFunctionality.ContractCall; +import static com.hederahashgraph.api.proto.java.SubType.DEFAULT; import static com.swirlds.common.utility.CommonUtils.hex; import static com.swirlds.common.utility.CommonUtils.unhex; import static java.lang.System.arraycopy; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.junit.jupiter.api.Assertions.assertEquals; +import com.esaulpaugh.headlong.abi.Address; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; import com.google.protobuf.ByteString; +import com.hedera.node.app.hapi.fees.pricing.AssetsLoader; import com.hedera.services.bdd.spec.HapiPropertySource; +import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.HapiSpecOperation; import com.hedera.services.bdd.spec.transactions.TxnUtils; +import com.hedera.services.bdd.spec.utilops.CustomSpecAssert; import com.hederahashgraph.api.proto.java.AccountAmount; import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.ContractID; +import com.hederahashgraph.api.proto.java.HederaFunctionality; import com.hederahashgraph.api.proto.java.Key; import com.hederahashgraph.api.proto.java.NftTransfer; +import com.hederahashgraph.api.proto.java.SubType; import com.hederahashgraph.api.proto.java.Timestamp; import com.hederahashgraph.api.proto.java.TokenID; import com.swirlds.common.utility.CommonUtils; @@ -47,12 +56,16 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.math.BigDecimal; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; +import java.util.Collections; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.IntStream; +import java.util.stream.LongStream; import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -60,12 +73,14 @@ import org.apache.tuweni.bytes.Bytes32; import org.bouncycastle.util.encoders.Hex; import org.hyperledger.besu.crypto.Hash; +import org.jetbrains.annotations.NotNull; import org.json.JSONArray; import org.json.JSONObject; import org.json.JSONTokener; public class Utils { public static final String RESOURCE_PATH = "src/main/resource/contract/contracts/%1$s/%1$s%2$s"; + public static final String UNIQUE_CLASSPATH_RESOURCE_TPL = "contract/contracts/%s/%s"; private static final Logger log = LogManager.getLogger(Utils.class); private static final String JSON_EXTENSION = ".json"; @@ -323,4 +338,74 @@ public static HapiSpecOperation captureChildCreate2MetaFor( public static Instant asInstant(final Timestamp timestamp) { return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()); } + + public static Address[] nCopiesOfSender(final int n, final Address mirrorAddr) { + return Collections.nCopies(n, mirrorAddr).toArray(Address[]::new); + } + + public static Address[] nNonMirrorAddressFrom(final int n, final long m) { + return LongStream.range(m, m + n).mapToObj(Utils::nonMirrorAddrWith).toArray(Address[]::new); + } + + public static Address headlongFromHexed(final String addr) { + return Address.wrap(Address.toChecksumAddress("0x" + addr)); + } + + public static Address mirrorAddrWith(final long num) { + return Address.wrap( + Address.toChecksumAddress(new BigInteger(1, HapiPropertySource.asSolidityAddress(0, 0, num)))); + } + + public static Address nonMirrorAddrWith(final long num) { + return nonMirrorAddrWith(666, num); + } + + public static Address nonMirrorAddrWith(final long seed, final long num) { + return Address.wrap(Address.toChecksumAddress( + new BigInteger(1, HapiPropertySource.asSolidityAddress((int) seed, seed, num)))); + } + + public static long expectedPrecompileGasFor( + final HapiSpec spec, final HederaFunctionality function, final SubType type) { + final var gasThousandthsOfTinycentPrice = spec.fees() + .getCurrentOpFeeData() + .get(ContractCall) + .get(DEFAULT) + .getServicedata() + .getGas(); + final var assetsLoader = new AssetsLoader(); + final BigDecimal hapiUsdPrice; + try { + hapiUsdPrice = assetsLoader.loadCanonicalPrices().get(function).get(type); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + final var precompileTinycentPrice = hapiUsdPrice + .multiply(BigDecimal.valueOf(1.2)) + .multiply(BigDecimal.valueOf(100 * 100_000_000L)) + .longValueExact(); + return (precompileTinycentPrice * 1000 / gasThousandthsOfTinycentPrice); + } + + @NotNull + public static String getNestedContractAddress(final String outerContract, final HapiSpec spec) { + return HapiPropertySource.asHexedSolidityAddress(spec.registry().getContractId(outerContract)); + } + + @NotNull + @SuppressWarnings("java:S5960") + public static CustomSpecAssert assertTxnRecordHasNoTraceabilityEnrichedContractFnResult( + final String nestedTransferTxn) { + return assertionsHold((spec, log) -> { + final var subOp = getTxnRecord(nestedTransferTxn); + allRunFor(spec, subOp); + + final var rcd = subOp.getResponseRecord(); + + final var contractCallResult = rcd.getContractCallResult(); + assertEquals(0L, contractCallResult.getGas(), "Result not expected to externalize gas"); + assertEquals(0L, contractCallResult.getAmount(), "Result not expected to externalize amount"); + assertEquals(ByteString.EMPTY, contractCallResult.getFunctionParameters()); + }); + } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCallSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCallSuite.java index 7ddbef2ffc39..a75a0786238c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCallSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCallSuite.java @@ -35,12 +35,9 @@ import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractRecords; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenNftInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnUtils.asId; import static com.hedera.services.bdd.spec.transactions.TxnUtils.literalInitcodeFor; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.burnToken; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCallWithFunctionAbi; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; @@ -49,15 +46,11 @@ 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.cryptoUpdate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.fileCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; -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.spec.transactions.TxnVerbs.tokenUpdate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromAccountToAlias; -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.assertionsHold; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.balanceSnapshot; @@ -66,15 +59,12 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.logIt; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyListNamed; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sleepFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.updateLargeFile; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.contract.Utils.FunctionType.FUNCTION; import static com.hedera.services.bdd.suites.contract.Utils.asAddress; import static com.hedera.services.bdd.suites.contract.Utils.asToken; import static com.hedera.services.bdd.suites.contract.Utils.captureChildCreate2MetaFor; -import static com.hedera.services.bdd.suites.contract.Utils.extractByteCode; import static com.hedera.services.bdd.suites.contract.Utils.getABIFor; import static com.hedera.services.bdd.suites.contract.Utils.getABIForContract; import static com.hedera.services.bdd.suites.utils.contracts.SimpleBytesResult.bigIntResult; @@ -91,7 +81,6 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.OBTAINER_SAME_CONTRACT_ID; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.OK; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; -import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; import static com.swirlds.common.utility.CommonUtils.unhex; import com.esaulpaugh.headlong.abi.ABIType; @@ -104,9 +93,7 @@ import com.hedera.services.bdd.spec.HapiPropertySource; import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.HapiSpecSetup; -import com.hedera.services.bdd.spec.keys.KeyShape; import com.hedera.services.bdd.spec.keys.SigControl; -import com.hedera.services.bdd.spec.transactions.contract.HapiContractCreate; import com.hedera.services.bdd.spec.transactions.token.TokenMovement; import com.hedera.services.bdd.spec.utilops.CustomSpecAssert; import com.hedera.services.bdd.suites.HapiSuite; @@ -115,7 +102,6 @@ import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.Timestamp; import com.hederahashgraph.api.proto.java.TokenID; -import com.hederahashgraph.api.proto.java.TokenSupplyType; import com.hederahashgraph.api.proto.java.TokenType; import com.swirlds.common.utility.CommonUtils; import java.math.BigInteger; @@ -173,7 +159,6 @@ public class ContractCallSuite extends HapiSuite { private static final String FAIL_INVALID_INITIAL_BALANCE = "failInvalidInitialBalance"; private static final String SUCCESS_WITH_ZERO_INITIAL_BALANCE = "successWithZeroInitialBalance"; private static final String KILL_ME = "killMe"; - private static final String CONTRACT_CALLER = "contractCaller"; private static final String RECEIVABLE_SIG_REQ_ACCOUNT = "receivableSigReqAccount"; private static final String RECEIVABLE_SIG_REQ_ACCOUNT_INFO = "receivableSigReqAccountInfo"; private static final String TRANSFER_TO_ADDRESS = "transferToAddress"; @@ -193,8 +178,6 @@ public class ContractCallSuite extends HapiSuite { private static final String RECEIVER_1_INFO = "receiver1Info"; private static final String RECEIVER_2_INFO = "receiver2Info"; private static final String RECEIVER_3_INFO = "receiver3Info"; - public static final String STATE_MUTABILITY_NONPAYABLE_TYPE_FUNCTION = - " \"stateMutability\": \"nonpayable\", \"type\": \"function\" }"; public static final String DELEGATE_CALL_SPECIFIC = "delegateCallSpecific"; public static void main(String... args) { @@ -220,7 +203,6 @@ public List getSpecsInSuite() { smartContractInlineAssemblyCheck(), ocToken(), erc721TokenUriAndHtsNftInfoTreatNonUtf8BytesDifferently(), - contractTransferToSigReqAccountWithKeySucceeds(), minChargeIsTXGasUsedByContractCall(), hscsEvm005TransferOfHBarsWorksBetweenContracts(), hscsEvm006ContractHBarTransferToAccount(), @@ -247,9 +229,6 @@ public List getSpecsInSuite() { whitelistingAliasedContract(), cannotUseMirrorAddressOfAliasedContractInPrecompileMethod(), exchangeRatePrecompileWorks(), - canMintAndTransferInSameContractOperation(), - workingHoursDemo(), - lpFarmSimulation(), nestedContractCannotOverSendValue(), depositMoreThanBalanceFailsGracefully(), lowLevelEcrecCallBehavior(), @@ -747,142 +726,6 @@ private HapiSpec bitcarbonTestStillPasses() { .gas(1_000_000))); } - private HapiSpec workingHoursDemo() { - final var gasToOffer = 4_000_000; - final var contract = "WorkingHours"; - final var ticketToken = "ticketToken"; - final var adminKey = "admin"; - final var treasury = "treasury"; - final var newSupplyKey = "newSupplyKey"; - - final var ticketTaking = "ticketTaking"; - final var ticketWorking = "ticketWorking"; - final var mint = "minting"; - final var burn = "burning"; - final var preMints = List.of(ByteString.copyFromUtf8("HELLO"), ByteString.copyFromUtf8("GOODBYE")); - - final AtomicLong ticketSerialNo = new AtomicLong(); - - return defaultHapiSpec("WorkingHoursDemo") - .given( - newKeyNamed(adminKey), - cryptoCreate(treasury), - // we need a new user, expiry to 1 Jan 2100 costs 11M gas for token - // associate - tokenCreate(ticketToken) - .treasury(treasury) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .initialSupply(0L) - .supplyType(TokenSupplyType.INFINITE) - .adminKey(adminKey) - .supplyKey(adminKey), - mintToken(ticketToken, preMints).via(mint), - burnToken(ticketToken, List.of(1L)).via(burn), - uploadInitCode(contract)) - .when( - withOpContext((spec, opLog) -> { - final var registry = spec.registry(); - final var tokenId = registry.getTokenID(ticketToken); - final var treasuryId = registry.getAccountID(treasury); - final var creation = contractCreate( - contract, - asHeadlongAddress(asAddress(tokenId)), - asHeadlongAddress(asAddress(treasuryId))) - .gas(gasToOffer); - allRunFor(spec, creation); - }), - newKeyNamed(newSupplyKey).shape(KeyShape.CONTRACT.signedWith(contract)), - tokenUpdate(ticketToken).supplyKey(newSupplyKey)) - .then( - /* Take a ticket */ - contractCall(contract, "takeTicket") - .alsoSigningWithFullPrefix(DEFAULT_CONTRACT_SENDER, treasury) - .gas(4_000_000) - .via(ticketTaking) - .exposingResultTo(result -> { - LOG.info("Explicit mint result is {}", result); - ticketSerialNo.set(((Long) result[0])); - }), - getTxnRecord(ticketTaking), - getAccountBalance(DEFAULT_CONTRACT_SENDER).logged().hasTokenBalance(ticketToken, 1L), - /* Our ticket number is 3 (b/c of the two pre-mints), so we must call - * work twice before the contract will actually accept our ticket. */ - sourcing(() -> contractCall(contract, "workTicket", ticketSerialNo.get()) - .gas(2_000_000) - .alsoSigningWithFullPrefix(DEFAULT_CONTRACT_SENDER)), - getAccountBalance(DEFAULT_CONTRACT_SENDER).hasTokenBalance(ticketToken, 1L), - sourcing(() -> contractCall(contract, "workTicket", ticketSerialNo.get()) - .gas(2_000_000) - .alsoSigningWithFullPrefix(DEFAULT_CONTRACT_SENDER) - .via(ticketWorking)), - getAccountBalance(DEFAULT_CONTRACT_SENDER).hasTokenBalance(ticketToken, 0L), - getTokenInfo(ticketToken).hasTotalSupply(1L), - /* Review the history */ - getTxnRecord(ticketTaking).andAllChildRecords().logged(), - getTxnRecord(ticketWorking).andAllChildRecords().logged()); - } - - private HapiSpec canMintAndTransferInSameContractOperation() { - final AtomicReference tokenMirrorAddr = new AtomicReference<>(); - final AtomicReference aCivilianMirrorAddr = new AtomicReference<>(); - final var nfToken = "nfToken"; - final var multiKey = "multiKey"; - final var aCivilian = "aCivilian"; - final var treasuryContract = "SomeERC721Scenarios"; - final var mintAndTransferTxn = "mintAndTransferTxn"; - final var mintAndTransferAndBurnTxn = "mintAndTransferAndBurnTxn"; - - return defaultHapiSpec("CanMintAndTransferInSameContractOperation") - .given( - newKeyNamed(multiKey), - cryptoCreate(aCivilian) - .exposingCreatedIdTo(id -> aCivilianMirrorAddr.set(asHexedSolidityAddress(id))), - uploadInitCode(treasuryContract), - contractCreate(treasuryContract).adminKey(multiKey), - tokenCreate(nfToken) - .supplyKey(multiKey) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(treasuryContract) - .initialSupply(0) - .exposingCreatedIdTo(idLit -> - tokenMirrorAddr.set(asHexedSolidityAddress(HapiPropertySource.asToken(idLit)))), - mintToken( - nfToken, - List.of( - // 1 - ByteString.copyFromUtf8("A penny for"), - // 2 - ByteString.copyFromUtf8("the Old Guy"))), - tokenAssociate(aCivilian, nfToken), - cryptoTransfer(movingUnique(nfToken, 2L).between(treasuryContract, aCivilian))) - .when(sourcing(() -> contractCall( - treasuryContract, - "nonSequiturMintAndTransfer", - asHeadlongAddress(tokenMirrorAddr.get()), - asHeadlongAddress(aCivilianMirrorAddr.get())) - .via(mintAndTransferTxn) - .gas(4_000_000) - .alsoSigningWithFullPrefix(multiKey))) - .then( - getTokenInfo(nfToken).hasTotalSupply(4L), - getTokenNftInfo(nfToken, 3L) - .hasSerialNum(3L) - .hasAccountID(aCivilian) - .hasMetadata(ByteString.copyFrom(new byte[] {(byte) 0xee})), - getTokenNftInfo(nfToken, 4L) - .hasSerialNum(4L) - .hasAccountID(aCivilian) - .hasMetadata(ByteString.copyFrom(new byte[] {(byte) 0xff})), - sourcing(() -> contractCall( - treasuryContract, - "nonSequiturMintAndTransferAndBurn", - asHeadlongAddress(tokenMirrorAddr.get()), - asHeadlongAddress(aCivilianMirrorAddr.get())) - .via(mintAndTransferAndBurnTxn) - .gas(4_000_000) - .alsoSigningWithFullPrefix(multiKey, aCivilian))); - } - private HapiSpec exchangeRatePrecompileWorks() { final var valueToTinycentCall = "recoverUsd"; final var rateAware = "ExchangeRatePrecompile"; @@ -1626,50 +1469,6 @@ HapiSpec payTestSelfDestructCall() { getAccountBalance(RECEIVER).hasTinyBars(2_000L)); } - private HapiSpec contractTransferToSigReqAccountWithKeySucceeds() { - return defaultHapiSpec("ContractTransferToSigReqAccountWithKeySucceeds") - .given( - cryptoCreate(CONTRACT_CALLER).balance(1_000_000_000_000L), - cryptoCreate(RECEIVABLE_SIG_REQ_ACCOUNT) - .balance(1_000_000_000_000L) - .receiverSigRequired(true), - getAccountInfo(CONTRACT_CALLER).savingSnapshot("contractCallerInfo"), - getAccountInfo(RECEIVABLE_SIG_REQ_ACCOUNT).savingSnapshot(RECEIVABLE_SIG_REQ_ACCOUNT_INFO), - uploadInitCode(TRANSFERRING_CONTRACT)) - .when(contractCreate(TRANSFERRING_CONTRACT).gas(300_000L).balance(5000L)) - .then(withOpContext((spec, opLog) -> { - final var accountAddress = spec.registry() - .getAccountInfo(RECEIVABLE_SIG_REQ_ACCOUNT_INFO) - .getContractAccountID(); - final var receivableAccountKey = spec.registry() - .getAccountInfo(RECEIVABLE_SIG_REQ_ACCOUNT_INFO) - .getKey(); - final var contractCallerKey = - spec.registry().getAccountInfo("contractCallerInfo").getKey(); - spec.registry().saveKey("receivableKey", receivableAccountKey); - spec.registry().saveKey("contractCallerKey", contractCallerKey); - /* if any of the keys are missing, INVALID_SIGNATURE is returned */ - final var call = contractCall( - TRANSFERRING_CONTRACT, - TRANSFER_TO_ADDRESS, - asHeadlongAddress(accountAddress), - BigInteger.ONE) - .payingWith(CONTRACT_CALLER) - .gas(300_000) - .alsoSigningWithFullPrefix("receivableKey"); - /* calling with the receivableSigReqAccount should pass without adding keys */ - final var callWithReceivable = contractCall( - TRANSFERRING_CONTRACT, - TRANSFER_TO_ADDRESS, - asHeadlongAddress(accountAddress), - BigInteger.ONE) - .payingWith(RECEIVABLE_SIG_REQ_ACCOUNT) - .gas(300_000) - .hasKnownStatus(SUCCESS); - allRunFor(spec, call, callWithReceivable); - })); - } - private HapiSpec contractTransferToSigReqAccountWithoutKeyFails() { return defaultHapiSpec("ContractTransferToSigReqAccountWithoutKeyFails") .given( @@ -2274,131 +2073,6 @@ private HapiSpec transferZeroHbars() { getAccountBalance(RECEIVER).hasTinyBars(10_000L)); } - private HapiSpec lpFarmSimulation() { - final var adminKey = "adminKey"; - final var gasToOffer = 4_000_000; - final var farmInitcodeLoc = "src/main/resource/contract/bytecodes/farmInitcode.bin"; - final var consAbi = "{ \"inputs\": [ { \"internalType\": \"address\", \"name\": \"_devaddr\", \"type\":" - + " \"address\" }, { \"internalType\": \"address\", \"name\": \"_rentPayer\"," - + " \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\":" - + " \"_saucePerSecond\", \"type\": \"uint256\" }, { \"internalType\":" - + " \"uint256\", \"name\": \"_hbarPerSecond\", \"type\": \"uint256\" }, {" - + " \"internalType\": \"uint256\", \"name\": \"_maxSauceSupply\", \"type\":" - + " \"uint256\" }, { \"internalType\": \"uint256\", \"name\":" - + " \"_depositFeeTinyCents\", \"type\": \"uint256\" } ], \"stateMutability\":" - + " \"nonpayable\", \"type\": \"constructor\" }"; - final var addPoolAbi = "{ \"inputs\": [ { \"internalType\": \"uint256\", \"name\": \"_allocPoint\"," - + " \"type\": \"uint256\" }, { \"internalType\": \"address\", \"name\":" - + " \"_lpToken\", \"type\": \"address\" } ], \"name\": \"add\"," - + " \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\":" - + " \"function\" }"; - final var depositAbi = "{ \"inputs\": [ { \"internalType\": \"uint256\", \"name\": \"_pid\", \"type\":" - + " \"uint256\" }, { \"internalType\": \"uint256\", \"name\": \"_amount\"," - + " \"type\": \"uint256\" } ], \"name\": \"deposit\", \"outputs\": []," - + " \"stateMutability\": \"payable\", \"type\": \"function\" }"; - final var withdrawAbi = "{ \"inputs\": [ { \"internalType\": \"uint256\", \"name\": \"_pid\", \"type\":" - + " \"uint256\" }, { \"internalType\": \"uint256\", \"name\": \"_amount\"," - + " \"type\": \"uint256\" } ], \"name\": \"withdraw\", \"outputs\": []," - + STATE_MUTABILITY_NONPAYABLE_TYPE_FUNCTION; - final var setSauceAbi = "{ \"inputs\": [ { \"internalType\": \"address\", \"name\": \"_sauce\", \"type\":" - + " \"address\" } ], \"name\": \"setSauceAddress\", \"outputs\": []," - + STATE_MUTABILITY_NONPAYABLE_TYPE_FUNCTION; - final var transferAbi = "{ \"inputs\": [ { \"internalType\": \"address\", \"name\": \"newOwner\", \"type\":" - + " \"address\" } ], \"name\": \"transferOwnership\", \"outputs\": []," - + STATE_MUTABILITY_NONPAYABLE_TYPE_FUNCTION; - final var initcode = "farmInitcode"; - final var farm = "farm"; - final var dev = "dev"; - final var lp = "lp"; - final var sauce = "sauce"; - final var rentPayer = "rentPayer"; - final AtomicReference devAddr = new AtomicReference<>(); - final AtomicReference ownerAddr = new AtomicReference<>(); - final AtomicReference sauceAddr = new AtomicReference<>(); - final AtomicReference lpTokenAddr = new AtomicReference<>(); - final AtomicReference rentPayerAddr = new AtomicReference<>(); - - return defaultHapiSpec("FarmSimulation") - .given( - newKeyNamed(adminKey), - fileCreate(initcode), - cryptoCreate(OWNER) - .balance(ONE_MILLION_HBARS) - .exposingCreatedIdTo(id -> ownerAddr.set(asHexedSolidityAddress(id))), - cryptoCreate(dev) - .balance(ONE_MILLION_HBARS) - .exposingCreatedIdTo(id -> devAddr.set(asHexedSolidityAddress(id))), - cryptoCreate(rentPayer) - .balance(ONE_MILLION_HBARS) - .exposingCreatedIdTo(id -> rentPayerAddr.set(asHexedSolidityAddress(id))), - updateLargeFile(GENESIS, initcode, extractByteCode(farmInitcodeLoc)), - sourcing(() -> new HapiContractCreate( - farm, - consAbi, - asHeadlongAddress(devAddr.get()), - asHeadlongAddress(rentPayerAddr.get()), - BigInteger.valueOf(4804540L), - BigInteger.valueOf(10000L), - BigInteger.valueOf(1000000000000000L), - BigInteger.valueOf(2500000000L)) - .bytecode(initcode)), - tokenCreate(sauce) - .supplyType(TokenSupplyType.FINITE) - .initialSupply(300_000_000) - .maxSupply(1_000_000_000) - .treasury(farm) - .adminKey(adminKey) - .supplyKey(adminKey) - .exposingCreatedIdTo(idLit -> - sauceAddr.set(asHexedSolidityAddress(HapiPropertySource.asToken(idLit)))), - tokenCreate(lp) - .treasury(dev) - .initialSupply(1_000_000_000) - .exposingCreatedIdTo(idLit -> - lpTokenAddr.set(asHexedSolidityAddress(HapiPropertySource.asToken(idLit)))), - tokenAssociate(dev, sauce), - sourcing( - () -> contractCallWithFunctionAbi(farm, setSauceAbi, asHeadlongAddress(sauceAddr.get())) - .gas(gasToOffer) - .refusingEthConversion()), - sourcing( - () -> contractCallWithFunctionAbi(farm, transferAbi, asHeadlongAddress(ownerAddr.get())) - .gas(gasToOffer) - .refusingEthConversion())) - .when( - sourcing(() -> contractCallWithFunctionAbi( - farm, - addPoolAbi, - BigInteger.valueOf(2392L), - asHeadlongAddress(lpTokenAddr.get())) - .via("add") - .payingWith(OWNER) - .gas(gasToOffer) - .refusingEthConversion()), - newKeyNamed("contractControl").shape(KeyShape.CONTRACT.signedWith(farm)), - tokenUpdate(sauce).supplyKey("contractControl"), - sourcing(() -> contractCallWithFunctionAbi( - farm, depositAbi, BigInteger.ZERO, BigInteger.valueOf(100_000)) - .sending(ONE_HUNDRED_HBARS) - .payingWith(dev) - .gas(gasToOffer) - .refusingEthConversion()), - sleepFor(1000), - sourcing(() -> contractCallWithFunctionAbi( - farm, depositAbi, BigInteger.ZERO, BigInteger.valueOf(100_000)) - .sending(ONE_HUNDRED_HBARS) - .payingWith(dev) - .gas(gasToOffer) - .via("second") - .refusingEthConversion()), - getTxnRecord("second").andAllChildRecords().logged()) - .then(sourcing(() -> contractCallWithFunctionAbi( - farm, withdrawAbi, BigInteger.ZERO, BigInteger.valueOf(200_000)) - .payingWith(dev) - .gas(gasToOffer) - .refusingEthConversion())); - } - private HapiSpec consTimeManagementWorksWithRevertedInternalCreations() { final var contract = "ConsTimeRepro"; final var failingCall = "FailingCall"; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCallV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCallV1SecurityModelSuite.java new file mode 100644 index 000000000000..a2217c59f1a9 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCallV1SecurityModelSuite.java @@ -0,0 +1,435 @@ +/* + * Copyright (C) 2020-2023 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.hapi; + +import static com.hedera.services.bdd.spec.HapiPropertySource.asHexedSolidityAddress; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenNftInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.burnToken; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCallWithFunctionAbi; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +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.fileCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; +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.spec.transactions.TxnVerbs.tokenUpdate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; +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.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sleepFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.updateLargeFile; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.extractByteCode; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; + +import com.google.protobuf.ByteString; +import com.hedera.services.bdd.spec.HapiPropertySource; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.keys.KeyShape; +import com.hedera.services.bdd.spec.transactions.contract.HapiContractCreate; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.TokenSupplyType; +import com.hederahashgraph.api.proto.java.TokenType; +import java.math.BigInteger; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class ContractCallV1SecurityModelSuite extends HapiSuite { + + private static final Logger LOG = LogManager.getLogger(ContractCallV1SecurityModelSuite.class); + + public static final String PAY_RECEIVABLE_CONTRACT = "PayReceivable"; + public static final String TRANSFERRING_CONTRACT = "Transferring"; + private static final String OWNER = "owner"; + private static final String CONTRACT_CALLER = "contractCaller"; + private static final String RECEIVABLE_SIG_REQ_ACCOUNT = "receivableSigReqAccount"; + private static final String RECEIVABLE_SIG_REQ_ACCOUNT_INFO = "receivableSigReqAccountInfo"; + private static final String TRANSFER_TO_ADDRESS = "transferToAddress"; + public static final String STATE_MUTABILITY_NONPAYABLE_TYPE_FUNCTION = + " \"stateMutability\": \"nonpayable\", \"type\": \"function\" }"; + + public static void main(String... args) { + new ContractCallV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + public List getSpecsInSuite() { + return List.of( + contractTransferToSigReqAccountWithKeySucceeds(), + canMintAndTransferInSameContractOperation(), + workingHoursDemo(), + lpFarmSimulation()); + } + + private HapiSpec workingHoursDemo() { + final var gasToOffer = 4_000_000; + final var contract = "WorkingHours"; + final var ticketToken = "ticketToken"; + final var adminKey = "admin"; + final var treasury = "treasury"; + final var newSupplyKey = "newSupplyKey"; + + final var ticketTaking = "ticketTaking"; + final var ticketWorking = "ticketWorking"; + final var mint = "minting"; + final var burn = "burning"; + final var preMints = List.of(ByteString.copyFromUtf8("HELLO"), ByteString.copyFromUtf8("GOODBYE")); + + final AtomicLong ticketSerialNo = new AtomicLong(); + + return propertyPreservingHapiSpec("WorkingHoursDemo") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenBurn,TokenCreate,TokenMint,TokenUpdate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(adminKey), + cryptoCreate(treasury), + // we need a new user, expiry to 1 Jan 2100 costs 11M gas for token + // associate + tokenCreate(ticketToken) + .treasury(treasury) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .initialSupply(0L) + .supplyType(TokenSupplyType.INFINITE) + .adminKey(adminKey) + .supplyKey(adminKey), + mintToken(ticketToken, preMints).via(mint), + burnToken(ticketToken, List.of(1L)).via(burn), + uploadInitCode(contract)) + .when( + withOpContext((spec, opLog) -> { + final var registry = spec.registry(); + final var tokenId = registry.getTokenID(ticketToken); + final var treasuryId = registry.getAccountID(treasury); + final var creation = contractCreate( + contract, + asHeadlongAddress(asAddress(tokenId)), + asHeadlongAddress(asAddress(treasuryId))) + .gas(gasToOffer); + allRunFor(spec, creation); + }), + newKeyNamed(newSupplyKey).shape(KeyShape.CONTRACT.signedWith(contract)), + tokenUpdate(ticketToken).supplyKey(newSupplyKey)) + .then( + /* Take a ticket */ + contractCall(contract, "takeTicket") + .alsoSigningWithFullPrefix(DEFAULT_CONTRACT_SENDER, treasury) + .gas(4_000_000) + .via(ticketTaking) + .exposingResultTo(result -> { + LOG.info("Explicit mint result is {}", result); + ticketSerialNo.set(((Long) result[0])); + }), + getTxnRecord(ticketTaking), + getAccountBalance(DEFAULT_CONTRACT_SENDER).logged().hasTokenBalance(ticketToken, 1L), + /* Our ticket number is 3 (b/c of the two pre-mints), so we must call + * work twice before the contract will actually accept our ticket. */ + sourcing(() -> contractCall(contract, "workTicket", ticketSerialNo.get()) + .gas(2_000_000) + .alsoSigningWithFullPrefix(DEFAULT_CONTRACT_SENDER)), + getAccountBalance(DEFAULT_CONTRACT_SENDER).hasTokenBalance(ticketToken, 1L), + sourcing(() -> contractCall(contract, "workTicket", ticketSerialNo.get()) + .gas(2_000_000) + .alsoSigningWithFullPrefix(DEFAULT_CONTRACT_SENDER) + .via(ticketWorking)), + getAccountBalance(DEFAULT_CONTRACT_SENDER).hasTokenBalance(ticketToken, 0L), + getTokenInfo(ticketToken).hasTotalSupply(1L), + /* Review the history */ + getTxnRecord(ticketTaking).andAllChildRecords().logged(), + getTxnRecord(ticketWorking).andAllChildRecords().logged()); + } + + private HapiSpec canMintAndTransferInSameContractOperation() { + final AtomicReference tokenMirrorAddr = new AtomicReference<>(); + final AtomicReference aCivilianMirrorAddr = new AtomicReference<>(); + final var nfToken = "nfToken"; + final var multiKey = "multiKey"; + final var aCivilian = "aCivilian"; + final var treasuryContract = "SomeERC721Scenarios"; + final var mintAndTransferTxn = "mintAndTransferTxn"; + final var mintAndTransferAndBurnTxn = "mintAndTransferAndBurnTxn"; + + return propertyPreservingHapiSpec("CanMintAndTransferInSameContractOperation") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenMint", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(multiKey), + cryptoCreate(aCivilian) + .exposingCreatedIdTo(id -> aCivilianMirrorAddr.set(asHexedSolidityAddress(id))), + uploadInitCode(treasuryContract), + contractCreate(treasuryContract).adminKey(multiKey), + tokenCreate(nfToken) + .supplyKey(multiKey) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(treasuryContract) + .initialSupply(0) + .exposingCreatedIdTo(idLit -> + tokenMirrorAddr.set(asHexedSolidityAddress(HapiPropertySource.asToken(idLit)))), + mintToken( + nfToken, + List.of( + // 1 + ByteString.copyFromUtf8("A penny for"), + // 2 + ByteString.copyFromUtf8("the Old Guy"))), + tokenAssociate(aCivilian, nfToken), + cryptoTransfer(movingUnique(nfToken, 2L).between(treasuryContract, aCivilian))) + .when(sourcing(() -> contractCall( + treasuryContract, + "nonSequiturMintAndTransfer", + asHeadlongAddress(tokenMirrorAddr.get()), + asHeadlongAddress(aCivilianMirrorAddr.get())) + .via(mintAndTransferTxn) + .gas(4_000_000) + .alsoSigningWithFullPrefix(multiKey))) + .then( + getTokenInfo(nfToken).hasTotalSupply(4L), + getTokenNftInfo(nfToken, 3L) + .hasSerialNum(3L) + .hasAccountID(aCivilian) + .hasMetadata(ByteString.copyFrom(new byte[] {(byte) 0xee})), + getTokenNftInfo(nfToken, 4L) + .hasSerialNum(4L) + .hasAccountID(aCivilian) + .hasMetadata(ByteString.copyFrom(new byte[] {(byte) 0xff})), + sourcing(() -> contractCall( + treasuryContract, + "nonSequiturMintAndTransferAndBurn", + asHeadlongAddress(tokenMirrorAddr.get()), + asHeadlongAddress(aCivilianMirrorAddr.get())) + .via(mintAndTransferAndBurnTxn) + .gas(4_000_000) + .alsoSigningWithFullPrefix(multiKey, aCivilian))); + } + + private HapiSpec contractTransferToSigReqAccountWithKeySucceeds() { + return propertyPreservingHapiSpec("ContractTransferToSigReqAccountWithKeySucceeds") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(CONTRACT_CALLER).balance(1_000_000_000_000L), + cryptoCreate(RECEIVABLE_SIG_REQ_ACCOUNT) + .balance(1_000_000_000_000L) + .receiverSigRequired(true), + getAccountInfo(CONTRACT_CALLER).savingSnapshot("contractCallerInfo"), + getAccountInfo(RECEIVABLE_SIG_REQ_ACCOUNT).savingSnapshot(RECEIVABLE_SIG_REQ_ACCOUNT_INFO), + uploadInitCode(TRANSFERRING_CONTRACT)) + .when(contractCreate(TRANSFERRING_CONTRACT).gas(300_000L).balance(5000L)) + .then(withOpContext((spec, opLog) -> { + final var accountAddress = spec.registry() + .getAccountInfo(RECEIVABLE_SIG_REQ_ACCOUNT_INFO) + .getContractAccountID(); + final var receivableAccountKey = spec.registry() + .getAccountInfo(RECEIVABLE_SIG_REQ_ACCOUNT_INFO) + .getKey(); + final var contractCallerKey = + spec.registry().getAccountInfo("contractCallerInfo").getKey(); + spec.registry().saveKey("receivableKey", receivableAccountKey); + spec.registry().saveKey("contractCallerKey", contractCallerKey); + /* if any of the keys are missing, INVALID_SIGNATURE is returned */ + final var call = contractCall( + TRANSFERRING_CONTRACT, + TRANSFER_TO_ADDRESS, + asHeadlongAddress(accountAddress), + BigInteger.ONE) + .payingWith(CONTRACT_CALLER) + .gas(300_000) + .alsoSigningWithFullPrefix("receivableKey"); + /* calling with the receivableSigReqAccount should pass without adding keys */ + final var callWithReceivable = contractCall( + TRANSFERRING_CONTRACT, + TRANSFER_TO_ADDRESS, + asHeadlongAddress(accountAddress), + BigInteger.ONE) + .payingWith(RECEIVABLE_SIG_REQ_ACCOUNT) + .gas(300_000) + .hasKnownStatus(SUCCESS); + allRunFor(spec, call, callWithReceivable); + })); + } + + private HapiSpec lpFarmSimulation() { + final var adminKey = "adminKey"; + final var gasToOffer = 4_000_000; + final var farmInitcodeLoc = "src/main/resource/contract/bytecodes/farmInitcode.bin"; + final var consAbi = "{ \"inputs\": [ { \"internalType\": \"address\", \"name\": \"_devaddr\", \"type\":" + + " \"address\" }, { \"internalType\": \"address\", \"name\": \"_rentPayer\"," + + " \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\":" + + " \"_saucePerSecond\", \"type\": \"uint256\" }, { \"internalType\":" + + " \"uint256\", \"name\": \"_hbarPerSecond\", \"type\": \"uint256\" }, {" + + " \"internalType\": \"uint256\", \"name\": \"_maxSauceSupply\", \"type\":" + + " \"uint256\" }, { \"internalType\": \"uint256\", \"name\":" + + " \"_depositFeeTinyCents\", \"type\": \"uint256\" } ], \"stateMutability\":" + + " \"nonpayable\", \"type\": \"constructor\" }"; + final var addPoolAbi = "{ \"inputs\": [ { \"internalType\": \"uint256\", \"name\": \"_allocPoint\"," + + " \"type\": \"uint256\" }, { \"internalType\": \"address\", \"name\":" + + " \"_lpToken\", \"type\": \"address\" } ], \"name\": \"add\"," + + " \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\":" + + " \"function\" }"; + final var depositAbi = "{ \"inputs\": [ { \"internalType\": \"uint256\", \"name\": \"_pid\", \"type\":" + + " \"uint256\" }, { \"internalType\": \"uint256\", \"name\": \"_amount\"," + + " \"type\": \"uint256\" } ], \"name\": \"deposit\", \"outputs\": []," + + " \"stateMutability\": \"payable\", \"type\": \"function\" }"; + final var withdrawAbi = "{ \"inputs\": [ { \"internalType\": \"uint256\", \"name\": \"_pid\", \"type\":" + + " \"uint256\" }, { \"internalType\": \"uint256\", \"name\": \"_amount\"," + + " \"type\": \"uint256\" } ], \"name\": \"withdraw\", \"outputs\": []," + + STATE_MUTABILITY_NONPAYABLE_TYPE_FUNCTION; + final var setSauceAbi = "{ \"inputs\": [ { \"internalType\": \"address\", \"name\": \"_sauce\", \"type\":" + + " \"address\" } ], \"name\": \"setSauceAddress\", \"outputs\": []," + + STATE_MUTABILITY_NONPAYABLE_TYPE_FUNCTION; + final var transferAbi = "{ \"inputs\": [ { \"internalType\": \"address\", \"name\": \"newOwner\", \"type\":" + + " \"address\" } ], \"name\": \"transferOwnership\", \"outputs\": []," + + STATE_MUTABILITY_NONPAYABLE_TYPE_FUNCTION; + final var initcode = "farmInitcode"; + final var farm = "farm"; + final var dev = "dev"; + final var lp = "lp"; + final var sauce = "sauce"; + final var rentPayer = "rentPayer"; + final AtomicReference devAddr = new AtomicReference<>(); + final AtomicReference ownerAddr = new AtomicReference<>(); + final AtomicReference sauceAddr = new AtomicReference<>(); + final AtomicReference lpTokenAddr = new AtomicReference<>(); + final AtomicReference rentPayerAddr = new AtomicReference<>(); + + return propertyPreservingHapiSpec("lpFarmSimulation") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenMint,TokenUpdate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(adminKey), + fileCreate(initcode), + cryptoCreate(OWNER) + .balance(ONE_MILLION_HBARS) + .exposingCreatedIdTo(id -> ownerAddr.set(asHexedSolidityAddress(id))), + cryptoCreate(dev) + .balance(ONE_MILLION_HBARS) + .exposingCreatedIdTo(id -> devAddr.set(asHexedSolidityAddress(id))), + cryptoCreate(rentPayer) + .balance(ONE_MILLION_HBARS) + .exposingCreatedIdTo(id -> rentPayerAddr.set(asHexedSolidityAddress(id))), + updateLargeFile(GENESIS, initcode, extractByteCode(farmInitcodeLoc)), + sourcing(() -> new HapiContractCreate( + farm, + consAbi, + asHeadlongAddress(devAddr.get()), + asHeadlongAddress(rentPayerAddr.get()), + BigInteger.valueOf(4804540L), + BigInteger.valueOf(10000L), + BigInteger.valueOf(1000000000000000L), + BigInteger.valueOf(2500000000L)) + .bytecode(initcode)), + tokenCreate(sauce) + .supplyType(TokenSupplyType.FINITE) + .initialSupply(300_000_000) + .maxSupply(1_000_000_000) + .treasury(farm) + .adminKey(adminKey) + .supplyKey(adminKey) + .exposingCreatedIdTo(idLit -> + sauceAddr.set(asHexedSolidityAddress(HapiPropertySource.asToken(idLit)))), + tokenCreate(lp) + .treasury(dev) + .initialSupply(1_000_000_000) + .exposingCreatedIdTo(idLit -> + lpTokenAddr.set(asHexedSolidityAddress(HapiPropertySource.asToken(idLit)))), + tokenAssociate(dev, sauce), + sourcing( + () -> contractCallWithFunctionAbi(farm, setSauceAbi, asHeadlongAddress(sauceAddr.get())) + .gas(gasToOffer) + .refusingEthConversion()), + sourcing( + () -> contractCallWithFunctionAbi(farm, transferAbi, asHeadlongAddress(ownerAddr.get())) + .gas(gasToOffer) + .refusingEthConversion())) + .when( + sourcing(() -> contractCallWithFunctionAbi( + farm, + addPoolAbi, + BigInteger.valueOf(2392L), + asHeadlongAddress(lpTokenAddr.get())) + .via("add") + .payingWith(OWNER) + .gas(gasToOffer) + .refusingEthConversion()), + newKeyNamed("contractControl").shape(KeyShape.CONTRACT.signedWith(farm)), + tokenUpdate(sauce).supplyKey("contractControl"), + sourcing(() -> contractCallWithFunctionAbi( + farm, depositAbi, BigInteger.ZERO, BigInteger.valueOf(100_000)) + .sending(ONE_HUNDRED_HBARS) + .payingWith(dev) + .gas(gasToOffer) + .refusingEthConversion()), + sleepFor(1000), + sourcing(() -> contractCallWithFunctionAbi( + farm, depositAbi, BigInteger.ZERO, BigInteger.valueOf(100_000)) + .sending(ONE_HUNDRED_HBARS) + .payingWith(dev) + .gas(gasToOffer) + .via("second") + .refusingEthConversion()), + getTxnRecord("second").andAllChildRecords().logged()) + .then(sourcing(() -> contractCallWithFunctionAbi( + farm, withdrawAbi, BigInteger.ZERO, BigInteger.valueOf(200_000)) + .payingWith(dev) + .gas(gasToOffer) + .refusingEthConversion())); + } + + @Override + protected Logger getResultsLogger() { + return LOG; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCreateSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCreateSuite.java index 07fabcf28448..b125fa549af0 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCreateSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCreateSuite.java @@ -35,7 +35,6 @@ import static com.hedera.services.bdd.spec.keys.SigControl.ON; import static com.hedera.services.bdd.spec.queries.QueryVerbs.contractCallLocal; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnUtils.bytecodePath; @@ -118,7 +117,6 @@ public List getSpecsInSuite() { childCreationsHaveExpectedKeysWithOmittedAdminKey(), cannotCreateTooLargeContract(), revertedTryExtCallHasNoSideEffects(), - receiverSigReqTransferRecipientMustSignWithFullPubKeyPrefix(), cannotSendToNonExistentAccount(), delegateContractIdRequiredForTransferInDelegateCall(), vanillaSuccess(), @@ -416,57 +414,6 @@ private HapiSpec delegateContractIdRequiredForTransferInDelegateCall() { getAccountBalance(beneficiary).hasTinyBars(3 * (totalToSend / 2))); } - private HapiSpec receiverSigReqTransferRecipientMustSignWithFullPubKeyPrefix() { - final var sendInternalAndDelegateContract = "SendInternalAndDelegate"; - final var justSendContract = "JustSend"; - final var beneficiary = "civilian"; - final var balanceToDistribute = 1_000L; - - final AtomicLong justSendContractNum = new AtomicLong(); - final AtomicLong beneficiaryAccountNum = new AtomicLong(); - - return defaultHapiSpec("ReceiverSigReqTransferRecipientMustSignWithFullPubKeyPrefix") - .given( - cryptoCreate(beneficiary) - .balance(0L) - .receiverSigRequired(true) - .exposingCreatedIdTo(id -> beneficiaryAccountNum.set(id.getAccountNum())), - uploadInitCode(sendInternalAndDelegateContract, justSendContract)) - .when( - contractCreate(justSendContract).gas(300_000L).exposingNumTo(justSendContractNum::set), - contractCreate(sendInternalAndDelegateContract) - .gas(300_000L) - .balance(balanceToDistribute)) - .then( - /* Sending requires receiver signature */ - sourcing(() -> contractCall( - sendInternalAndDelegateContract, - "sendRepeatedlyTo", - BigInteger.valueOf(justSendContractNum.get()), - BigInteger.valueOf(beneficiaryAccountNum.get()), - BigInteger.valueOf(balanceToDistribute / 2)) - .hasKnownStatus(INVALID_SIGNATURE)), - /* But it's not enough to just sign using an incomplete prefix */ - sourcing(() -> contractCall( - sendInternalAndDelegateContract, - "sendRepeatedlyTo", - BigInteger.valueOf(justSendContractNum.get()), - BigInteger.valueOf(beneficiaryAccountNum.get()), - BigInteger.valueOf(balanceToDistribute / 2)) - .signedBy(DEFAULT_PAYER, beneficiary) - .hasKnownStatus(INVALID_SIGNATURE)), - /* We have to specify the full prefix so the sig can be verified async */ - getAccountInfo(beneficiary).logged(), - sourcing(() -> contractCall( - sendInternalAndDelegateContract, - "sendRepeatedlyTo", - BigInteger.valueOf(justSendContractNum.get()), - BigInteger.valueOf(beneficiaryAccountNum.get()), - BigInteger.valueOf(balanceToDistribute / 2)) - .alsoSigningWithFullPrefix(beneficiary)), - getAccountBalance(beneficiary).logged()); - } - private HapiSpec cannotCreateTooLargeContract() { ByteString contents; try { @@ -559,11 +506,8 @@ HapiSpec vanillaSuccess() { return defaultHapiSpec("VanillaSuccess") .given( uploadInitCode(contract), - contractCreate(contract).adminKey(THRESHOLD).maxAutomaticTokenAssociations(10), - getContractInfo(contract) - .has(contractWith().maxAutoAssociations(10)) - .logged() - .saveToRegistry(PARENT_INFO)) + contractCreate(contract).adminKey(THRESHOLD), + getContractInfo(contract).saveToRegistry(PARENT_INFO)) .when( contractCall(contract, "create").gas(1_000_000L).via("createChildTxn"), contractCall(contract, "getIndirect").gas(1_000_000L).via("getChildResultTxn"), diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCreateV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCreateV1SecurityModelSuite.java new file mode 100644 index 000000000000..ebdc5fc55428 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractCreateV1SecurityModelSuite.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2020-2023 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.hapi; + +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; + +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.suites.HapiSuite; +import java.math.BigInteger; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse +public class ContractCreateV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(ContractCreateV1SecurityModelSuite.class); + + public static final String EMPTY_CONSTRUCTOR_CONTRACT = "EmptyConstructor"; + + public static void main(String... args) { + new ContractCreateV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public List getSpecsInSuite() { + return List.of(receiverSigReqTransferRecipientMustSignWithFullPubKeyPrefix()); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + private HapiSpec receiverSigReqTransferRecipientMustSignWithFullPubKeyPrefix() { + final var sendInternalAndDelegateContract = "SendInternalAndDelegate"; + final var justSendContract = "JustSend"; + final var beneficiary = "civilian"; + final var balanceToDistribute = 1_000L; + + final AtomicLong justSendContractNum = new AtomicLong(); + final AtomicLong beneficiaryAccountNum = new AtomicLong(); + + return propertyPreservingHapiSpec("ReceiverSigReqTransferRecipientMustSignWithFullPubKeyPrefix") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(beneficiary) + .balance(0L) + .receiverSigRequired(true) + .exposingCreatedIdTo(id -> beneficiaryAccountNum.set(id.getAccountNum())), + uploadInitCode(sendInternalAndDelegateContract, justSendContract)) + .when( + contractCreate(justSendContract).gas(300_000L).exposingNumTo(justSendContractNum::set), + contractCreate(sendInternalAndDelegateContract) + .gas(300_000L) + .balance(balanceToDistribute)) + .then( + /* Sending requires receiver signature */ + sourcing(() -> contractCall( + sendInternalAndDelegateContract, + "sendRepeatedlyTo", + BigInteger.valueOf(justSendContractNum.get()), + BigInteger.valueOf(beneficiaryAccountNum.get()), + BigInteger.valueOf(balanceToDistribute / 2)) + .hasKnownStatus(INVALID_SIGNATURE)), + /* But it's not enough to just sign using an incomplete prefix */ + sourcing(() -> contractCall( + sendInternalAndDelegateContract, + "sendRepeatedlyTo", + BigInteger.valueOf(justSendContractNum.get()), + BigInteger.valueOf(beneficiaryAccountNum.get()), + BigInteger.valueOf(balanceToDistribute / 2)) + .signedBy(DEFAULT_PAYER, beneficiary) + .hasKnownStatus(INVALID_SIGNATURE)), + /* We have to specify the full prefix so the sig can be verified async */ + getAccountInfo(beneficiary).logged(), + sourcing(() -> contractCall( + sendInternalAndDelegateContract, + "sendRepeatedlyTo", + BigInteger.valueOf(justSendContractNum.get()), + BigInteger.valueOf(beneficiaryAccountNum.get()), + BigInteger.valueOf(balanceToDistribute / 2)) + .alsoSigningWithFullPrefix(beneficiary)), + getAccountBalance(beneficiary).logged()); + } + + @Override + protected Logger getResultsLogger() { + return log; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/opcodes/Create2OperationSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/opcodes/Create2OperationSuite.java index 169ddefc6af1..28048daeb693 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/opcodes/Create2OperationSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/opcodes/Create2OperationSuite.java @@ -174,7 +174,6 @@ public List getSpecsInSuite() { allLogOpcodesResolveExpectedContractId(), eip1014AliasIsPriorityInErcOwnerPrecompile(), canAssociateInConstructor(), - childInheritanceOfAdminKeyAuthorizesParentAssociationInConstructor(), /* --- HIP 583 --- */ canMergeCreate2ChildWithHollowAccount()); } @@ -717,32 +716,6 @@ private HapiSpec eip1014AliasIsPriorityInErcOwnerPrecompile() { .withOwner(unhex(userAliasAddr.get()))))))); } - private HapiSpec childInheritanceOfAdminKeyAuthorizesParentAssociationInConstructor() { - final var ft = "fungibleToken"; - final var multiKey = SWISS; - final var creationAndAssociation = "creationAndAssociation"; - final var immediateChildAssoc = "ImmediateChildAssociation"; - - final AtomicReference tokenMirrorAddr = new AtomicReference<>(); - final AtomicReference childMirrorAddr = new AtomicReference<>(); - - return defaultHapiSpec("childInheritanceOfAdminKeyAuthorizesParentAssociationInConstructor") - .given( - newKeyNamed(multiKey), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(ft) - .exposingCreatedIdTo(id -> - tokenMirrorAddr.set(hex(asSolidityAddress(HapiPropertySource.asToken(id)))))) - .when(uploadInitCode(immediateChildAssoc), sourcing(() -> contractCreate( - immediateChildAssoc, asHeadlongAddress(tokenMirrorAddr.get())) - .gas(2_000_000) - .adminKey(multiKey) - .payingWith(GENESIS) - .exposingNumTo(n -> childMirrorAddr.set("0.0." + (n + 1))) - .via(creationAndAssociation))) - .then(sourcing(() -> getContractInfo(childMirrorAddr.get()).logged())); - } - @SuppressWarnings("java:S5669") private HapiSpec canUseAliasesInPrecompilesAndContractKeys() { final var creation2 = CREATE_2_TXN; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/opcodes/Create2OperationV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/opcodes/Create2OperationV1SecurityModelSuite.java new file mode 100644 index 000000000000..f9eb230bed58 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/opcodes/Create2OperationV1SecurityModelSuite.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022-2023 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.opcodes; + +import static com.hedera.services.bdd.spec.HapiPropertySource.asSolidityAddress; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +import static com.hedera.services.bdd.spec.keys.TrieSigMapGenerator.uniqueWithFullPrefixesFor; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +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.contract.HapiParserUtil.asHeadlongAddress; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.swirlds.common.utility.CommonUtils.hex; + +import com.hedera.services.bdd.spec.HapiPropertySource; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.suites.HapiSuite; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class Create2OperationV1SecurityModelSuite extends HapiSuite { + + private static final Logger LOG = LogManager.getLogger(Create2OperationV1SecurityModelSuite.class); + private static final String SWISS = "swiss"; + private static final String CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS = "contracts.allowSystemUseOfHapiSigs"; + + public static void main(String... args) { + new Create2OperationV1SecurityModelSuite().runSuiteSync(); + } + + @Override + protected Logger getResultsLogger() { + return LOG; + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + public List getSpecsInSuite() { + return List.of(childInheritanceOfAdminKeyAuthorizesParentAssociationInConstructor()); + } + + private HapiSpec childInheritanceOfAdminKeyAuthorizesParentAssociationInConstructor() { + final var ft = "fungibleToken"; + final var multiKey = SWISS; + final var creationAndAssociation = "creationAndAssociation"; + final var immediateChildAssoc = "ImmediateChildAssociation"; + + final AtomicReference tokenMirrorAddr = new AtomicReference<>(); + final AtomicReference childMirrorAddr = new AtomicReference<>(); + + return propertyPreservingHapiSpec("childInheritanceOfAdminKeyAuthorizesParentAssociationInConstructor") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "TokenAssociateToAccount", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + "10_000_000"), + newKeyNamed(multiKey), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(ft) + .exposingCreatedIdTo(id -> + tokenMirrorAddr.set(hex(asSolidityAddress(HapiPropertySource.asToken(id)))))) + .when(uploadInitCode(immediateChildAssoc), sourcing(() -> contractCreate( + immediateChildAssoc, asHeadlongAddress(tokenMirrorAddr.get())) + .gas(2_000_000) + .adminKey(multiKey) + .payingWith(GENESIS) + .sigMapPrefixes(uniqueWithFullPrefixesFor(GENESIS, multiKey)) + .signedBy(GENESIS, multiKey) + .exposingNumTo(n -> childMirrorAddr.set("0.0." + (n + 1))) + .via(creationAndAssociation))) + .then(sourcing(() -> getContractInfo(childMirrorAddr.get()).logged())); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/opcodes/SelfDestructSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/opcodes/SelfDestructSuite.java index b8c3300a47d1..14540c408a07 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/opcodes/SelfDestructSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/opcodes/SelfDestructSuite.java @@ -28,7 +28,7 @@ import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; -import static com.hedera.services.bdd.suites.contract.precompile.LazyCreateThroughPrecompileSuite.mirrorAddrWith; +import static com.hedera.services.bdd.suites.contract.Utils.mirrorAddrWith; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/AssociatePrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/AssociatePrecompileSuite.java index 46d01131c9ac..aaf659a995b8 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/AssociatePrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/AssociatePrecompileSuite.java @@ -16,7 +16,6 @@ package com.hedera.services.bdd.suites.contract.precompile; -import static com.hedera.services.bdd.spec.HapiPropertySource.asDotDelimitedLongArray; import static com.hedera.services.bdd.spec.HapiPropertySource.idAsHeadlongAddress; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; import static com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts.resultWith; @@ -34,7 +33,6 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoUpdate; 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.contract.HapiParserUtil.asHeadlongAddress; import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.emptyChildRecordsCheck; @@ -42,16 +40,12 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.asToken; import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; -import static com.hederahashgraph.api.proto.java.TokenFreezeStatus.FreezeNotApplicable; -import static com.hederahashgraph.api.proto.java.TokenFreezeStatus.Frozen; -import static com.hederahashgraph.api.proto.java.TokenFreezeStatus.Unfrozen; -import static com.hederahashgraph.api.proto.java.TokenKycStatus.KycNotApplicable; -import static com.hederahashgraph.api.proto.java.TokenKycStatus.Revoked; import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -61,34 +55,24 @@ import com.hedera.services.bdd.spec.keys.KeyShape; import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; import com.hedera.services.bdd.suites.HapiSuite; -import com.hedera.services.bdd.suites.token.TokenAssociationSpecs; import com.hederahashgraph.api.proto.java.AccountID; -import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.TokenID; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; public class AssociatePrecompileSuite extends HapiSuite { private static final Logger log = LogManager.getLogger(AssociatePrecompileSuite.class); private static final long GAS_TO_OFFER = 4_000_000L; - private static final long TOTAL_SUPPLY = 1_000; private static final KeyShape DELEGATE_CONTRACT_KEY_SHAPE = KeyShape.threshOf(1, SIMPLE, DELEGATE_CONTRACT); private static final String TOKEN_TREASURY = "treasury"; - private static final String OUTER_CONTRACT = "NestedAssociateDissociate"; private static final String INNER_CONTRACT = "AssociateDissociate"; public static final String THE_CONTRACT = "AssociateDissociate"; private static final String THE_GRACEFULLY_FAILING_CONTRACT = "GracefullyFailing"; private static final String ACCOUNT = "anybody"; - private static final String FROZEN_TOKEN = "Frozen token"; - private static final String UNFROZEN_TOKEN = "Unfrozen token"; - private static final String KYC_TOKEN = "KYC token"; private static final String DELEGATE_KEY = "Delegate key"; - private static final String FREEZE_KEY = "Freeze key"; - private static final String KYC_KEY = "KYC key"; private static final byte[] ACCOUNT_ADDRESS = asAddress(AccountID.newBuilder().build()); private static final byte[] TOKEN_ADDRESS = asAddress(TokenID.newBuilder().build()); @@ -121,10 +105,7 @@ List negativeSpecs() { } List positiveSpecs() { - return List.of( - nestedAssociateWorksAsExpected(), - multipleAssociatePrecompileWithSignatureWorksForFungible(), - associateWithMissingEvmAddressHasSaneTxnAndRecord()); + return List.of(associateWithMissingEvmAddressHasSaneTxnAndRecord()); } /* -- HSCS-PREC-27 from HTS Precompile Test Plan -- */ @@ -279,128 +260,6 @@ private HapiSpec invalidlyFormattedAbiCallGracefullyFailsWithMultipleContractCal getAccountInfo(ACCOUNT).hasToken(relationshipWith(VANILLA_TOKEN))); } - /* -- HSCS-PREC-006 from HTS Precompile Test Plan -- */ - private HapiSpec multipleAssociatePrecompileWithSignatureWorksForFungible() { - final AtomicReference accountID = new AtomicReference<>(); - final AtomicReference frozenTokenID = new AtomicReference<>(); - final AtomicReference unfrozenTokenID = new AtomicReference<>(); - final AtomicReference kycTokenID = new AtomicReference<>(); - final AtomicReference vanillaTokenID = new AtomicReference<>(); - - return defaultHapiSpec("multipleAssociatePrecompileWithSignatureWorksForFungible") - .given( - newKeyNamed(FREEZE_KEY), - newKeyNamed(KYC_KEY), - cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).exposingCreatedIdTo(accountID::set), - cryptoCreate(TOKEN_TREASURY).balance(0L), - tokenCreate(FROZEN_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .initialSupply(TOTAL_SUPPLY) - .freezeKey(FREEZE_KEY) - .freezeDefault(true) - .exposingCreatedIdTo(id -> frozenTokenID.set(asToken(id))), - tokenCreate(UNFROZEN_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .freezeKey(FREEZE_KEY) - .freezeDefault(false) - .exposingCreatedIdTo(id -> unfrozenTokenID.set(asToken(id))), - tokenCreate(KYC_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .kycKey(KYC_KEY) - .exposingCreatedIdTo(id -> kycTokenID.set(asToken(id))), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - uploadInitCode(THE_CONTRACT), - contractCreate(THE_CONTRACT)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - THE_CONTRACT, - "tokensAssociate", - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - new Address[] { - HapiParserUtil.asHeadlongAddress(asAddress(frozenTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(unfrozenTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(kycTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())) - }) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("MultipleTokensAssociationsTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(ResponseCodeEnum.SUCCESS)))) - .then( - childRecordsCheck( - "MultipleTokensAssociationsTxn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)))), - getAccountInfo(ACCOUNT) - .hasToken(relationshipWith(FROZEN_TOKEN) - .kyc(KycNotApplicable) - .freeze(Frozen)) - .hasToken(relationshipWith(UNFROZEN_TOKEN) - .kyc(KycNotApplicable) - .freeze(Unfrozen)) - .hasToken( - relationshipWith(KYC_TOKEN).kyc(Revoked).freeze(FreezeNotApplicable)) - .hasToken(relationshipWith(TokenAssociationSpecs.VANILLA_TOKEN) - .kyc(KycNotApplicable) - .freeze(FreezeNotApplicable))); - } - - /* -- HSCS-PREC-010 from HTS Precompile Test Plan -- */ - private HapiSpec nestedAssociateWorksAsExpected() { - final AtomicReference accountID = new AtomicReference<>(); - final AtomicReference vanillaTokenID = new AtomicReference<>(); - - return defaultHapiSpec("nestedAssociateWorksAsExpected") - .given( - cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).exposingCreatedIdTo(accountID::set), - cryptoCreate(TOKEN_TREASURY).balance(0L), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - uploadInitCode(INNER_CONTRACT, OUTER_CONTRACT), - contractCreate(INNER_CONTRACT)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - OUTER_CONTRACT, asHeadlongAddress(getNestedContractAddress(INNER_CONTRACT, spec))), - contractCall( - OUTER_CONTRACT, - "associateDissociateContractCall", - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get()))) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("nestedAssociateTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(ResponseCodeEnum.SUCCESS)))) - .then( - childRecordsCheck( - "nestedAssociateTxn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)))), - getAccountInfo(ACCOUNT).hasNoTokenRelationship(VANILLA_TOKEN)); - } - private HapiSpec associateWithMissingEvmAddressHasSaneTxnAndRecord() { final AtomicReference
tokenAddress = new AtomicReference<>(); final var missingAddress = @@ -451,19 +310,4 @@ private HapiSpec invalidSingleAbiCallConsumesAllProvidedGas() { protected Logger getResultsLogger() { return log; } - - /* --- Helpers --- */ - private static TokenID asToken(String v) { - long[] nativeParts = asDotDelimitedLongArray(v); - return TokenID.newBuilder() - .setShardNum(nativeParts[0]) - .setRealmNum(nativeParts[1]) - .setTokenNum(nativeParts[2]) - .build(); - } - - @NotNull - public static String getNestedContractAddress(final String outerContract, final HapiSpec spec) { - return HapiPropertySource.asHexedSolidityAddress(spec.registry().getContractId(outerContract)); - } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/AssociatePrecompileV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/AssociatePrecompileV1SecurityModelSuite.java new file mode 100644 index 000000000000..f6cfe83cb05d --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/AssociatePrecompileV1SecurityModelSuite.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2021-2023 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.spec.HapiSpec.propertyPreservingHapiSpec; +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.getAccountInfo; +import static com.hedera.services.bdd.spec.queries.crypto.ExpectedTokenRel.relationshipWith; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +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.contract.HapiParserUtil.asHeadlongAddress; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.asToken; +import static com.hedera.services.bdd.suites.contract.Utils.getNestedContractAddress; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.hederahashgraph.api.proto.java.TokenFreezeStatus.FreezeNotApplicable; +import static com.hederahashgraph.api.proto.java.TokenFreezeStatus.Frozen; +import static com.hederahashgraph.api.proto.java.TokenFreezeStatus.Unfrozen; +import static com.hederahashgraph.api.proto.java.TokenKycStatus.KycNotApplicable; +import static com.hederahashgraph.api.proto.java.TokenKycStatus.Revoked; +import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; + +import com.esaulpaugh.headlong.abi.Address; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hedera.services.bdd.suites.token.TokenAssociationSpecs; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; +import com.hederahashgraph.api.proto.java.TokenID; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class AssociatePrecompileV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(AssociatePrecompileV1SecurityModelSuite.class); + + private static final long GAS_TO_OFFER = 4_000_000L; + private static final long TOTAL_SUPPLY = 1_000; + private static final String TOKEN_TREASURY = "treasury"; + private static final String OUTER_CONTRACT = "NestedAssociateDissociate"; + private static final String INNER_CONTRACT = "AssociateDissociate"; + public static final String THE_CONTRACT = "AssociateDissociate"; + private static final String ACCOUNT = "anybody"; + private static final String FROZEN_TOKEN = "Frozen token"; + private static final String UNFROZEN_TOKEN = "Unfrozen token"; + private static final String KYC_TOKEN = "KYC token"; + private static final String FREEZE_KEY = "Freeze key"; + private static final String KYC_KEY = "KYC key"; + + public static void main(String... args) { + new AssociatePrecompileV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + public List getSpecsInSuite() { + return allOf(positiveSpecs(), negativeSpecs()); + } + + List negativeSpecs() { + return List.of(); + } + + List positiveSpecs() { + return List.of(nestedAssociateWorksAsExpected(), multipleAssociatePrecompileWithSignatureWorksForFungible()); + } + + /* -- HSCS-PREC-006 from HTS Precompile Test Plan -- */ + private HapiSpec multipleAssociatePrecompileWithSignatureWorksForFungible() { + final AtomicReference accountID = new AtomicReference<>(); + final AtomicReference frozenTokenID = new AtomicReference<>(); + final AtomicReference unfrozenTokenID = new AtomicReference<>(); + final AtomicReference kycTokenID = new AtomicReference<>(); + final AtomicReference vanillaTokenID = new AtomicReference<>(); + + return propertyPreservingHapiSpec("multipleAssociatePrecompileWithSignatureWorksForFungible") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(FREEZE_KEY), + newKeyNamed(KYC_KEY), + cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).exposingCreatedIdTo(accountID::set), + cryptoCreate(TOKEN_TREASURY).balance(0L), + tokenCreate(FROZEN_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .initialSupply(TOTAL_SUPPLY) + .freezeKey(FREEZE_KEY) + .freezeDefault(true) + .exposingCreatedIdTo(id -> frozenTokenID.set(asToken(id))), + tokenCreate(UNFROZEN_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .freezeKey(FREEZE_KEY) + .freezeDefault(false) + .exposingCreatedIdTo(id -> unfrozenTokenID.set(asToken(id))), + tokenCreate(KYC_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .kycKey(KYC_KEY) + .exposingCreatedIdTo(id -> kycTokenID.set(asToken(id))), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + uploadInitCode(THE_CONTRACT), + contractCreate(THE_CONTRACT)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + THE_CONTRACT, + "tokensAssociate", + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + new Address[] { + HapiParserUtil.asHeadlongAddress(asAddress(frozenTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(unfrozenTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(kycTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())) + }) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("MultipleTokensAssociationsTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(ResponseCodeEnum.SUCCESS)))) + .then( + childRecordsCheck( + "MultipleTokensAssociationsTxn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)))), + getAccountInfo(ACCOUNT) + .hasToken(relationshipWith(FROZEN_TOKEN) + .kyc(KycNotApplicable) + .freeze(Frozen)) + .hasToken(relationshipWith(UNFROZEN_TOKEN) + .kyc(KycNotApplicable) + .freeze(Unfrozen)) + .hasToken( + relationshipWith(KYC_TOKEN).kyc(Revoked).freeze(FreezeNotApplicable)) + .hasToken(relationshipWith(TokenAssociationSpecs.VANILLA_TOKEN) + .kyc(KycNotApplicable) + .freeze(FreezeNotApplicable))); + } + + /* -- HSCS-PREC-010 from HTS Precompile Test Plan -- */ + private HapiSpec nestedAssociateWorksAsExpected() { + final AtomicReference accountID = new AtomicReference<>(); + final AtomicReference vanillaTokenID = new AtomicReference<>(); + + return propertyPreservingHapiSpec("nestedAssociateWorksAsExpected") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenDissociateFromAccount", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).exposingCreatedIdTo(accountID::set), + cryptoCreate(TOKEN_TREASURY).balance(0L), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + uploadInitCode(INNER_CONTRACT, OUTER_CONTRACT), + contractCreate(INNER_CONTRACT)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + OUTER_CONTRACT, asHeadlongAddress(getNestedContractAddress(INNER_CONTRACT, spec))), + contractCall( + OUTER_CONTRACT, + "associateDissociateContractCall", + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get()))) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("nestedAssociateTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(ResponseCodeEnum.SUCCESS)))) + .then( + childRecordsCheck( + "nestedAssociateTxn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)))), + getAccountInfo(ACCOUNT).hasNoTokenRelationship(VANILLA_TOKEN)); + } + + @Override + protected Logger getResultsLogger() { + return log; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractBurnHTSSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractBurnHTSSuite.java index 99ef6c0bebd0..30864e6668e9 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractBurnHTSSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractBurnHTSSuite.java @@ -18,51 +18,21 @@ import static com.google.protobuf.ByteString.copyFromUtf8; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; -import static com.hedera.services.bdd.spec.assertions.AssertUtils.inOrder; -import static com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts.resultWith; -import static com.hedera.services.bdd.spec.assertions.ContractLogAsserts.logWith; -import static com.hedera.services.bdd.spec.assertions.SomeFungibleTransfers.changingFungibleBalances; -import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith; -import static com.hedera.services.bdd.spec.keys.KeyShape.DELEGATE_CONTRACT; -import static com.hedera.services.bdd.spec.keys.KeyShape.SIMPLE; -import static com.hedera.services.bdd.spec.keys.KeyShape.sigs; -import static com.hedera.services.bdd.spec.keys.SigControl.ON; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; -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.contractCreate; 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.mintToken; -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.spec.transactions.TxnVerbs.tokenUpdate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; -import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; -import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFee; -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.childRecordsCheck; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; -import static com.hedera.services.bdd.suites.contract.Utils.asAddress; -import static com.hedera.services.bdd.suites.contract.Utils.asHexedAddress; -import static com.hedera.services.bdd.suites.contract.Utils.parsedToByteString; -import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; 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.REVERTED_SUCCESS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; import com.esaulpaugh.headlong.abi.Address; -import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; import com.hedera.services.bdd.spec.HapiPropertySource; import com.hedera.services.bdd.spec.HapiSpec; -import com.hedera.services.bdd.spec.assertions.AccountInfoAsserts; -import com.hedera.services.bdd.spec.keys.KeyShape; -import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; import com.hedera.services.bdd.suites.HapiSuite; import com.hederahashgraph.api.proto.java.TokenType; import java.math.BigInteger; @@ -83,10 +53,7 @@ public class ContractBurnHTSSuite extends HapiSuite { private static final String TOKEN = "Token"; private static final String TOKEN_TREASURY = "TokenTreasury"; private static final String MULTI_KEY = "purpose"; - private static final String CONTRACT_KEY = "Contract key"; - private static final String SUPPLY_KEY = "Supply key"; public static final String CREATION_TX = "creationTx"; - private static final String BURN_AFTER_NESTED_MINT_TX = "burnAfterNestedMint"; public static final String BURN_TOKEN_WITH_EVENT = "burnTokenWithEvent"; private static final String FIRST = "First!"; private static final String SECOND = "Second!"; @@ -108,297 +75,11 @@ public List getSpecsInSuite() { } List negativeSpecs() { - return List.of( - hscsPreC020RollbackBurnThatFailsAfterAPrecompileTransfer(), - burnFungibleV1andV2WithZeroAndNegativeValues(), - burnNonFungibleV1andV2WithNegativeValues()); + return List.of(burnFungibleV1andV2WithZeroAndNegativeValues(), burnNonFungibleV1andV2WithNegativeValues()); } List positiveSpecs() { - return List.of( - hscsPrec004TokenBurnOfFungibleTokenUnits(), - hscsPrec005TokenBurnOfNft(), - hscsPrec011BurnAfterNestedMint()); - } - - private HapiSpec hscsPrec004TokenBurnOfFungibleTokenUnits() { - final var gasUsed = 14085L; - return defaultHapiSpec("HSCS_PREC_004_token_burn_of_fungible_token_units") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(ALICE).balance(10 * ONE_HUNDRED_HBARS), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(50L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .treasury(TOKEN_TREASURY), - uploadInitCode(THE_BURN_CONTRACT), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - THE_BURN_CONTRACT, - asHeadlongAddress(asHexedAddress( - spec.registry().getTokenID(TOKEN)))) - .payingWith(ALICE) - .via(CREATION_TX) - .gas(GAS_TO_OFFER))), - getTxnRecord(CREATION_TX).logged()) - .when( - // Burning 0 amount for Fungible tokens should fail - contractCall(THE_BURN_CONTRACT, BURN_TOKEN_WITH_EVENT, BigInteger.ZERO, new long[0]) - .payingWith(ALICE) - .alsoSigningWithFullPrefix(MULTI_KEY) - .gas(GAS_TO_OFFER) - .via("burnZero") - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TOKEN, 50), - contractCall(THE_BURN_CONTRACT, BURN_TOKEN_WITH_EVENT, BigInteger.ONE, new long[0]) - .payingWith(ALICE) - .alsoSigningWithFullPrefix(MULTI_KEY) - .gas(GAS_TO_OFFER) - .via("burn"), - getTxnRecord("burn") - .hasPriority(recordWith() - .contractCallResult(resultWith() - .logs(inOrder(logWith() - .noData() - .withTopicsInOrder(List.of(parsedToByteString(49))))))), - getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TOKEN, 49), - childRecordsCheck( - "burn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_BURN) - .withStatus(SUCCESS) - .withTotalSupply(49)) - .gasUsed(gasUsed)) - .newTotalSupply(49) - .tokenTransfers( - changingFungibleBalances().including(TOKEN, TOKEN_TREASURY, -1)) - .newTotalSupply(49)), - newKeyNamed(CONTRACT_KEY).shape(DELEGATE_CONTRACT.signedWith(THE_BURN_CONTRACT)), - tokenUpdate(TOKEN).supplyKey(CONTRACT_KEY), - contractCall(THE_BURN_CONTRACT, "burnToken", BigInteger.ONE, new long[0]) - .via("burn with contract key") - .gas(GAS_TO_OFFER), - childRecordsCheck( - "burn with contract key", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_BURN) - .withStatus(SUCCESS) - .withTotalSupply(48))) - .newTotalSupply(48) - .tokenTransfers( - changingFungibleBalances().including(TOKEN, TOKEN_TREASURY, -1)))) - .then(getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TOKEN, 48)); - } - - private HapiSpec hscsPrec005TokenBurnOfNft() { - final var gasUsed = 14085; - return defaultHapiSpec("HSCS_PREC_005_token_burn_of_NFT") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(ALICE).balance(10 * ONE_HUNDRED_HBARS), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .initialSupply(0L) - .supplyKey(MULTI_KEY) - .treasury(TOKEN_TREASURY), - mintToken(TOKEN, List.of(copyFromUtf8(FIRST))), - mintToken(TOKEN, List.of(copyFromUtf8(SECOND))), - uploadInitCode(THE_BURN_CONTRACT), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - THE_BURN_CONTRACT, - asHeadlongAddress(asHexedAddress( - spec.registry().getTokenID(TOKEN)))) - .payingWith(ALICE) - .via(CREATION_TX) - .gas(GAS_TO_OFFER))), - getTxnRecord(CREATION_TX).logged()) - .when( - withOpContext((spec, opLog) -> { - final var serialNumbers = new long[] {1L}; - allRunFor( - spec, - contractCall(THE_BURN_CONTRACT, "burnToken", BigInteger.ZERO, serialNumbers) - .payingWith(ALICE) - .alsoSigningWithFullPrefix(MULTI_KEY) - .gas(GAS_TO_OFFER) - .via("burn")); - }), - childRecordsCheck( - "burn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_BURN) - .withStatus(SUCCESS) - .withTotalSupply(1)) - .gasUsed(gasUsed)) - .newTotalSupply(1))) - .then(getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TOKEN, 1)); - } - - private HapiSpec hscsPrec011BurnAfterNestedMint() { - final var innerContract = "MintToken"; - final var outerContract = "NestedBurn"; - final var revisedKey = KeyShape.threshOf(1, SIMPLE, DELEGATE_CONTRACT, DELEGATE_CONTRACT); - - return defaultHapiSpec("HSCS_PREC_011_burn_after_nested_mint") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(ALICE).balance(10 * ONE_HUNDRED_HBARS), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(50L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .treasury(TOKEN_TREASURY), - uploadInitCode(innerContract, outerContract), - contractCreate(innerContract).gas(GAS_TO_OFFER), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - outerContract, - asHeadlongAddress(getNestedContractAddress(innerContract, spec))) - .payingWith(ALICE) - .via(CREATION_TX) - .gas(GAS_TO_OFFER))), - getTxnRecord(CREATION_TX).logged()) - .when( - withOpContext((spec, opLog) -> allRunFor( - spec, - newKeyNamed(CONTRACT_KEY) - .shape(revisedKey.signedWith(sigs(ON, innerContract, outerContract))), - tokenUpdate(TOKEN).supplyKey(CONTRACT_KEY), - contractCall( - outerContract, - BURN_AFTER_NESTED_MINT_TX, - BigInteger.ONE, - HapiParserUtil.asHeadlongAddress(asAddress( - spec.registry().getTokenID(TOKEN))), - new long[0]) - .payingWith(ALICE) - .via(BURN_AFTER_NESTED_MINT_TX))), - childRecordsCheck( - BURN_AFTER_NESTED_MINT_TX, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_MINT) - .withStatus(SUCCESS) - .withTotalSupply(51) - .withSerialNumbers())) - .tokenTransfers( - changingFungibleBalances().including(TOKEN, TOKEN_TREASURY, 1)) - .newTotalSupply(51), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_BURN) - .withStatus(SUCCESS) - .withTotalSupply(50))) - .tokenTransfers( - changingFungibleBalances().including(TOKEN, TOKEN_TREASURY, -1)) - .newTotalSupply(50))) - .then(getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TOKEN, 50)); - } - - private HapiSpec hscsPreC020RollbackBurnThatFailsAfterAPrecompileTransfer() { - final var bob = "bob"; - final var feeCollector = "feeCollector"; - final var tokenWithHbarFee = "tokenWithHbarFee"; - final var theContract = "TransferAndBurn"; - - return defaultHapiSpec("HSCS_PREC_020_rollback_burn_that_fails_after_a_precompile_transfer") - .given( - newKeyNamed(SUPPLY_KEY), - cryptoCreate(ALICE).balance(ONE_HUNDRED_HBARS), - cryptoCreate(bob).balance(ONE_HUNDRED_HBARS), - cryptoCreate(TOKEN_TREASURY).balance(ONE_HUNDRED_HBARS), - cryptoCreate(feeCollector).balance(0L), - tokenCreate(tokenWithHbarFee) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .supplyKey(SUPPLY_KEY) - .initialSupply(0L) - .treasury(TOKEN_TREASURY) - .withCustom(fixedHbarFee(300 * ONE_HBAR, feeCollector)), - mintToken(tokenWithHbarFee, List.of(copyFromUtf8(FIRST))), - mintToken(tokenWithHbarFee, List.of(copyFromUtf8(SECOND))), - uploadInitCode(theContract), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - theContract, - asHeadlongAddress(asHexedAddress( - spec.registry().getTokenID(tokenWithHbarFee)))) - .payingWith(bob) - .gas(GAS_TO_OFFER))), - tokenAssociate(ALICE, tokenWithHbarFee), - tokenAssociate(bob, tokenWithHbarFee), - tokenAssociate(theContract, tokenWithHbarFee), - cryptoTransfer(movingUnique(tokenWithHbarFee, 2L).between(TOKEN_TREASURY, ALICE)) - .payingWith(GENESIS), - getAccountInfo(feeCollector) - .has(AccountInfoAsserts.accountWith().balance(0L))) - .when( - withOpContext((spec, opLog) -> { - final var serialNumbers = new long[] {1L}; - allRunFor( - spec, - contractCall( - theContract, - "transferBurn", - HapiParserUtil.asHeadlongAddress(asAddress( - spec.registry().getAccountID(ALICE))), - HapiParserUtil.asHeadlongAddress(asAddress( - spec.registry().getAccountID(bob))), - BigInteger.ZERO, - 2L, - serialNumbers) - .alsoSigningWithFullPrefix(ALICE, SUPPLY_KEY) - .gas(GAS_TO_OFFER) - .via("contractCallTxn") - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)); - }), - childRecordsCheck( - "contractCallTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(REVERTED_SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_BURN) - .withStatus(SUCCESS) - .withTotalSupply(1))), - recordWith() - .status(INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .withStatus( - INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE))))) - .then( - getAccountBalance(bob).hasTokenBalance(tokenWithHbarFee, 0), - getAccountBalance(TOKEN_TREASURY).hasTokenBalance(tokenWithHbarFee, 1), - getAccountBalance(ALICE).hasTokenBalance(tokenWithHbarFee, 1)); + return List.of(); } private HapiSpec burnFungibleV1andV2WithZeroAndNegativeValues() { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractBurnHTSV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractBurnHTSV1SecurityModelSuite.java new file mode 100644 index 000000000000..f2b317537f2c --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractBurnHTSV1SecurityModelSuite.java @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2021-2023 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.google.protobuf.ByteString.copyFromUtf8; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +import static com.hedera.services.bdd.spec.assertions.AssertUtils.inOrder; +import static com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts.resultWith; +import static com.hedera.services.bdd.spec.assertions.ContractLogAsserts.logWith; +import static com.hedera.services.bdd.spec.assertions.SomeFungibleTransfers.changingFungibleBalances; +import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith; +import static com.hedera.services.bdd.spec.keys.KeyShape.DELEGATE_CONTRACT; +import static com.hedera.services.bdd.spec.keys.KeyShape.SIMPLE; +import static com.hedera.services.bdd.spec.keys.KeyShape.sigs; +import static com.hedera.services.bdd.spec.keys.SigControl.ON; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; +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.contractCreate; +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.mintToken; +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.spec.transactions.TxnVerbs.tokenUpdate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFee; +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.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.asHexedAddress; +import static com.hedera.services.bdd.suites.contract.Utils.parsedToByteString; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +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.REVERTED_SUCCESS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; + +import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; +import com.hedera.services.bdd.spec.HapiPropertySource; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.assertions.AccountInfoAsserts; +import com.hedera.services.bdd.spec.keys.KeyShape; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.TokenType; +import java.math.BigInteger; +import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse +public class ContractBurnHTSV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(ContractBurnHTSV1SecurityModelSuite.class); + + private static final long GAS_TO_OFFER = 4_000_000L; + public static final String THE_BURN_CONTRACT = "BurnToken"; + public static final String MULTIVERSION_BURN_CONTRACT = "MultiversionBurn"; + + public static final String ALICE = "Alice"; + private static final String TOKEN = "Token"; + private static final String TOKEN_TREASURY = "TokenTreasury"; + private static final String MULTI_KEY = "purpose"; + private static final String SUPPLY_KEY = "Supply key"; + private static final String CONTRACT_KEY = "Contract key"; + public static final String CREATION_TX = "creationTx"; + private static final String BURN_AFTER_NESTED_MINT_TX = "burnAfterNestedMint"; + public static final String BURN_TOKEN_WITH_EVENT = "burnTokenWithEvent"; + private static final String FIRST = "First!"; + private static final String SECOND = "Second!"; + + public static void main(String... args) { + new ContractBurnHTSV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + public List getSpecsInSuite() { + return allOf(positiveSpecs(), negativeSpecs()); + } + + List negativeSpecs() { + return List.of(hscsPreC020RollbackBurnThatFailsAfterAPrecompileTransfer()); + } + + List positiveSpecs() { + return List.of( + hscsPrec004TokenBurnOfFungibleTokenUnits(), + hscsPrec005TokenBurnOfNft(), + hscsPrec011BurnAfterNestedMint()); + } + + private HapiSpec hscsPreC020RollbackBurnThatFailsAfterAPrecompileTransfer() { + final var bob = "bob"; + final var feeCollector = "feeCollector"; + final var tokenWithHbarFee = "tokenWithHbarFee"; + final var theContract = "TransferAndBurn"; + + return propertyPreservingHapiSpec("hscsPreC020RollbackBurnThatFailsAfterAPrecompileTransfer") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenBurn,TokenCreate,TokenMint", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(SUPPLY_KEY), + cryptoCreate(ALICE).balance(ONE_HUNDRED_HBARS), + cryptoCreate(bob).balance(ONE_HUNDRED_HBARS), + cryptoCreate(TOKEN_TREASURY).balance(ONE_HUNDRED_HBARS), + cryptoCreate(feeCollector).balance(0L), + tokenCreate(tokenWithHbarFee) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyKey(SUPPLY_KEY) + .initialSupply(0L) + .treasury(TOKEN_TREASURY) + .withCustom(fixedHbarFee(300 * ONE_HBAR, feeCollector)), + mintToken(tokenWithHbarFee, List.of(copyFromUtf8(FIRST))), + mintToken(tokenWithHbarFee, List.of(copyFromUtf8(SECOND))), + uploadInitCode(theContract), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + theContract, + asHeadlongAddress(asHexedAddress( + spec.registry().getTokenID(tokenWithHbarFee)))) + .payingWith(bob) + .gas(GAS_TO_OFFER))), + tokenAssociate(ALICE, tokenWithHbarFee), + tokenAssociate(bob, tokenWithHbarFee), + tokenAssociate(theContract, tokenWithHbarFee), + cryptoTransfer(movingUnique(tokenWithHbarFee, 2L).between(TOKEN_TREASURY, ALICE)) + .payingWith(GENESIS), + getAccountInfo(feeCollector) + .has(AccountInfoAsserts.accountWith().balance(0L))) + .when( + withOpContext((spec, opLog) -> { + final var serialNumbers = new long[] {1L}; + allRunFor( + spec, + contractCall( + theContract, + "transferBurn", + HapiParserUtil.asHeadlongAddress(asAddress( + spec.registry().getAccountID(ALICE))), + HapiParserUtil.asHeadlongAddress(asAddress( + spec.registry().getAccountID(bob))), + BigInteger.ZERO, + 2L, + serialNumbers) + .alsoSigningWithFullPrefix(ALICE, SUPPLY_KEY) + .gas(GAS_TO_OFFER) + .via("contractCallTxn") + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)); + }), + childRecordsCheck( + "contractCallTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(REVERTED_SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_BURN) + .withStatus(SUCCESS) + .withTotalSupply(1))), + recordWith() + .status(INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .withStatus( + INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE))))) + .then( + getAccountBalance(bob).hasTokenBalance(tokenWithHbarFee, 0), + getAccountBalance(TOKEN_TREASURY).hasTokenBalance(tokenWithHbarFee, 1), + getAccountBalance(ALICE).hasTokenBalance(tokenWithHbarFee, 1)); + } + + private HapiSpec hscsPrec004TokenBurnOfFungibleTokenUnits() { + final var gasUsed = 14085L; + return propertyPreservingHapiSpec("hscsPrec004TokenBurnOfFungibleTokenUnits") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenBurn,TokenCreate,TokenUpdate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(ALICE).balance(10 * ONE_HUNDRED_HBARS), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(50L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .treasury(TOKEN_TREASURY), + uploadInitCode(THE_BURN_CONTRACT), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + THE_BURN_CONTRACT, + asHeadlongAddress(asHexedAddress( + spec.registry().getTokenID(TOKEN)))) + .payingWith(ALICE) + .via(CREATION_TX) + .gas(GAS_TO_OFFER))), + getTxnRecord(CREATION_TX).logged()) + .when( + // Burning 0 amount for Fungible tokens should fail + contractCall(THE_BURN_CONTRACT, BURN_TOKEN_WITH_EVENT, BigInteger.ZERO, new long[0]) + .payingWith(ALICE) + .alsoSigningWithFullPrefix(MULTI_KEY) + .gas(GAS_TO_OFFER) + .via("burnZero") + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TOKEN, 50), + contractCall(THE_BURN_CONTRACT, BURN_TOKEN_WITH_EVENT, BigInteger.ONE, new long[0]) + .payingWith(ALICE) + .alsoSigningWithFullPrefix(MULTI_KEY) + .gas(GAS_TO_OFFER) + .via("burn"), + getTxnRecord("burn") + .hasPriority(recordWith() + .contractCallResult(resultWith() + .logs(inOrder(logWith() + .noData() + .withTopicsInOrder(List.of(parsedToByteString(49))))))), + getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TOKEN, 49), + childRecordsCheck( + "burn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_BURN) + .withStatus(SUCCESS) + .withTotalSupply(49)) + .gasUsed(gasUsed)) + .newTotalSupply(49) + .tokenTransfers( + changingFungibleBalances().including(TOKEN, TOKEN_TREASURY, -1)) + .newTotalSupply(49)), + newKeyNamed(CONTRACT_KEY).shape(DELEGATE_CONTRACT.signedWith(THE_BURN_CONTRACT)), + tokenUpdate(TOKEN).supplyKey(CONTRACT_KEY), + contractCall(THE_BURN_CONTRACT, "burnToken", BigInteger.ONE, new long[0]) + .via("burn with contract key") + .gas(GAS_TO_OFFER), + childRecordsCheck( + "burn with contract key", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_BURN) + .withStatus(SUCCESS) + .withTotalSupply(48))) + .newTotalSupply(48) + .tokenTransfers( + changingFungibleBalances().including(TOKEN, TOKEN_TREASURY, -1)))) + .then(getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TOKEN, 48)); + } + + private HapiSpec hscsPrec005TokenBurnOfNft() { + final var gasUsed = 14085; + return propertyPreservingHapiSpec("hscsPrec005TokenBurnOfNft") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,TokenBurn,TokenCreate,TokenMint", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(ALICE).balance(10 * ONE_HUNDRED_HBARS), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .treasury(TOKEN_TREASURY), + mintToken(TOKEN, List.of(copyFromUtf8(FIRST))), + mintToken(TOKEN, List.of(copyFromUtf8(SECOND))), + uploadInitCode(THE_BURN_CONTRACT), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + THE_BURN_CONTRACT, + asHeadlongAddress(asHexedAddress( + spec.registry().getTokenID(TOKEN)))) + .payingWith(ALICE) + .via(CREATION_TX) + .gas(GAS_TO_OFFER))), + getTxnRecord(CREATION_TX).logged()) + .when( + withOpContext((spec, opLog) -> { + final var serialNumbers = new long[] {1L}; + allRunFor( + spec, + contractCall(THE_BURN_CONTRACT, "burnToken", BigInteger.ZERO, serialNumbers) + .payingWith(ALICE) + .alsoSigningWithFullPrefix(MULTI_KEY) + .gas(GAS_TO_OFFER) + .via("burn")); + }), + childRecordsCheck( + "burn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_BURN) + .withStatus(SUCCESS) + .withTotalSupply(1)) + .gasUsed(gasUsed)) + .newTotalSupply(1))) + .then(getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TOKEN, 1)); + } + + private HapiSpec hscsPrec011BurnAfterNestedMint() { + final var innerContract = "MintToken"; + final var outerContract = "NestedBurn"; + final var revisedKey = KeyShape.threshOf(1, SIMPLE, DELEGATE_CONTRACT, DELEGATE_CONTRACT); + + return propertyPreservingHapiSpec("hscsPrec011BurnAfterNestedMint") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,TokenBurn,TokenCreate,TokenMint", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(ALICE).balance(10 * ONE_HUNDRED_HBARS), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(50L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .treasury(TOKEN_TREASURY), + uploadInitCode(innerContract, outerContract), + contractCreate(innerContract).gas(GAS_TO_OFFER), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + outerContract, + asHeadlongAddress(getNestedContractAddress(innerContract, spec))) + .payingWith(ALICE) + .via(CREATION_TX) + .gas(GAS_TO_OFFER))), + getTxnRecord(CREATION_TX).logged()) + .when( + withOpContext((spec, opLog) -> allRunFor( + spec, + newKeyNamed(CONTRACT_KEY) + .shape(revisedKey.signedWith(sigs(ON, innerContract, outerContract))), + tokenUpdate(TOKEN).supplyKey(CONTRACT_KEY), + contractCall( + outerContract, + BURN_AFTER_NESTED_MINT_TX, + BigInteger.ONE, + HapiParserUtil.asHeadlongAddress(asAddress( + spec.registry().getTokenID(TOKEN))), + new long[0]) + .payingWith(ALICE) + .via(BURN_AFTER_NESTED_MINT_TX))), + childRecordsCheck( + BURN_AFTER_NESTED_MINT_TX, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_MINT) + .withStatus(SUCCESS) + .withTotalSupply(51) + .withSerialNumbers())) + .tokenTransfers( + changingFungibleBalances().including(TOKEN, TOKEN_TREASURY, 1)) + .newTotalSupply(51), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_BURN) + .withStatus(SUCCESS) + .withTotalSupply(50))) + .tokenTransfers( + changingFungibleBalances().including(TOKEN, TOKEN_TREASURY, -1)) + .newTotalSupply(50))) + .then(getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TOKEN, 50)); + } + + @NotNull + private String getNestedContractAddress(String outerContract, HapiSpec spec) { + return HapiPropertySource.asHexedSolidityAddress(spec.registry().getContractId(outerContract)); + } + + @Override + protected Logger getResultsLogger() { + return log; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractHTSSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractHTSSuite.java index 96bfb37952d1..1e7c7b24a882 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractHTSSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractHTSSuite.java @@ -16,64 +16,34 @@ package com.hedera.services.bdd.suites.contract.precompile; -import static com.google.protobuf.ByteString.copyFromUtf8; -import static com.hedera.services.bdd.spec.HapiPropertySource.asToken; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; import static com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts.resultWith; -import static com.hedera.services.bdd.spec.assertions.SomeFungibleTransfers.changingFungibleBalances; import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith; -import static com.hedera.services.bdd.spec.assertions.TransferListAsserts.including; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; -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.contractCreate; 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.mintToken; 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.spec.transactions.TxnVerbs.uploadInitCode; import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; -import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromTo; -import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFee; -import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFee; import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; -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.childRecordsCheck; 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.asAddress; -import static com.hedera.services.bdd.suites.utils.MiscEETUtils.metadata; +import static com.hedera.services.bdd.suites.contract.Utils.getNestedContractAddress; import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; 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.INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.REVERTED_SUCCESS; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TRANSFERS_NOT_ZERO_SUM_FOR_TOKEN; -import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; -import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; import com.esaulpaugh.headlong.abi.Address; import com.hedera.services.bdd.spec.HapiSpec; -import com.hedera.services.bdd.spec.assertions.AccountInfoAsserts; -import com.hedera.services.bdd.spec.assertions.ContractInfoAsserts; -import com.hedera.services.bdd.spec.assertions.NonFungibleTransfers; import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; -import com.hedera.services.bdd.spec.transactions.token.TokenMovement; import com.hedera.services.bdd.suites.HapiSuite; -import com.hederahashgraph.api.proto.java.AccountID; -import com.hederahashgraph.api.proto.java.TokenID; -import com.hederahashgraph.api.proto.java.TokenSupplyType; import com.hederahashgraph.api.proto.java.TokenType; -import java.math.BigInteger; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -85,19 +55,12 @@ public class ContractHTSSuite extends HapiSuite { private static final long GAS_TO_OFFER = 2_000_000L; private static final long TOTAL_SUPPLY = 1_000; - private static final long AMOUNT_TO_SEND = 10L; - private static final long CUSTOM_HBAR_FEE_AMOUNT = 100L; private static final String TOKEN_TREASURY = "treasury"; private static final String A_TOKEN = "TokenA"; - private static final String NFT = "nft"; private static final String ACCOUNT = "sender"; - private static final String FEE_COLLECTOR = "feeCollector"; private static final String RECEIVER = "receiver"; - private static final String SECOND_RECEIVER = "receiver2"; - - private static final String FEE_TOKEN = "feeToken"; private static final String UNIVERSAL_KEY = "multipurpose"; @@ -116,741 +79,11 @@ public List getSpecsInSuite() { } List negativeSpecs() { - return List.of(hscsPrec017RollbackAfterInsufficientBalance(), nonZeroTransfersFail()); + return List.of(nonZeroTransfersFail()); } List positiveSpecs() { - return List.of( - distributeMultipleTokens(), - depositAndWithdrawFungibleTokens(), - transferNft(), - transferMultipleNfts(), - tokenTransferFromFeeCollector(), - tokenTransferFromFeeCollectorStaticNestedCall(), - hbarTransferFromFeeCollector()); - } - - private HapiSpec hscsPrec017RollbackAfterInsufficientBalance() { - final var alice = "alice"; - final var bob = "bob"; - final var treasuryForToken = "treasuryForToken"; - final var feeCollector = "feeCollector"; - final var supplyKey = "supplyKey"; - final var tokenWithHbarFee = "tokenWithHbarFee"; - final var theContract = "TransferAmountAndToken"; - - return defaultHapiSpec("HSCS_PREC_017_rollback_after_insufficient_balance") - .given( - newKeyNamed(supplyKey), - cryptoCreate(alice).balance(7 * ONE_HBAR), - cryptoCreate(bob).balance(ONE_HUNDRED_HBARS), - cryptoCreate(treasuryForToken).balance(ONE_HUNDRED_HBARS), - cryptoCreate(feeCollector).balance(0L), - tokenCreate(tokenWithHbarFee) - .tokenType(NON_FUNGIBLE_UNIQUE) - .supplyKey(supplyKey) - .initialSupply(0L) - .treasury(treasuryForToken) - .withCustom(fixedHbarFee(4 * ONE_HBAR, feeCollector)), - mintToken(tokenWithHbarFee, List.of(copyFromUtf8("First!"))), - mintToken(tokenWithHbarFee, List.of(copyFromUtf8("Second!"))), - uploadInitCode(theContract), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - theContract, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(tokenWithHbarFee)))))), - tokenAssociate(alice, tokenWithHbarFee), - tokenAssociate(bob, tokenWithHbarFee), - tokenAssociate(theContract, tokenWithHbarFee), - cryptoTransfer(movingUnique(tokenWithHbarFee, 1L).between(treasuryForToken, alice)) - .payingWith(GENESIS), - cryptoTransfer(movingUnique(tokenWithHbarFee, 2L).between(treasuryForToken, alice)) - .payingWith(GENESIS), - getAccountInfo(feeCollector) - .has(AccountInfoAsserts.accountWith().balance(0L))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - theContract, - "transferToAddress", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(alice))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(bob))), - 1L, - 2L) - .payingWith(bob) - .alsoSigningWithFullPrefix(alice) - .gas(GAS_TO_OFFER) - .via("contractCallTxn") - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) - .then( - childRecordsCheck( - "contractCallTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(REVERTED_SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))), - recordWith() - .status(INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .withStatus( - INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE)))), - getAccountInfo(feeCollector) - .has(AccountInfoAsserts.accountWith().balance(0L))); - } - - private HapiSpec depositAndWithdrawFungibleTokens() { - final var theContract = "ZenosBank"; - - return defaultHapiSpec("depositAndWithdrawFungibleTokens") - .given( - newKeyNamed(UNIVERSAL_KEY), - cryptoCreate(RECEIVER), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(A_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .initialSupply(TOTAL_SUPPLY) - .treasury(TOKEN_TREASURY), - uploadInitCode(theContract), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - theContract, - HapiParserUtil.asHeadlongAddress(asAddress( - spec.registry().getTokenID(A_TOKEN)))) - .via("creationTx"))), - tokenAssociate(DEFAULT_CONTRACT_SENDER, List.of(A_TOKEN)), - tokenAssociate(theContract, List.of(A_TOKEN)), - cryptoTransfer(moving(200, A_TOKEN).between(TOKEN_TREASURY, DEFAULT_CONTRACT_SENDER))) - .when( - // If we are using Ethereum transactions, the DEFAULT_CONTRACT_SENDER - // signature will have to - // be validated via EthTxSigs, because in any case only DEFAULT_PAYER signs - // this call - contractCall(theContract, "depositTokens", 50L) - .gas(GAS_TO_OFFER) - .via("zeno"), - contractCall(theContract, "depositTokens", 0L) - .gas(GAS_TO_OFFER) - .via("zeroTransfers"), - contractCall(theContract, "withdrawTokens") - .payingWith(RECEIVER) - .alsoSigningWithFullPrefix(theContract) - .gas(GAS_TO_OFFER) - .via("receiverTx") - // The depositTokens will associate the Ethereum - // DEFAULT_CONTRACT_SENDER; and this - // contract fails if the msg.sender is already associated - .refusingEthConversion()) - .then( - childRecordsCheck( - "zeno", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))) - .tokenTransfers(changingFungibleBalances() - .including(A_TOKEN, DEFAULT_CONTRACT_SENDER, -50L) - .including(A_TOKEN, theContract, 50L))), - childRecordsCheck( - "receiverTx", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))) - .tokenTransfers(changingFungibleBalances() - .including(A_TOKEN, theContract, -25L) - .including(A_TOKEN, RECEIVER, 25L)))); - } - - private HapiSpec distributeMultipleTokens() { - final var theSecondReceiver = "somebody2"; - - return defaultHapiSpec("DistributeMultipleTokens") - .given( - newKeyNamed(UNIVERSAL_KEY), - cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS), - cryptoCreate(RECEIVER), - cryptoCreate(theSecondReceiver), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(A_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(TOTAL_SUPPLY) - .treasury(TOKEN_TREASURY), - uploadInitCode(VERSATILE_TRANSFERS, FEE_DISTRIBUTOR), - contractCreate(FEE_DISTRIBUTOR), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - VERSATILE_TRANSFERS, - asHeadlongAddress(getNestedContractAddress(FEE_DISTRIBUTOR, spec))))), - tokenAssociate(ACCOUNT, List.of(A_TOKEN)), - tokenAssociate(VERSATILE_TRANSFERS, List.of(A_TOKEN)), - tokenAssociate(RECEIVER, List.of(A_TOKEN)), - tokenAssociate(theSecondReceiver, List.of(A_TOKEN)), - cryptoTransfer(moving(200, A_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) - .when(withOpContext((spec, opLog) -> { - final var sender = asAddress(spec.registry().getAccountID(ACCOUNT)); - final var receiver1 = asAddress(spec.registry().getAccountID(RECEIVER)); - final var receiver2 = asAddress(spec.registry().getAccountID(theSecondReceiver)); - final var accounts = new Address[] { - HapiParserUtil.asHeadlongAddress(sender), - HapiParserUtil.asHeadlongAddress(receiver1), - HapiParserUtil.asHeadlongAddress(receiver2) - }; - final var amounts = new long[] {-10L, 5L, 5L}; - - allRunFor( - spec, - contractCall( - VERSATILE_TRANSFERS, - "distributeTokens", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(A_TOKEN))), - accounts, - amounts) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER) - .via("distributeTx")); - })) - .then(childRecordsCheck( - "distributeTx", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))) - .tokenTransfers(changingFungibleBalances() - .including(A_TOKEN, ACCOUNT, -10L) - .including(A_TOKEN, RECEIVER, 5L) - .including(A_TOKEN, theSecondReceiver, 5L)))); - } - - private HapiSpec tokenTransferFromFeeCollector() { - return defaultHapiSpec("TokenTransferFromFeeCollector") - .given( - cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).maxAutomaticTokenAssociations(10), - cryptoCreate(FEE_COLLECTOR), - cryptoCreate(RECEIVER).maxAutomaticTokenAssociations(10), - cryptoCreate(SECOND_RECEIVER), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(FEE_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .initialSupply(TOTAL_SUPPLY) - .treasury(TOKEN_TREASURY), - tokenAssociate(FEE_COLLECTOR, FEE_TOKEN), - tokenCreate(A_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(TOTAL_SUPPLY) - .treasury(TOKEN_TREASURY) - .withCustom(fixedHtsFee(100L, FEE_TOKEN, FEE_COLLECTOR)), - tokenAssociate(ACCOUNT, A_TOKEN), - tokenAssociate(RECEIVER, A_TOKEN), - tokenAssociate(SECOND_RECEIVER, A_TOKEN), - cryptoTransfer(moving(TOTAL_SUPPLY, FEE_TOKEN).between(TOKEN_TREASURY, ACCOUNT)), - cryptoTransfer(moving(TOTAL_SUPPLY, A_TOKEN).between(TOKEN_TREASURY, ACCOUNT)), - uploadInitCode(VERSATILE_TRANSFERS, FEE_DISTRIBUTOR), - contractCreate(FEE_DISTRIBUTOR), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - VERSATILE_TRANSFERS, - asHeadlongAddress(getNestedContractAddress(FEE_DISTRIBUTOR, spec)))))) - .when(withOpContext((spec, opLog) -> { - final var sender = asAddress(spec.registry().getAccountID(ACCOUNT)); - final var receiver1 = asAddress(spec.registry().getAccountID(RECEIVER)); - final var receiver2 = asAddress(spec.registry().getAccountID(SECOND_RECEIVER)); - final var accounts = new Address[] { - HapiParserUtil.asHeadlongAddress(sender), - HapiParserUtil.asHeadlongAddress(receiver1), - HapiParserUtil.asHeadlongAddress(receiver2) - }; - final var amounts = new long[] {-10L, 5L, 5L}; - - /* --- HSCS-PREC-009 --- */ - allRunFor( - spec, - contractCall( - VERSATILE_TRANSFERS, - "feeDistributionAfterTransfer", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(A_TOKEN))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(FEE_TOKEN))), - accounts, - amounts, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(FEE_COLLECTOR)))) - .gas(GAS_TO_OFFER) - .via("distributeTx") - .alsoSigningWithFullPrefix(ACCOUNT, FEE_COLLECTOR) - .hasKnownStatus(SUCCESS)); - - /* --- HSCS-PREC-018 --- */ - allRunFor( - spec, - contractCall( - VERSATILE_TRANSFERS, - "feeDistributionAfterTransfer", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(A_TOKEN))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(FEE_TOKEN))), - accounts, - amounts, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(FEE_COLLECTOR)))) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER) - .via("missingSignatureTx") - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)); - - /* --- HSCS-PREC-023 --- */ - allRunFor( - spec, - contractCall( - VERSATILE_TRANSFERS, - "feeDistributionAfterTransfer", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(A_TOKEN))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(FEE_TOKEN))), - accounts, - amounts, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(RECEIVER)))) - .alsoSigningWithFullPrefix(ACCOUNT, RECEIVER) - .gas(GAS_TO_OFFER) - .via("failingChildFrameTx") - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)); - })) - .then( - childRecordsCheck( - "distributeTx", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))) - .tokenTransfers(changingFungibleBalances() - .including(A_TOKEN, ACCOUNT, -10L) - .including(A_TOKEN, RECEIVER, 5L) - .including(A_TOKEN, SECOND_RECEIVER, 5L)), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))) - .tokenTransfers(changingFungibleBalances() - .including(FEE_TOKEN, FEE_COLLECTOR, -100L) - .including(FEE_TOKEN, ACCOUNT, 100L))), - childRecordsCheck( - "missingSignatureTx", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(REVERTED_SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))), - recordWith() - .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .withStatus(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))), - childRecordsCheck( - "failingChildFrameTx", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(REVERTED_SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))), - recordWith() - .status(INSUFFICIENT_TOKEN_BALANCE) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INSUFFICIENT_TOKEN_BALANCE)))), - getAccountBalance(ACCOUNT).hasTokenBalance(FEE_TOKEN, 1000), - getAccountBalance(FEE_COLLECTOR).hasTokenBalance(FEE_TOKEN, 0)); - } - - private HapiSpec tokenTransferFromFeeCollectorStaticNestedCall() { - return defaultHapiSpec("TokenTransferFromFeeCollectorStaticNestedCall") - .given( - cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).maxAutomaticTokenAssociations(10), - cryptoCreate(FEE_COLLECTOR), - cryptoCreate(RECEIVER).maxAutomaticTokenAssociations(10), - cryptoCreate(SECOND_RECEIVER), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(FEE_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .initialSupply(TOTAL_SUPPLY) - .treasury(TOKEN_TREASURY), - tokenAssociate(FEE_COLLECTOR, FEE_TOKEN), - tokenCreate(A_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(TOTAL_SUPPLY) - .treasury(TOKEN_TREASURY) - .withCustom(fixedHtsFee(100L, FEE_TOKEN, FEE_COLLECTOR)), - tokenAssociate(ACCOUNT, A_TOKEN), - tokenAssociate(RECEIVER, A_TOKEN), - tokenAssociate(SECOND_RECEIVER, A_TOKEN), - cryptoTransfer(moving(TOTAL_SUPPLY, FEE_TOKEN).between(TOKEN_TREASURY, ACCOUNT)), - cryptoTransfer(moving(TOTAL_SUPPLY, A_TOKEN).between(TOKEN_TREASURY, ACCOUNT)), - uploadInitCode(VERSATILE_TRANSFERS, FEE_DISTRIBUTOR), - contractCreate(FEE_DISTRIBUTOR), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - VERSATILE_TRANSFERS, - asHeadlongAddress(getNestedContractAddress(FEE_DISTRIBUTOR, spec)))))) - .when(withOpContext((spec, opLog) -> { - final var sender = asAddress(spec.registry().getAccountID(ACCOUNT)); - final var receiver1 = asAddress(spec.registry().getAccountID(RECEIVER)); - final var receiver2 = asAddress(spec.registry().getAccountID(SECOND_RECEIVER)); - final var accounts = new Address[] { - HapiParserUtil.asHeadlongAddress(sender), - HapiParserUtil.asHeadlongAddress(receiver1), - HapiParserUtil.asHeadlongAddress(receiver2) - }; - final var amounts = new long[] {-10L, 5L, 5L}; - - /* --- HSCS-PREC-009 --- */ - allRunFor( - spec, - contractCall( - VERSATILE_TRANSFERS, - "feeDistributionAfterTransferStaticNestedCall", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(A_TOKEN))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(FEE_TOKEN))), - accounts, - amounts, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(FEE_COLLECTOR)))) - .alsoSigningWithFullPrefix(ACCOUNT, FEE_COLLECTOR) - .gas(GAS_TO_OFFER) - .via("distributeTx") - .hasKnownStatus(SUCCESS)); - - /* --- HSCS-PREC-018 --- */ - allRunFor( - spec, - contractCall( - VERSATILE_TRANSFERS, - "feeDistributionAfterTransferStaticNestedCall", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(A_TOKEN))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(FEE_TOKEN))), - accounts, - amounts, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(FEE_COLLECTOR)))) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER) - .via("missingSignatureTx") - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)); - - /* --- HSCS-PREC-023 --- */ - allRunFor( - spec, - contractCall( - VERSATILE_TRANSFERS, - "feeDistributionAfterTransferStaticNestedCall", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(A_TOKEN))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(FEE_TOKEN))), - accounts, - amounts, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(RECEIVER)))) - .alsoSigningWithFullPrefix(ACCOUNT, RECEIVER) - .gas(GAS_TO_OFFER) - .via("failingChildFrameTx") - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)); - })) - .then( - childRecordsCheck( - "distributeTx", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))) - .tokenTransfers(changingFungibleBalances() - .including(A_TOKEN, ACCOUNT, -10L) - .including(A_TOKEN, RECEIVER, 5L) - .including(A_TOKEN, SECOND_RECEIVER, 5L)), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))) - .tokenTransfers(changingFungibleBalances() - .including(FEE_TOKEN, FEE_COLLECTOR, -100L) - .including(FEE_TOKEN, ACCOUNT, 100L))), - childRecordsCheck( - "missingSignatureTx", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(REVERTED_SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))), - recordWith() - .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .withStatus(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))), - childRecordsCheck( - "failingChildFrameTx", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(REVERTED_SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))), - recordWith() - .status(INSUFFICIENT_TOKEN_BALANCE) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INSUFFICIENT_TOKEN_BALANCE)))), - getAccountBalance(ACCOUNT).hasTokenBalance(FEE_TOKEN, 1000), - getAccountBalance(FEE_COLLECTOR).hasTokenBalance(FEE_TOKEN, 0)); - } - - /* --- HSCS-PREC-009 --- - * Contract is a custom hbar fee collector - * Contract that otherwise wouldn't have enough balance for a .transfer of hbars can perform the transfer after - * collecting the custom hbar fees from a nested token transfer through the HTS precompile - * */ - private HapiSpec hbarTransferFromFeeCollector() { - final var outerContract = "HbarFeeCollector"; - final var innerContract = "NestedHTSTransferrer"; - - final AtomicReference tokenID = new AtomicReference<>(); - final AtomicReference senderAccountID = new AtomicReference<>(); - final AtomicReference tokenReceiverAccountID = new AtomicReference<>(); - final AtomicReference hbarReceiverAccountID = new AtomicReference<>(); - - return defaultHapiSpec("HbarTransferFromFeeCollector") - .given( - cryptoCreate(ACCOUNT) - .balance(10 * ONE_HUNDRED_HBARS) - .exposingCreatedIdTo(senderAccountID::set) - .maxAutomaticTokenAssociations(10), - cryptoCreate(RECEIVER) - .exposingCreatedIdTo(tokenReceiverAccountID::set) - .maxAutomaticTokenAssociations(10), - cryptoCreate(SECOND_RECEIVER) - .exposingCreatedIdTo(hbarReceiverAccountID::set) - .balance(0L), - cryptoCreate(TOKEN_TREASURY), - uploadInitCode(outerContract, innerContract), - contractCreate(innerContract), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - outerContract, - asHeadlongAddress(getNestedContractAddress(innerContract, spec)))))) - .when(withOpContext((spec, opLog) -> { - allRunFor( - spec, - tokenCreate(A_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(TOTAL_SUPPLY) - .treasury(TOKEN_TREASURY) - .exposingCreatedIdTo(id -> tokenID.set(asToken(id))) - .withCustom(fixedHbarFee(CUSTOM_HBAR_FEE_AMOUNT, outerContract)), - cryptoTransfer(moving(TOTAL_SUPPLY, A_TOKEN).between(TOKEN_TREASURY, ACCOUNT))); - allRunFor( - spec, - contractCall( - outerContract, - "feeDistributionAfterTransfer", - HapiParserUtil.asHeadlongAddress(asAddress(tokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(senderAccountID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(tokenReceiverAccountID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(hbarReceiverAccountID.get())), - AMOUNT_TO_SEND, - BigInteger.valueOf(CUSTOM_HBAR_FEE_AMOUNT)) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER) - .via("distributeTx")); - })) - .then( - getTxnRecord("distributeTx") - .andAllChildRecords() - .logged() - .hasPriority(recordWith() - .transfers(including(tinyBarsFromTo( - outerContract, SECOND_RECEIVER, CUSTOM_HBAR_FEE_AMOUNT)))), - childRecordsCheck( - "distributeTx", - SUCCESS, - recordWith() - .status(SUCCESS) - .transfers(including( - tinyBarsFromTo(ACCOUNT, outerContract, CUSTOM_HBAR_FEE_AMOUNT))) - .tokenTransfers(changingFungibleBalances() - .including(A_TOKEN, ACCOUNT, -AMOUNT_TO_SEND) - .including(A_TOKEN, RECEIVER, AMOUNT_TO_SEND))), - getAccountBalance(SECOND_RECEIVER).hasTinyBars(CUSTOM_HBAR_FEE_AMOUNT)); - } - - private HapiSpec transferNft() { - return defaultHapiSpec("TransferNft") - .given( - newKeyNamed(UNIVERSAL_KEY), - cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS), - cryptoCreate(RECEIVER), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(NFT) - .tokenType(NON_FUNGIBLE_UNIQUE) - .supplyKey(UNIVERSAL_KEY) - .supplyType(TokenSupplyType.INFINITE) - .initialSupply(0) - .treasury(TOKEN_TREASURY), - tokenAssociate(ACCOUNT, NFT), - mintToken(NFT, List.of(metadata("firstMemo"), metadata("secondMemo"))), - uploadInitCode(VERSATILE_TRANSFERS, FEE_DISTRIBUTOR), - contractCreate(FEE_DISTRIBUTOR).maxAutomaticTokenAssociations(2), - getContractInfo(FEE_DISTRIBUTOR) - .has(ContractInfoAsserts.contractWith().maxAutoAssociations(2)) - .logged(), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - VERSATILE_TRANSFERS, - asHeadlongAddress(getNestedContractAddress(FEE_DISTRIBUTOR, spec))))), - tokenAssociate(VERSATILE_TRANSFERS, List.of(NFT)), - tokenAssociate(RECEIVER, List.of(NFT)), - cryptoTransfer(TokenMovement.movingUnique(NFT, 1).between(TOKEN_TREASURY, ACCOUNT)) - .logged()) - .when(withOpContext((spec, opLog) -> { - final var tokenAddress = asAddress(spec.registry().getTokenID(NFT)); - final var sender = asAddress(spec.registry().getAccountID(ACCOUNT)); - final var receiver = asAddress(spec.registry().getAccountID(RECEIVER)); - - allRunFor( - spec, - contractCall( - VERSATILE_TRANSFERS, - "transferNft", - HapiParserUtil.asHeadlongAddress(tokenAddress), - HapiParserUtil.asHeadlongAddress(sender), - HapiParserUtil.asHeadlongAddress(receiver), - 1L) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER) - .via("distributeTx")); - })) - .then( - getTokenInfo(NFT).hasTotalSupply(2), - getAccountInfo(RECEIVER).hasOwnedNfts(1), - getAccountBalance(RECEIVER).hasTokenBalance(NFT, 1), - getAccountInfo(ACCOUNT).hasOwnedNfts(0), - getAccountBalance(ACCOUNT).hasTokenBalance(NFT, 0), - childRecordsCheck( - "distributeTx", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))) - .tokenTransfers(NonFungibleTransfers.changingNFTBalances() - .including(NFT, ACCOUNT, RECEIVER, 1L)))); - } - - private HapiSpec transferMultipleNfts() { - return defaultHapiSpec("TransferMultipleNfts") - .given( - newKeyNamed(UNIVERSAL_KEY), - cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS), - cryptoCreate(RECEIVER), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(NFT) - .tokenType(NON_FUNGIBLE_UNIQUE) - .supplyKey(UNIVERSAL_KEY) - .supplyType(TokenSupplyType.INFINITE) - .initialSupply(0) - .treasury(TOKEN_TREASURY), - tokenAssociate(ACCOUNT, NFT), - mintToken(NFT, List.of(metadata("firstMemo"), metadata("secondMemo"))), - uploadInitCode(VERSATILE_TRANSFERS, FEE_DISTRIBUTOR), - contractCreate(FEE_DISTRIBUTOR), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - VERSATILE_TRANSFERS, - asHeadlongAddress(getNestedContractAddress(FEE_DISTRIBUTOR, spec))))), - tokenAssociate(VERSATILE_TRANSFERS, List.of(NFT)), - tokenAssociate(RECEIVER, List.of(NFT)), - cryptoTransfer(TokenMovement.movingUnique(NFT, 1, 2).between(TOKEN_TREASURY, ACCOUNT))) - .when(withOpContext((spec, opLog) -> { - final var tokenAddress = asAddress(spec.registry().getTokenID(NFT)); - final var sender = asAddress(spec.registry().getAccountID(ACCOUNT)); - final var receiver = asAddress(spec.registry().getAccountID(RECEIVER)); - final var theSenders = new Address[] { - HapiParserUtil.asHeadlongAddress(sender), HapiParserUtil.asHeadlongAddress(sender) - }; - final var theReceivers = new Address[] { - HapiParserUtil.asHeadlongAddress(receiver), HapiParserUtil.asHeadlongAddress(receiver) - }; - final var theSerialNumbers = new long[] {1L, 2L}; - - allRunFor( - spec, - contractCall( - VERSATILE_TRANSFERS, - "transferNfts", - HapiParserUtil.asHeadlongAddress(tokenAddress), - theSenders, - theReceivers, - theSerialNumbers) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER) - .via("distributeTx")); - })) - .then( - childRecordsCheck( - "distributeTx", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))) - .tokenTransfers(NonFungibleTransfers.changingNFTBalances() - .including(NFT, ACCOUNT, RECEIVER, 1L) - .including(NFT, ACCOUNT, RECEIVER, 2L))), - getTokenInfo(NFT).hasTotalSupply(2), - getAccountInfo(RECEIVER).hasOwnedNfts(2), - getAccountBalance(RECEIVER).hasTokenBalance(NFT, 2), - getAccountInfo(ACCOUNT).hasOwnedNfts(0), - getAccountBalance(ACCOUNT).hasTokenBalance(NFT, 0)); + return List.of(); } private HapiSpec nonZeroTransfersFail() { @@ -911,10 +144,6 @@ private HapiSpec nonZeroTransfersFail() { htsPrecompileResult().withStatus(TRANSFERS_NOT_ZERO_SUM_FOR_TOKEN))))); } - private String getNestedContractAddress(final String contract, final HapiSpec spec) { - return AssociatePrecompileSuite.getNestedContractAddress(contract, spec); - } - @Override protected Logger getResultsLogger() { return log; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractHTSV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractHTSV1SecurityModelSuite.java new file mode 100644 index 000000000000..1a3a8885b114 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractHTSV1SecurityModelSuite.java @@ -0,0 +1,913 @@ +/* + * Copyright (C) 2021-2023 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.google.protobuf.ByteString.copyFromUtf8; +import static com.hedera.services.bdd.spec.HapiPropertySource.asToken; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +import static com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts.resultWith; +import static com.hedera.services.bdd.spec.assertions.SomeFungibleTransfers.changingFungibleBalances; +import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith; +import static com.hedera.services.bdd.spec.assertions.TransferListAsserts.including; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; +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.contractCreate; +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.mintToken; +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.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; +import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromTo; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFee; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +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.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.getNestedContractAddress; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.utils.MiscEETUtils.metadata; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +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.INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.REVERTED_SUCCESS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; +import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; + +import com.esaulpaugh.headlong.abi.Address; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.assertions.AccountInfoAsserts; +import com.hedera.services.bdd.spec.assertions.ContractInfoAsserts; +import com.hedera.services.bdd.spec.assertions.NonFungibleTransfers; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.spec.transactions.token.TokenMovement; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.TokenID; +import com.hederahashgraph.api.proto.java.TokenSupplyType; +import com.hederahashgraph.api.proto.java.TokenType; +import java.math.BigInteger; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse +public class ContractHTSV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(ContractHTSV1SecurityModelSuite.class); + + public static final String VERSATILE_TRANSFERS = "VersatileTransfers"; + public static final String FEE_DISTRIBUTOR = "FeeDistributor"; + + private static final long GAS_TO_OFFER = 2_000_000L; + private static final long TOTAL_SUPPLY = 1_000; + private static final long AMOUNT_TO_SEND = 10L; + private static final long CUSTOM_HBAR_FEE_AMOUNT = 100L; + private static final String TOKEN_TREASURY = "treasury"; + + private static final String A_TOKEN = "TokenA"; + private static final String NFT = "nft"; + + private static final String ACCOUNT = "sender"; + private static final String FEE_COLLECTOR = "feeCollector"; + private static final String RECEIVER = "receiver"; + private static final String SECOND_RECEIVER = "receiver2"; + + private static final String FEE_TOKEN = "feeToken"; + + private static final String UNIVERSAL_KEY = "multipurpose"; + + public static void main(String... args) { + new ContractHTSV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + public List getSpecsInSuite() { + return allOf(positiveSpecs(), negativeSpecs()); + } + + List negativeSpecs() { + return List.of(hscsPrec017RollbackAfterInsufficientBalance()); + } + + List positiveSpecs() { + return List.of( + distributeMultipleTokens(), + depositAndWithdrawFungibleTokens(), + transferNft(), + transferMultipleNfts(), + tokenTransferFromFeeCollector(), + tokenTransferFromFeeCollectorStaticNestedCall(), + hbarTransferFromFeeCollector()); + } + + private HapiSpec hscsPrec017RollbackAfterInsufficientBalance() { + final var alice = "alice"; + final var bob = "bob"; + final var treasuryForToken = "treasuryForToken"; + final var feeCollector = "feeCollector"; + final var supplyKey = "supplyKey"; + final var tokenWithHbarFee = "tokenWithHbarFee"; + final var theContract = "TransferAmountAndToken"; + + return propertyPreservingHapiSpec("hscsPrec017RollbackAfterInsufficientBalance") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenMint", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(supplyKey), + cryptoCreate(alice).balance(7 * ONE_HBAR), + cryptoCreate(bob).balance(ONE_HUNDRED_HBARS), + cryptoCreate(treasuryForToken).balance(ONE_HUNDRED_HBARS), + cryptoCreate(feeCollector).balance(0L), + tokenCreate(tokenWithHbarFee) + .tokenType(NON_FUNGIBLE_UNIQUE) + .supplyKey(supplyKey) + .initialSupply(0L) + .treasury(treasuryForToken) + .withCustom(fixedHbarFee(4 * ONE_HBAR, feeCollector)), + mintToken(tokenWithHbarFee, List.of(copyFromUtf8("First!"))), + mintToken(tokenWithHbarFee, List.of(copyFromUtf8("Second!"))), + uploadInitCode(theContract), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + theContract, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(tokenWithHbarFee)))))), + tokenAssociate(alice, tokenWithHbarFee), + tokenAssociate(bob, tokenWithHbarFee), + tokenAssociate(theContract, tokenWithHbarFee), + cryptoTransfer(movingUnique(tokenWithHbarFee, 1L).between(treasuryForToken, alice)) + .payingWith(GENESIS), + cryptoTransfer(movingUnique(tokenWithHbarFee, 2L).between(treasuryForToken, alice)) + .payingWith(GENESIS), + getAccountInfo(feeCollector) + .has(AccountInfoAsserts.accountWith().balance(0L))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + theContract, + "transferToAddress", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(alice))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(bob))), + 1L, + 2L) + .payingWith(bob) + .alsoSigningWithFullPrefix(alice) + .gas(GAS_TO_OFFER) + .via("contractCallTxn") + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) + .then( + childRecordsCheck( + "contractCallTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(REVERTED_SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))), + recordWith() + .status(INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .withStatus( + INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE)))), + getAccountInfo(feeCollector) + .has(AccountInfoAsserts.accountWith().balance(0L))); + } + + private HapiSpec depositAndWithdrawFungibleTokens() { + final var theContract = "ZenosBank"; + + return propertyPreservingHapiSpec("depositAndWithdrawFungibleTokens") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "CryptoTransfer,TokenAssociateToAccount,TokenCreate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(UNIVERSAL_KEY), + cryptoCreate(RECEIVER), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(A_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .initialSupply(TOTAL_SUPPLY) + .treasury(TOKEN_TREASURY), + uploadInitCode(theContract), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + theContract, + HapiParserUtil.asHeadlongAddress(asAddress( + spec.registry().getTokenID(A_TOKEN)))) + .via("creationTx"))), + tokenAssociate(DEFAULT_CONTRACT_SENDER, List.of(A_TOKEN)), + tokenAssociate(theContract, List.of(A_TOKEN)), + cryptoTransfer(moving(200, A_TOKEN).between(TOKEN_TREASURY, DEFAULT_CONTRACT_SENDER))) + .when( + // If we are using Ethereum transactions, the DEFAULT_CONTRACT_SENDER + // signature will have to + // be validated via EthTxSigs, because in any case only DEFAULT_PAYER signs + // this call + contractCall(theContract, "depositTokens", 50L) + .gas(GAS_TO_OFFER) + .via("zeno"), + contractCall(theContract, "depositTokens", 0L) + .gas(GAS_TO_OFFER) + .via("zeroTransfers"), + contractCall(theContract, "withdrawTokens") + .payingWith(RECEIVER) + .alsoSigningWithFullPrefix(theContract) + .gas(GAS_TO_OFFER) + .via("receiverTx") + // The depositTokens will associate the Ethereum + // DEFAULT_CONTRACT_SENDER; and this + // contract fails if the msg.sender is already associated + .refusingEthConversion()) + .then( + childRecordsCheck( + "zeno", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))) + .tokenTransfers(changingFungibleBalances() + .including(A_TOKEN, DEFAULT_CONTRACT_SENDER, -50L) + .including(A_TOKEN, theContract, 50L))), + childRecordsCheck( + "receiverTx", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))) + .tokenTransfers(changingFungibleBalances() + .including(A_TOKEN, theContract, -25L) + .including(A_TOKEN, RECEIVER, 25L)))); + } + + private HapiSpec distributeMultipleTokens() { + final var theSecondReceiver = "somebody2"; + + return propertyPreservingHapiSpec("distributeMultipleTokens") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(UNIVERSAL_KEY), + cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS), + cryptoCreate(RECEIVER), + cryptoCreate(theSecondReceiver), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(A_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(TOTAL_SUPPLY) + .treasury(TOKEN_TREASURY), + uploadInitCode(VERSATILE_TRANSFERS, FEE_DISTRIBUTOR), + contractCreate(FEE_DISTRIBUTOR), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + VERSATILE_TRANSFERS, + asHeadlongAddress(getNestedContractAddress(FEE_DISTRIBUTOR, spec))))), + tokenAssociate(ACCOUNT, List.of(A_TOKEN)), + tokenAssociate(VERSATILE_TRANSFERS, List.of(A_TOKEN)), + tokenAssociate(RECEIVER, List.of(A_TOKEN)), + tokenAssociate(theSecondReceiver, List.of(A_TOKEN)), + cryptoTransfer(moving(200, A_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) + .when(withOpContext((spec, opLog) -> { + final var sender = asAddress(spec.registry().getAccountID(ACCOUNT)); + final var receiver1 = asAddress(spec.registry().getAccountID(RECEIVER)); + final var receiver2 = asAddress(spec.registry().getAccountID(theSecondReceiver)); + final var accounts = new Address[] { + HapiParserUtil.asHeadlongAddress(sender), + HapiParserUtil.asHeadlongAddress(receiver1), + HapiParserUtil.asHeadlongAddress(receiver2) + }; + final var amounts = new long[] {-10L, 5L, 5L}; + + allRunFor( + spec, + contractCall( + VERSATILE_TRANSFERS, + "distributeTokens", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(A_TOKEN))), + accounts, + amounts) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER) + .via("distributeTx")); + })) + .then(childRecordsCheck( + "distributeTx", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))) + .tokenTransfers(changingFungibleBalances() + .including(A_TOKEN, ACCOUNT, -10L) + .including(A_TOKEN, RECEIVER, 5L) + .including(A_TOKEN, theSecondReceiver, 5L)))); + } + + private HapiSpec tokenTransferFromFeeCollector() { + return propertyPreservingHapiSpec("tokenTransferFromFeeCollector") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).maxAutomaticTokenAssociations(10), + cryptoCreate(FEE_COLLECTOR), + cryptoCreate(RECEIVER).maxAutomaticTokenAssociations(10), + cryptoCreate(SECOND_RECEIVER), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(FEE_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .initialSupply(TOTAL_SUPPLY) + .treasury(TOKEN_TREASURY), + tokenAssociate(FEE_COLLECTOR, FEE_TOKEN), + tokenCreate(A_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(TOTAL_SUPPLY) + .treasury(TOKEN_TREASURY) + .withCustom(fixedHtsFee(100L, FEE_TOKEN, FEE_COLLECTOR)), + tokenAssociate(ACCOUNT, A_TOKEN), + tokenAssociate(RECEIVER, A_TOKEN), + tokenAssociate(SECOND_RECEIVER, A_TOKEN), + cryptoTransfer(moving(TOTAL_SUPPLY, FEE_TOKEN).between(TOKEN_TREASURY, ACCOUNT)), + cryptoTransfer(moving(TOTAL_SUPPLY, A_TOKEN).between(TOKEN_TREASURY, ACCOUNT)), + uploadInitCode(VERSATILE_TRANSFERS, FEE_DISTRIBUTOR), + contractCreate(FEE_DISTRIBUTOR), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + VERSATILE_TRANSFERS, + asHeadlongAddress(getNestedContractAddress(FEE_DISTRIBUTOR, spec)))))) + .when(withOpContext((spec, opLog) -> { + final var sender = asAddress(spec.registry().getAccountID(ACCOUNT)); + final var receiver1 = asAddress(spec.registry().getAccountID(RECEIVER)); + final var receiver2 = asAddress(spec.registry().getAccountID(SECOND_RECEIVER)); + final var accounts = new Address[] { + HapiParserUtil.asHeadlongAddress(sender), + HapiParserUtil.asHeadlongAddress(receiver1), + HapiParserUtil.asHeadlongAddress(receiver2) + }; + final var amounts = new long[] {-10L, 5L, 5L}; + + /* --- HSCS-PREC-009 --- */ + allRunFor( + spec, + contractCall( + VERSATILE_TRANSFERS, + "feeDistributionAfterTransfer", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(A_TOKEN))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(FEE_TOKEN))), + accounts, + amounts, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(FEE_COLLECTOR)))) + .gas(GAS_TO_OFFER) + .via("distributeTx") + .alsoSigningWithFullPrefix(ACCOUNT, FEE_COLLECTOR) + .hasKnownStatus(SUCCESS)); + + /* --- HSCS-PREC-018 --- */ + allRunFor( + spec, + contractCall( + VERSATILE_TRANSFERS, + "feeDistributionAfterTransfer", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(A_TOKEN))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(FEE_TOKEN))), + accounts, + amounts, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(FEE_COLLECTOR)))) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER) + .via("missingSignatureTx") + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)); + + /* --- HSCS-PREC-023 --- */ + allRunFor( + spec, + contractCall( + VERSATILE_TRANSFERS, + "feeDistributionAfterTransfer", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(A_TOKEN))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(FEE_TOKEN))), + accounts, + amounts, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(RECEIVER)))) + .alsoSigningWithFullPrefix(ACCOUNT, RECEIVER) + .gas(GAS_TO_OFFER) + .via("failingChildFrameTx") + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)); + })) + .then( + childRecordsCheck( + "distributeTx", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))) + .tokenTransfers(changingFungibleBalances() + .including(A_TOKEN, ACCOUNT, -10L) + .including(A_TOKEN, RECEIVER, 5L) + .including(A_TOKEN, SECOND_RECEIVER, 5L)), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))) + .tokenTransfers(changingFungibleBalances() + .including(FEE_TOKEN, FEE_COLLECTOR, -100L) + .including(FEE_TOKEN, ACCOUNT, 100L))), + childRecordsCheck( + "missingSignatureTx", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(REVERTED_SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))), + recordWith() + .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .withStatus(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))), + childRecordsCheck( + "failingChildFrameTx", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(REVERTED_SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))), + recordWith() + .status(INSUFFICIENT_TOKEN_BALANCE) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INSUFFICIENT_TOKEN_BALANCE)))), + getAccountBalance(ACCOUNT).hasTokenBalance(FEE_TOKEN, 1000), + getAccountBalance(FEE_COLLECTOR).hasTokenBalance(FEE_TOKEN, 0)); + } + + private HapiSpec tokenTransferFromFeeCollectorStaticNestedCall() { + return propertyPreservingHapiSpec("tokenTransferFromFeeCollectorStaticNestedCall") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).maxAutomaticTokenAssociations(10), + cryptoCreate(FEE_COLLECTOR), + cryptoCreate(RECEIVER).maxAutomaticTokenAssociations(10), + cryptoCreate(SECOND_RECEIVER), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(FEE_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .initialSupply(TOTAL_SUPPLY) + .treasury(TOKEN_TREASURY), + tokenAssociate(FEE_COLLECTOR, FEE_TOKEN), + tokenCreate(A_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(TOTAL_SUPPLY) + .treasury(TOKEN_TREASURY) + .withCustom(fixedHtsFee(100L, FEE_TOKEN, FEE_COLLECTOR)), + tokenAssociate(ACCOUNT, A_TOKEN), + tokenAssociate(RECEIVER, A_TOKEN), + tokenAssociate(SECOND_RECEIVER, A_TOKEN), + cryptoTransfer(moving(TOTAL_SUPPLY, FEE_TOKEN).between(TOKEN_TREASURY, ACCOUNT)), + cryptoTransfer(moving(TOTAL_SUPPLY, A_TOKEN).between(TOKEN_TREASURY, ACCOUNT)), + uploadInitCode(VERSATILE_TRANSFERS, FEE_DISTRIBUTOR), + contractCreate(FEE_DISTRIBUTOR), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + VERSATILE_TRANSFERS, + asHeadlongAddress(getNestedContractAddress(FEE_DISTRIBUTOR, spec)))))) + .when(withOpContext((spec, opLog) -> { + final var sender = asAddress(spec.registry().getAccountID(ACCOUNT)); + final var receiver1 = asAddress(spec.registry().getAccountID(RECEIVER)); + final var receiver2 = asAddress(spec.registry().getAccountID(SECOND_RECEIVER)); + final var accounts = new Address[] { + HapiParserUtil.asHeadlongAddress(sender), + HapiParserUtil.asHeadlongAddress(receiver1), + HapiParserUtil.asHeadlongAddress(receiver2) + }; + final var amounts = new long[] {-10L, 5L, 5L}; + + /* --- HSCS-PREC-009 --- */ + allRunFor( + spec, + contractCall( + VERSATILE_TRANSFERS, + "feeDistributionAfterTransferStaticNestedCall", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(A_TOKEN))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(FEE_TOKEN))), + accounts, + amounts, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(FEE_COLLECTOR)))) + .alsoSigningWithFullPrefix(ACCOUNT, FEE_COLLECTOR) + .gas(GAS_TO_OFFER) + .via("distributeTx") + .hasKnownStatus(SUCCESS)); + + /* --- HSCS-PREC-018 --- */ + allRunFor( + spec, + contractCall( + VERSATILE_TRANSFERS, + "feeDistributionAfterTransferStaticNestedCall", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(A_TOKEN))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(FEE_TOKEN))), + accounts, + amounts, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(FEE_COLLECTOR)))) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER) + .via("missingSignatureTx") + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)); + + /* --- HSCS-PREC-023 --- */ + allRunFor( + spec, + contractCall( + VERSATILE_TRANSFERS, + "feeDistributionAfterTransferStaticNestedCall", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(A_TOKEN))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(FEE_TOKEN))), + accounts, + amounts, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(RECEIVER)))) + .alsoSigningWithFullPrefix(ACCOUNT, RECEIVER) + .gas(GAS_TO_OFFER) + .via("failingChildFrameTx") + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)); + })) + .then( + childRecordsCheck( + "distributeTx", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))) + .tokenTransfers(changingFungibleBalances() + .including(A_TOKEN, ACCOUNT, -10L) + .including(A_TOKEN, RECEIVER, 5L) + .including(A_TOKEN, SECOND_RECEIVER, 5L)), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))) + .tokenTransfers(changingFungibleBalances() + .including(FEE_TOKEN, FEE_COLLECTOR, -100L) + .including(FEE_TOKEN, ACCOUNT, 100L))), + childRecordsCheck( + "missingSignatureTx", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(REVERTED_SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))), + recordWith() + .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .withStatus(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))), + childRecordsCheck( + "failingChildFrameTx", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(REVERTED_SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))), + recordWith() + .status(INSUFFICIENT_TOKEN_BALANCE) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INSUFFICIENT_TOKEN_BALANCE)))), + getAccountBalance(ACCOUNT).hasTokenBalance(FEE_TOKEN, 1000), + getAccountBalance(FEE_COLLECTOR).hasTokenBalance(FEE_TOKEN, 0)); + } + + /* --- HSCS-PREC-009 --- + * Contract is a custom hbar fee collector + * Contract that otherwise wouldn't have enough balance for a transfer of hbars can perform the transfer after + * collecting the custom hbar fees from a nested token transfer through the HTS precompile + * */ + private HapiSpec hbarTransferFromFeeCollector() { + final var outerContract = "HbarFeeCollector"; + final var innerContract = "NestedHTSTransferrer"; + + final AtomicReference tokenID = new AtomicReference<>(); + final AtomicReference senderAccountID = new AtomicReference<>(); + final AtomicReference tokenReceiverAccountID = new AtomicReference<>(); + final AtomicReference hbarReceiverAccountID = new AtomicReference<>(); + + return propertyPreservingHapiSpec("hbarTransferFromFeeCollector") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "CryptoTransfer", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(ACCOUNT) + .balance(10 * ONE_HUNDRED_HBARS) + .exposingCreatedIdTo(senderAccountID::set) + .maxAutomaticTokenAssociations(10), + cryptoCreate(RECEIVER) + .exposingCreatedIdTo(tokenReceiverAccountID::set) + .maxAutomaticTokenAssociations(10), + cryptoCreate(SECOND_RECEIVER) + .exposingCreatedIdTo(hbarReceiverAccountID::set) + .balance(0L), + cryptoCreate(TOKEN_TREASURY), + uploadInitCode(outerContract, innerContract), + contractCreate(innerContract), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + outerContract, + asHeadlongAddress(getNestedContractAddress(innerContract, spec)))))) + .when(withOpContext((spec, opLog) -> { + allRunFor( + spec, + tokenCreate(A_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(TOTAL_SUPPLY) + .treasury(TOKEN_TREASURY) + .exposingCreatedIdTo(id -> tokenID.set(asToken(id))) + .withCustom(fixedHbarFee(CUSTOM_HBAR_FEE_AMOUNT, outerContract)), + cryptoTransfer(moving(TOTAL_SUPPLY, A_TOKEN).between(TOKEN_TREASURY, ACCOUNT))); + allRunFor( + spec, + contractCall( + outerContract, + "feeDistributionAfterTransfer", + HapiParserUtil.asHeadlongAddress(asAddress(tokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(senderAccountID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(tokenReceiverAccountID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(hbarReceiverAccountID.get())), + AMOUNT_TO_SEND, + BigInteger.valueOf(CUSTOM_HBAR_FEE_AMOUNT)) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER) + .via("distributeTx")); + })) + .then( + getTxnRecord("distributeTx") + .andAllChildRecords() + .logged() + .hasPriority(recordWith() + .transfers(including(tinyBarsFromTo( + outerContract, SECOND_RECEIVER, CUSTOM_HBAR_FEE_AMOUNT)))), + childRecordsCheck( + "distributeTx", + SUCCESS, + recordWith() + .status(SUCCESS) + .transfers(including( + tinyBarsFromTo(ACCOUNT, outerContract, CUSTOM_HBAR_FEE_AMOUNT))) + .tokenTransfers(changingFungibleBalances() + .including(A_TOKEN, ACCOUNT, -AMOUNT_TO_SEND) + .including(A_TOKEN, RECEIVER, AMOUNT_TO_SEND))), + getAccountBalance(SECOND_RECEIVER).hasTinyBars(CUSTOM_HBAR_FEE_AMOUNT)); + } + + private HapiSpec transferNft() { + return propertyPreservingHapiSpec("transferNft") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenMint", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(UNIVERSAL_KEY), + cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS), + cryptoCreate(RECEIVER), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(NFT) + .tokenType(NON_FUNGIBLE_UNIQUE) + .supplyKey(UNIVERSAL_KEY) + .supplyType(TokenSupplyType.INFINITE) + .initialSupply(0) + .treasury(TOKEN_TREASURY), + tokenAssociate(ACCOUNT, NFT), + mintToken(NFT, List.of(metadata("firstMemo"), metadata("secondMemo"))), + uploadInitCode(VERSATILE_TRANSFERS, FEE_DISTRIBUTOR), + contractCreate(FEE_DISTRIBUTOR).maxAutomaticTokenAssociations(2), + getContractInfo(FEE_DISTRIBUTOR) + .has(ContractInfoAsserts.contractWith().maxAutoAssociations(2)) + .logged(), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + VERSATILE_TRANSFERS, + asHeadlongAddress(getNestedContractAddress(FEE_DISTRIBUTOR, spec))))), + tokenAssociate(VERSATILE_TRANSFERS, List.of(NFT)), + tokenAssociate(RECEIVER, List.of(NFT)), + cryptoTransfer(TokenMovement.movingUnique(NFT, 1).between(TOKEN_TREASURY, ACCOUNT)) + .logged()) + .when(withOpContext((spec, opLog) -> { + final var tokenAddress = asAddress(spec.registry().getTokenID(NFT)); + final var sender = asAddress(spec.registry().getAccountID(ACCOUNT)); + final var receiver = asAddress(spec.registry().getAccountID(RECEIVER)); + + allRunFor( + spec, + contractCall( + VERSATILE_TRANSFERS, + "transferNft", + HapiParserUtil.asHeadlongAddress(tokenAddress), + HapiParserUtil.asHeadlongAddress(sender), + HapiParserUtil.asHeadlongAddress(receiver), + 1L) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER) + .via("distributeTx")); + })) + .then( + getTokenInfo(NFT).hasTotalSupply(2), + getAccountInfo(RECEIVER).hasOwnedNfts(1), + getAccountBalance(RECEIVER).hasTokenBalance(NFT, 1), + getAccountInfo(ACCOUNT).hasOwnedNfts(0), + getAccountBalance(ACCOUNT).hasTokenBalance(NFT, 0), + childRecordsCheck( + "distributeTx", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))) + .tokenTransfers(NonFungibleTransfers.changingNFTBalances() + .including(NFT, ACCOUNT, RECEIVER, 1L)))); + } + + private HapiSpec transferMultipleNfts() { + return propertyPreservingHapiSpec("transferMultipleNfts") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenMint", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(UNIVERSAL_KEY), + cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS), + cryptoCreate(RECEIVER), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(NFT) + .tokenType(NON_FUNGIBLE_UNIQUE) + .supplyKey(UNIVERSAL_KEY) + .supplyType(TokenSupplyType.INFINITE) + .initialSupply(0) + .treasury(TOKEN_TREASURY), + tokenAssociate(ACCOUNT, NFT), + mintToken(NFT, List.of(metadata("firstMemo"), metadata("secondMemo"))), + uploadInitCode(VERSATILE_TRANSFERS, FEE_DISTRIBUTOR), + contractCreate(FEE_DISTRIBUTOR), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + VERSATILE_TRANSFERS, + asHeadlongAddress(getNestedContractAddress(FEE_DISTRIBUTOR, spec))))), + tokenAssociate(VERSATILE_TRANSFERS, List.of(NFT)), + tokenAssociate(RECEIVER, List.of(NFT)), + cryptoTransfer(TokenMovement.movingUnique(NFT, 1, 2).between(TOKEN_TREASURY, ACCOUNT))) + .when(withOpContext((spec, opLog) -> { + final var tokenAddress = asAddress(spec.registry().getTokenID(NFT)); + final var sender = asAddress(spec.registry().getAccountID(ACCOUNT)); + final var receiver = asAddress(spec.registry().getAccountID(RECEIVER)); + final var theSenders = new Address[] { + HapiParserUtil.asHeadlongAddress(sender), HapiParserUtil.asHeadlongAddress(sender) + }; + final var theReceivers = new Address[] { + HapiParserUtil.asHeadlongAddress(receiver), HapiParserUtil.asHeadlongAddress(receiver) + }; + final var theSerialNumbers = new long[] {1L, 2L}; + + allRunFor( + spec, + contractCall( + VERSATILE_TRANSFERS, + "transferNfts", + HapiParserUtil.asHeadlongAddress(tokenAddress), + theSenders, + theReceivers, + theSerialNumbers) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER) + .via("distributeTx")); + })) + .then( + childRecordsCheck( + "distributeTx", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))) + .tokenTransfers(NonFungibleTransfers.changingNFTBalances() + .including(NFT, ACCOUNT, RECEIVER, 1L) + .including(NFT, ACCOUNT, RECEIVER, 2L))), + getTokenInfo(NFT).hasTotalSupply(2), + getAccountInfo(RECEIVER).hasOwnedNfts(2), + getAccountBalance(RECEIVER).hasTokenBalance(NFT, 2), + getAccountInfo(ACCOUNT).hasOwnedNfts(0), + getAccountBalance(ACCOUNT).hasTokenBalance(NFT, 0)); + } + + @Override + protected Logger getResultsLogger() { + return log; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysHTSSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysHTSSuite.java index 4c5087de0fbd..7d5bde8713ba 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysHTSSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysHTSSuite.java @@ -51,6 +51,7 @@ 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.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.getNestedContractAddress; import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; import static com.hedera.services.bdd.suites.utils.MiscEETUtils.metadata; import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; @@ -85,7 +86,6 @@ import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; public class ContractKeysHTSSuite extends HapiSuite { private static final long GAS_TO_OFFER = 1_500_000L; @@ -150,7 +150,7 @@ public boolean canRunConcurrent() { @Override public List getSpecsInSuite() { - return allOf(hscsKey1(), hscsKey2(), hscsKey3(), hscsKey4(), hscsKey5(), hscsKey6(), hscsKey7(), hscsKey8()); + return allOf(hscsKey1(), hscsKey2(), hscsKey3(), hscsKey4(), hscsKey5(), hscsKey6()); } List hscsKey1() { @@ -169,7 +169,6 @@ List hscsKey2() { staticCallForTransferWithContractKey(), staticCallForBurnWithContractKey(), staticCallForMintWithContractKey(), - delegateCallForTransferWithContractKey(), delegateCallForBurnWithContractKey(), delegateCallForMintWithContractKey(), staticCallForDissociatePrecompileFails()); @@ -214,14 +213,6 @@ List hscsKey6() { return List.of(burnWithKeyAsPartOf1OfXThreshold()); } - List hscsKey7() { - return List.of(transferWithKeyAsPartOf2OfXThreshold()); - } - - List hscsKey8() { - return List.of(burnTokenWithFullPrefixAndPartialPrefixKeys()); - } - private HapiSpec burnWithKeyAsPartOf1OfXThreshold() { final var delegateContractKeyShape = KeyShape.threshOf(1, SIMPLE, DELEGATE_CONTRACT); final var contractKeyShape = KeyShape.threshOf(1, SIMPLE, KeyShape.CONTRACT); @@ -284,121 +275,6 @@ private HapiSpec burnWithKeyAsPartOf1OfXThreshold() { .including(TOKEN_USAGE, TOKEN_TREASURY, -1)))); } - private HapiSpec transferWithKeyAsPartOf2OfXThreshold() { - final AtomicReference accountID = new AtomicReference<>(); - final AtomicReference vanillaTokenTokenID = new AtomicReference<>(); - final AtomicReference receiverID = new AtomicReference<>(); - final var delegateContractKeyShape = KeyShape.threshOf(2, SIMPLE, SIMPLE, DELEGATE_CONTRACT, KeyShape.CONTRACT); - - return defaultHapiSpec("transferWithKeyAsPartOf2OfXThreshold") - .given( - newKeyNamed(SUPPLY_KEY), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(VANILLA_TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .supplyKey(SUPPLY_KEY) - .treasury(TOKEN_TREASURY) - .initialSupply(0) - .exposingCreatedIdTo(id -> vanillaTokenTokenID.set(asToken(id))), - mintToken(VANILLA_TOKEN, List.of(copyFromUtf8(FIRST_STRING_FOR_MINT))), - cryptoCreate(ACCOUNT).exposingCreatedIdTo(accountID::set), - cryptoCreate(RECEIVER).exposingCreatedIdTo(receiverID::set), - uploadInitCode(OUTER_CONTRACT, NESTED_CONTRACT), - contractCreate(NESTED_CONTRACT), - tokenAssociate(NESTED_CONTRACT, VANILLA_TOKEN), - tokenAssociate(ACCOUNT, VANILLA_TOKEN), - tokenAssociate(RECEIVER, VANILLA_TOKEN), - cryptoTransfer(movingUnique(VANILLA_TOKEN, 1L).between(TOKEN_TREASURY, ACCOUNT)) - .payingWith(GENESIS)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - OUTER_CONTRACT, asHeadlongAddress(getNestedContractAddress(NESTED_CONTRACT, spec))), - tokenAssociate(OUTER_CONTRACT, VANILLA_TOKEN), - newKeyNamed(DELEGATE_KEY) - .shape(delegateContractKeyShape.signedWith( - sigs(ON, ON, OUTER_CONTRACT, NESTED_CONTRACT))), - cryptoUpdate(ACCOUNT).key(DELEGATE_KEY), - contractCall( - OUTER_CONTRACT, - "transferDelegateCall", - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(receiverID.get())), - 1L) - .payingWith(GENESIS) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("delegateTransferCallWithDelegateContractKeyTxn") - .gas(GAS_TO_OFFER)))) - .then( - childRecordsCheck( - "delegateTransferCallWithDelegateContractKeyTxn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)))), - getAccountBalance(ACCOUNT).hasTokenBalance(VANILLA_TOKEN, 0), - getAccountBalance(RECEIVER).hasTokenBalance(VANILLA_TOKEN, 1)); - } - - private HapiSpec delegateCallForTransferWithContractKey() { - final AtomicReference accountID = new AtomicReference<>(); - final AtomicReference vanillaTokenTokenID = new AtomicReference<>(); - final AtomicReference receiverID = new AtomicReference<>(); - - return defaultHapiSpec("delegateCallForTransferWithContractKey") - .given( - newKeyNamed(SUPPLY_KEY), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(VANILLA_TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .supplyKey(SUPPLY_KEY) - .treasury(TOKEN_TREASURY) - .initialSupply(0) - .exposingCreatedIdTo(id -> vanillaTokenTokenID.set(asToken(id))), - mintToken(VANILLA_TOKEN, List.of(copyFromUtf8(FIRST_STRING_FOR_MINT))), - cryptoCreate(ACCOUNT).exposingCreatedIdTo(accountID::set), - cryptoCreate(RECEIVER).exposingCreatedIdTo(receiverID::set), - uploadInitCode(OUTER_CONTRACT, NESTED_CONTRACT), - contractCreate(NESTED_CONTRACT), - tokenAssociate(NESTED_CONTRACT, VANILLA_TOKEN), - tokenAssociate(ACCOUNT, VANILLA_TOKEN), - tokenAssociate(RECEIVER, VANILLA_TOKEN), - cryptoTransfer(movingUnique(VANILLA_TOKEN, 1L).between(TOKEN_TREASURY, ACCOUNT)) - .payingWith(GENESIS)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - OUTER_CONTRACT, asHeadlongAddress(getNestedContractAddress(NESTED_CONTRACT, spec))), - tokenAssociate(OUTER_CONTRACT, VANILLA_TOKEN), - newKeyNamed(CONTRACT_KEY).shape(CONTRACT_KEY_SHAPE.signedWith(sigs(ON, OUTER_CONTRACT))), - cryptoUpdate(ACCOUNT).key(CONTRACT_KEY), - contractCall( - OUTER_CONTRACT, - "transferDelegateCall", - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(receiverID.get())), - 1L) - .payingWith(GENESIS) - .via("delegateTransferCallWithContractKeyTxn") - .hasKnownStatus(ResponseCodeEnum.CONTRACT_REVERT_EXECUTED) - .gas(GAS_TO_OFFER)))) - .then( - childRecordsCheck( - "delegateTransferCallWithContractKeyTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .withStatus(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))), - getAccountBalance(ACCOUNT).hasTokenBalance(VANILLA_TOKEN, 1), - getAccountBalance(RECEIVER).hasTokenBalance(VANILLA_TOKEN, 0)); - } - private HapiSpec delegateCallForBurnWithContractKey() { final AtomicReference vanillaTokenTokenID = new AtomicReference<>(); @@ -2410,81 +2286,6 @@ private HapiSpec callForBurnWithContractKey() { .then(getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TOKEN_USAGE, 49)); } - private HapiSpec burnTokenWithFullPrefixAndPartialPrefixKeys() { - final var firstBurnTxn = "firstBurnTxn"; - final var secondBurnTxn = "secondBurnTxn"; - final var amount = 99L; - final AtomicLong fungibleNum = new AtomicLong(); - - return defaultHapiSpec("burnTokenWithFullPrefixAndPartialPrefixKeys") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(ACCOUNT_NAME).balance(10 * ONE_HUNDRED_HBARS), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(TYPE_OF_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(100) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .exposingCreatedIdTo(idLit -> fungibleNum.set(asDotDelimitedLongArray(idLit)[2])), - uploadInitCode(ORDINARY_CALLS_CONTRACT), - contractCreate(ORDINARY_CALLS_CONTRACT)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - ORDINARY_CALLS_CONTRACT, - "burnTokenCall", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(TYPE_OF_TOKEN))), - BigInteger.ONE, - new long[0]) - .via(firstBurnTxn) - .payingWith(ACCOUNT_NAME) - .signedBy(MULTI_KEY) - .signedBy(ACCOUNT_NAME) - .hasKnownStatus(SUCCESS), - contractCall( - ORDINARY_CALLS_CONTRACT, - "burnTokenCall", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(TYPE_OF_TOKEN))), - BigInteger.ONE, - new long[0]) - .via(secondBurnTxn) - .payingWith(ACCOUNT_NAME) - .alsoSigningWithFullPrefix(MULTI_KEY) - .hasKnownStatus(SUCCESS)))) - .then( - childRecordsCheck( - firstBurnTxn, - SUCCESS, - recordWith() - .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_BURN) - .withStatus(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))), - childRecordsCheck( - secondBurnTxn, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_BURN) - .withStatus(SUCCESS) - .withTotalSupply(99))) - .newTotalSupply(99)), - getTokenInfo(TYPE_OF_TOKEN).hasTotalSupply(amount), - getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TYPE_OF_TOKEN, amount)); - } - - @NotNull - private String getNestedContractAddress(String contract, HapiSpec spec) { - return AssociatePrecompileSuite.getNestedContractAddress(contract, spec); - } - @Override protected Logger getResultsLogger() { return log; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysHTSV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysHTSV1SecurityModelSuite.java new file mode 100644 index 000000000000..6fd9677bcd1c --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysHTSV1SecurityModelSuite.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2021-2023 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.google.protobuf.ByteString.copyFromUtf8; +import static com.hedera.services.bdd.spec.HapiPropertySource.asDotDelimitedLongArray; +import static com.hedera.services.bdd.spec.HapiPropertySource.asToken; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +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.keys.KeyShape.DELEGATE_CONTRACT; +import static com.hedera.services.bdd.spec.keys.KeyShape.SIMPLE; +import static com.hedera.services.bdd.spec.keys.KeyShape.sigs; +import static com.hedera.services.bdd.spec.keys.SigControl.ON; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +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.cryptoUpdate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; +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.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; +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.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.getNestedContractAddress; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; + +import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.keys.KeyShape; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; +import com.hederahashgraph.api.proto.java.TokenID; +import com.hederahashgraph.api.proto.java.TokenType; +import java.math.BigInteger; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class ContractKeysHTSV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(ContractKeysHTSV1SecurityModelSuite.class); + + private static final long GAS_TO_OFFER = 1_500_000L; + + private static final String TOKEN_TREASURY = "treasury"; + + private static final String ACCOUNT = "sender"; + private static final String RECEIVER = "receiver"; + + private static final KeyShape CONTRACT_KEY_SHAPE = KeyShape.threshOf(1, SIMPLE, KeyShape.CONTRACT); + + private static final String DELEGATE_KEY = "Delegate Contract Key"; + private static final String CONTRACT_KEY = "Contract Key"; + private static final String MULTI_KEY = "Multi Key"; + private static final String SUPPLY_KEY = "Supply Key"; + + private static final String ORDINARY_CALLS_CONTRACT = "HTSCalls"; + private static final String OUTER_CONTRACT = "DelegateContract"; + private static final String NESTED_CONTRACT = "ServiceContract"; + private static final String FIRST_STRING_FOR_MINT = "First!"; + private static final String ACCOUNT_NAME = "anybody"; + private static final String TYPE_OF_TOKEN = "fungibleToken"; + + public static void main(String... args) { + new ContractKeysHTSV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + public List getSpecsInSuite() { + return List.of( + delegateCallForTransferWithContractKey(), + transferWithKeyAsPartOf2OfXThreshold(), + burnTokenWithFullPrefixAndPartialPrefixKeys()); + } + + private HapiSpec transferWithKeyAsPartOf2OfXThreshold() { + final AtomicReference accountID = new AtomicReference<>(); + final AtomicReference vanillaTokenTokenID = new AtomicReference<>(); + final AtomicReference receiverID = new AtomicReference<>(); + final var delegateContractKeyShape = KeyShape.threshOf(2, SIMPLE, SIMPLE, DELEGATE_CONTRACT, KeyShape.CONTRACT); + + return propertyPreservingHapiSpec("transferWithKeyAsPartOf2OfXThreshold") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenMint", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(SUPPLY_KEY), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(VANILLA_TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyKey(SUPPLY_KEY) + .treasury(TOKEN_TREASURY) + .initialSupply(0) + .exposingCreatedIdTo(id -> vanillaTokenTokenID.set(asToken(id))), + mintToken(VANILLA_TOKEN, List.of(copyFromUtf8(FIRST_STRING_FOR_MINT))), + cryptoCreate(ACCOUNT).exposingCreatedIdTo(accountID::set), + cryptoCreate(RECEIVER).exposingCreatedIdTo(receiverID::set), + uploadInitCode(OUTER_CONTRACT, NESTED_CONTRACT), + contractCreate(NESTED_CONTRACT), + tokenAssociate(NESTED_CONTRACT, VANILLA_TOKEN), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + tokenAssociate(RECEIVER, VANILLA_TOKEN), + cryptoTransfer(movingUnique(VANILLA_TOKEN, 1L).between(TOKEN_TREASURY, ACCOUNT)) + .payingWith(GENESIS)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + OUTER_CONTRACT, asHeadlongAddress(getNestedContractAddress(NESTED_CONTRACT, spec))), + tokenAssociate(OUTER_CONTRACT, VANILLA_TOKEN), + newKeyNamed(DELEGATE_KEY) + .shape(delegateContractKeyShape.signedWith( + sigs(ON, ON, OUTER_CONTRACT, NESTED_CONTRACT))), + cryptoUpdate(ACCOUNT).key(DELEGATE_KEY), + contractCall( + OUTER_CONTRACT, + "transferDelegateCall", + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(receiverID.get())), + 1L) + .payingWith(GENESIS) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("delegateTransferCallWithDelegateContractKeyTxn") + .gas(GAS_TO_OFFER)))) + .then( + childRecordsCheck( + "delegateTransferCallWithDelegateContractKeyTxn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)))), + getAccountBalance(ACCOUNT).hasTokenBalance(VANILLA_TOKEN, 0), + getAccountBalance(RECEIVER).hasTokenBalance(VANILLA_TOKEN, 1)); + } + + private HapiSpec delegateCallForTransferWithContractKey() { + final AtomicReference accountID = new AtomicReference<>(); + final AtomicReference vanillaTokenTokenID = new AtomicReference<>(); + final AtomicReference receiverID = new AtomicReference<>(); + + return propertyPreservingHapiSpec("delegateCallForTransferWithContractKey") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenMint", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(SUPPLY_KEY), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(VANILLA_TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyKey(SUPPLY_KEY) + .treasury(TOKEN_TREASURY) + .initialSupply(0) + .exposingCreatedIdTo(id -> vanillaTokenTokenID.set(asToken(id))), + mintToken(VANILLA_TOKEN, List.of(copyFromUtf8(FIRST_STRING_FOR_MINT))), + cryptoCreate(ACCOUNT).exposingCreatedIdTo(accountID::set), + cryptoCreate(RECEIVER).exposingCreatedIdTo(receiverID::set), + uploadInitCode(OUTER_CONTRACT, NESTED_CONTRACT), + contractCreate(NESTED_CONTRACT), + tokenAssociate(NESTED_CONTRACT, VANILLA_TOKEN), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + tokenAssociate(RECEIVER, VANILLA_TOKEN), + cryptoTransfer(movingUnique(VANILLA_TOKEN, 1L).between(TOKEN_TREASURY, ACCOUNT)) + .payingWith(GENESIS)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + OUTER_CONTRACT, asHeadlongAddress(getNestedContractAddress(NESTED_CONTRACT, spec))), + tokenAssociate(OUTER_CONTRACT, VANILLA_TOKEN), + newKeyNamed(CONTRACT_KEY).shape(CONTRACT_KEY_SHAPE.signedWith(sigs(ON, OUTER_CONTRACT))), + cryptoUpdate(ACCOUNT).key(CONTRACT_KEY), + contractCall( + OUTER_CONTRACT, + "transferDelegateCall", + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(receiverID.get())), + 1L) + .payingWith(GENESIS) + .via("delegateTransferCallWithContractKeyTxn") + .hasKnownStatus(ResponseCodeEnum.CONTRACT_REVERT_EXECUTED) + .gas(GAS_TO_OFFER)))) + .then( + childRecordsCheck( + "delegateTransferCallWithContractKeyTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .withStatus(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))), + getAccountBalance(ACCOUNT).hasTokenBalance(VANILLA_TOKEN, 1), + getAccountBalance(RECEIVER).hasTokenBalance(VANILLA_TOKEN, 0)); + } + + private HapiSpec burnTokenWithFullPrefixAndPartialPrefixKeys() { + final var firstBurnTxn = "firstBurnTxn"; + final var secondBurnTxn = "secondBurnTxn"; + final var amount = 99L; + final AtomicLong fungibleNum = new AtomicLong(); + + return propertyPreservingHapiSpec("burnTokenWithFullPrefixAndPartialPrefixKeys") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenBurn,TokenCreate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(ACCOUNT_NAME).balance(10 * ONE_HUNDRED_HBARS), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(TYPE_OF_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(100) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .exposingCreatedIdTo(idLit -> fungibleNum.set(asDotDelimitedLongArray(idLit)[2])), + uploadInitCode(ORDINARY_CALLS_CONTRACT), + contractCreate(ORDINARY_CALLS_CONTRACT)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + ORDINARY_CALLS_CONTRACT, + "burnTokenCall", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(TYPE_OF_TOKEN))), + BigInteger.ONE, + new long[0]) + .via(firstBurnTxn) + .payingWith(ACCOUNT_NAME) + .signedBy(MULTI_KEY) + .signedBy(ACCOUNT_NAME) + .hasKnownStatus(SUCCESS), + contractCall( + ORDINARY_CALLS_CONTRACT, + "burnTokenCall", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(TYPE_OF_TOKEN))), + BigInteger.ONE, + new long[0]) + .via(secondBurnTxn) + .payingWith(ACCOUNT_NAME) + .alsoSigningWithFullPrefix(MULTI_KEY) + .hasKnownStatus(SUCCESS)))) + .then( + childRecordsCheck( + firstBurnTxn, + SUCCESS, + recordWith() + .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_BURN) + .withStatus(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))), + childRecordsCheck( + secondBurnTxn, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_BURN) + .withStatus(SUCCESS) + .withTotalSupply(99))) + .newTotalSupply(99)), + getTokenInfo(TYPE_OF_TOKEN).hasTotalSupply(amount), + getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TYPE_OF_TOKEN, amount)); + } + + @Override + protected Logger getResultsLogger() { + return log; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysStillWorkAsExpectedSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysStillWorkAsExpectedSuite.java index 705605ef8467..65d9fc996091 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysStillWorkAsExpectedSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractKeysStillWorkAsExpectedSuite.java @@ -60,6 +60,7 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.streamMustInclude; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.contract.hapi.ContractCallSuite.PAY_RECEIVABLE_CONTRACT; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.MULTI_KEY; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE; @@ -87,7 +88,6 @@ public class ContractKeysStillWorkAsExpectedSuite extends HapiSuite { private static final Logger log = LogManager.getLogger(ContractKeysStillWorkAsExpectedSuite.class); private static final String EVM_ALIAS_ENABLED_PROP = "cryptoCreateWithAliasAndEvmAddress.enabled"; - public static final String CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS = "contracts.maxNumWithHapiSigsAccess"; public static void main(String... args) { new ContractKeysStillWorkAsExpectedSuite().runSuiteSync(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractMintHTSSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractMintHTSSuite.java index e29a28462047..a6f18c266662 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractMintHTSSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractMintHTSSuite.java @@ -16,19 +16,13 @@ package com.hedera.services.bdd.suites.contract.precompile; -import static com.hedera.services.bdd.spec.HapiPropertySource.asToken; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; -import static com.hedera.services.bdd.spec.assertions.AssertUtils.inOrder; import static com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts.resultWith; -import static com.hedera.services.bdd.spec.assertions.ContractLogAsserts.logWith; -import static com.hedera.services.bdd.spec.assertions.SomeFungibleTransfers.changingFungibleBalances; import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith; import static com.hedera.services.bdd.spec.keys.KeyShape.DELEGATE_CONTRACT; import static com.hedera.services.bdd.spec.keys.KeyShape.sigs; import static com.hedera.services.bdd.spec.keys.SigControl.ON; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenNftInfo; 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.contractCreate; @@ -42,49 +36,31 @@ import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; 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.assertionsHold; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.contract.Utils.asAddress; -import static com.hedera.services.bdd.suites.contract.Utils.parsedToByteString; +import static com.hedera.services.bdd.suites.contract.Utils.assertTxnRecordHasNoTraceabilityEnrichedContractFnResult; +import static com.hedera.services.bdd.suites.contract.Utils.expectedPrecompileGasFor; +import static com.hedera.services.bdd.suites.contract.Utils.getNestedContractAddress; import static com.hedera.services.bdd.suites.utils.contracts.FunctionParameters.functionParameters; import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; -import static com.hederahashgraph.api.proto.java.HederaFunctionality.ContractCall; import static com.hederahashgraph.api.proto.java.HederaFunctionality.TokenMint; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.*; -import static com.hederahashgraph.api.proto.java.SubType.DEFAULT; -import static com.hederahashgraph.api.proto.java.SubType.TOKEN_FUNGIBLE_COMMON; import static com.hederahashgraph.api.proto.java.SubType.TOKEN_NON_FUNGIBLE_UNIQUE; -import static org.junit.jupiter.api.Assertions.assertEquals; -import com.google.protobuf.ByteString; -import com.hedera.node.app.hapi.fees.pricing.AssetsLoader; import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.assertions.NonFungibleTransfers; import com.hedera.services.bdd.spec.keys.KeyShape; import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; -import com.hedera.services.bdd.spec.utilops.CustomSpecAssert; import com.hedera.services.bdd.suites.HapiSuite; import com.hedera.services.bdd.suites.utils.contracts.FunctionParameters; -import com.hederahashgraph.api.proto.java.HederaFunctionality; -import com.hederahashgraph.api.proto.java.SubType; -import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TokenSupplyType; import com.hederahashgraph.api.proto.java.TokenType; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.Arrays; import java.util.List; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; public class ContractMintHTSSuite extends HapiSuite { @@ -96,23 +72,17 @@ public class ContractMintHTSSuite extends HapiSuite { private static final KeyShape DELEGATE_CONTRACT_KEY_SHAPE = KeyShape.threshOf(1, KeyShape.SIMPLE, DELEGATE_CONTRACT); private static final String DELEGATE_KEY = "DelegateKey"; - private static final String CONTRACT_KEY = "ContractKey"; private static final String MULTI_KEY = "purpose"; public static final String MINT_CONTRACT = "MintContract"; public static final String MINT_NFT_CONTRACT = "MintNFTContract"; private static final String NESTED_MINT_CONTRACT = "NestedMint"; - private static final String HELLO_WORLD_MINT = "HelloWorldMint"; private static final String ACCOUNT = "anybody"; private static final String DELEGATE_CONTRACT_KEY_NAME = "contractKey"; private static final String FUNGIBLE_TOKEN = "fungibleToken"; - private static final String FIRST_MINT_TXN = "firstMintTxn"; - private static final String SECOND_MINT_TXN = "secondMintTxn"; private static final String NON_FUNGIBLE_TOKEN = "nonFungibleToken"; private static final String TEST_METADATA_1 = "Test metadata 1"; - private static final String TEST_METADATA_2 = "Test metadata 2"; private static final String RECIPIENT = "recipient"; - private static final String MINT_FUNGIBLE_TOKEN = "mintFungibleToken"; public static final String MINT_FUNGIBLE_TOKEN_WITH_EVENT = "mintFungibleTokenWithEvent"; public static void main(final String... args) { @@ -130,276 +100,11 @@ public List getSpecsInSuite() { } List negativeSpecs() { - return List.of( - rollbackOnFailedMintAfterFungibleTransfer(), - rollbackOnFailedAssociateAfterNonFungibleMint(), - gasCostNotMetSetsInsufficientGasStatusInChildRecord()); + return List.of(rollbackOnFailedMintAfterFungibleTransfer()); } List positiveSpecs() { - return List.of( - helloWorldFungibleMint(), - helloWorldNftMint(), - happyPathFungibleTokenMint(), - happyPathNonFungibleTokenMint(), - transferNftAfterNestedMint(), - happyPathZeroUnitFungibleTokenMint()); - } - - private HapiSpec happyPathZeroUnitFungibleTokenMint() { - final var amount = 0L; - final var gasUsed = 14085L; - final AtomicReference fungible = new AtomicReference<>(); - - return defaultHapiSpec("happyPathZeroUnitFungibleTokenMint") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).payingWith(GENESIS), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(FUNGIBLE_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(0) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .exposingCreatedIdTo(idLit -> fungible.set(asToken(idLit))), - uploadInitCode(MINT_CONTRACT), - sourcing(() -> contractCreate( - MINT_CONTRACT, HapiParserUtil.asHeadlongAddress(asAddress(fungible.get()))))) - .when( - contractCall(MINT_CONTRACT, MINT_FUNGIBLE_TOKEN_WITH_EVENT, BigInteger.valueOf(amount)) - .via(FIRST_MINT_TXN) - .gas(GAS_TO_OFFER) - .payingWith(ACCOUNT) - .alsoSigningWithFullPrefix(MULTI_KEY), - getTxnRecord(FIRST_MINT_TXN).andAllChildRecords().logged()) - .then(childRecordsCheck( - FIRST_MINT_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_MINT) - .withStatus(SUCCESS) - .withTotalSupply(0) - .withSerialNumbers()) - .gasUsed(gasUsed)) - .newTotalSupply(0))); - } - - private HapiSpec helloWorldFungibleMint() { - final var amount = 1_234_567L; - final AtomicReference fungible = new AtomicReference<>(); - - return defaultHapiSpec("HelloWorldFungibleMint") - .given( - newKeyNamed(MULTI_KEY), - tokenCreate(FUNGIBLE_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(0) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .exposingCreatedIdTo(idLit -> fungible.set(asToken(idLit))), - uploadInitCode(HELLO_WORLD_MINT)) - .when( - sourcing(() -> contractCreate( - HELLO_WORLD_MINT, HapiParserUtil.asHeadlongAddress(asAddress(fungible.get())))), - contractCall(HELLO_WORLD_MINT, "brrr", BigInteger.valueOf(amount)) - .via(FIRST_MINT_TXN) - .alsoSigningWithFullPrefix(MULTI_KEY), - getTxnRecord(FIRST_MINT_TXN).andAllChildRecords().logged(), - getTokenInfo(FUNGIBLE_TOKEN).hasTotalSupply(amount), - /* And now make the token contract-controlled so no explicit supply sig is required */ - newKeyNamed(CONTRACT_KEY).shape(DELEGATE_CONTRACT.signedWith(HELLO_WORLD_MINT)), - tokenUpdate(FUNGIBLE_TOKEN).supplyKey(CONTRACT_KEY), - getTokenInfo(FUNGIBLE_TOKEN).logged(), - contractCall(HELLO_WORLD_MINT, "brrr", BigInteger.valueOf(amount)) - .via(SECOND_MINT_TXN), - getTxnRecord(SECOND_MINT_TXN).andAllChildRecords().logged(), - getTokenInfo(FUNGIBLE_TOKEN).hasTotalSupply(2 * amount)) - .then(childRecordsCheck( - SECOND_MINT_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_MINT) - .withStatus(SUCCESS) - .withTotalSupply(2469134L) - .withSerialNumbers())) - .newTotalSupply(2469134L) - .tokenTransfers( - changingFungibleBalances().including(FUNGIBLE_TOKEN, DEFAULT_PAYER, amount)))); - } - - private HapiSpec helloWorldNftMint() { - final AtomicReference nonFungible = new AtomicReference<>(); - - return defaultHapiSpec("HelloWorldNftMint") - .given( - newKeyNamed(MULTI_KEY), - tokenCreate(NON_FUNGIBLE_TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .initialSupply(0) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .exposingCreatedIdTo(idLit -> nonFungible.set(asToken(idLit))), - uploadInitCode(HELLO_WORLD_MINT), - sourcing(() -> contractCreate( - HELLO_WORLD_MINT, HapiParserUtil.asHeadlongAddress(asAddress(nonFungible.get()))))) - .when( - contractCall(HELLO_WORLD_MINT, "mint") - .via(FIRST_MINT_TXN) - .gas(GAS_TO_OFFER) - .alsoSigningWithFullPrefix(MULTI_KEY), - getTxnRecord(FIRST_MINT_TXN).andAllChildRecords().logged(), - getTokenInfo(NON_FUNGIBLE_TOKEN).hasTotalSupply(1), - /* And now make the token contract-controlled so no explicit supply sig is required */ - newKeyNamed(CONTRACT_KEY).shape(DELEGATE_CONTRACT.signedWith(HELLO_WORLD_MINT)), - tokenUpdate(NON_FUNGIBLE_TOKEN).supplyKey(CONTRACT_KEY), - getTokenInfo(NON_FUNGIBLE_TOKEN).logged(), - contractCall(HELLO_WORLD_MINT, "mint") - .via(SECOND_MINT_TXN) - .gas(GAS_TO_OFFER), - getTxnRecord(SECOND_MINT_TXN).andAllChildRecords().logged()) - .then( - getTokenInfo(NON_FUNGIBLE_TOKEN).hasTotalSupply(2), - getTokenNftInfo(NON_FUNGIBLE_TOKEN, 2L).logged(), - childRecordsCheck( - FIRST_MINT_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_MINT) - .withStatus(SUCCESS) - .withTotalSupply(1) - .withSerialNumbers(1))) - .newTotalSupply(1) - .serialNos(List.of(1L))), - childRecordsCheck( - SECOND_MINT_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_MINT) - .withStatus(SUCCESS) - .withTotalSupply(2) - .withSerialNumbers(2))) - .newTotalSupply(2) - .serialNos(List.of(2L)))); - } - - private HapiSpec happyPathFungibleTokenMint() { - final var amount = 10L; - final var gasUsed = 14085L; - final AtomicReference fungible = new AtomicReference<>(); - - return defaultHapiSpec("FungibleMint") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).payingWith(GENESIS), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(FUNGIBLE_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(0) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .exposingCreatedIdTo(idLit -> fungible.set(asToken(idLit))), - uploadInitCode(MINT_CONTRACT), - sourcing(() -> contractCreate( - MINT_CONTRACT, HapiParserUtil.asHeadlongAddress(asAddress(fungible.get()))))) - .when( - contractCall(MINT_CONTRACT, MINT_FUNGIBLE_TOKEN_WITH_EVENT, BigInteger.valueOf(10)) - .via(FIRST_MINT_TXN) - .gas(GAS_TO_OFFER) - .payingWith(ACCOUNT) - .alsoSigningWithFullPrefix(MULTI_KEY), - getTxnRecord(FIRST_MINT_TXN).andAllChildRecords().logged(), - getTxnRecord(FIRST_MINT_TXN) - .hasPriority(recordWith() - .contractCallResult(resultWith() - .logs(inOrder(logWith() - .noData() - .withTopicsInOrder(List.of( - parsedToByteString(amount), parsedToByteString(0)))))))) - .then( - getTokenInfo(FUNGIBLE_TOKEN).hasTotalSupply(amount), - getAccountBalance(TOKEN_TREASURY).hasTokenBalance(FUNGIBLE_TOKEN, amount), - childRecordsCheck( - FIRST_MINT_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_MINT) - .withStatus(SUCCESS) - .withTotalSupply(10) - .withSerialNumbers()) - .gasUsed(gasUsed)) - .newTotalSupply(10))); - } - - private HapiSpec happyPathNonFungibleTokenMint() { - final var totalSupply = 2; - final AtomicReference nonFungible = new AtomicReference<>(); - - return defaultHapiSpec("NonFungibleMint") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(NON_FUNGIBLE_TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .supplyType(TokenSupplyType.INFINITE) - .initialSupply(0) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .exposingCreatedIdTo(idLit -> nonFungible.set(asToken(idLit))), - uploadInitCode(MINT_CONTRACT), - sourcing(() -> contractCreate( - MINT_CONTRACT, HapiParserUtil.asHeadlongAddress(asAddress(nonFungible.get()))))) - .when( - contractCall(MINT_CONTRACT, "mintNonFungibleTokenWithEvent", (Object) - new byte[][] {TEST_METADATA_1.getBytes(), TEST_METADATA_2.getBytes()}) - .via(FIRST_MINT_TXN) - .payingWith(ACCOUNT) - .gas(GAS_TO_OFFER) - .alsoSigningWithFullPrefix(MULTI_KEY), - getTxnRecord(FIRST_MINT_TXN).andAllChildRecords().logged(), - getTxnRecord(FIRST_MINT_TXN) - .hasPriority(recordWith() - .contractCallResult(resultWith() - .logs(inOrder(logWith() - .noData() - .withTopicsInOrder(List.of( - parsedToByteString(totalSupply), - parsedToByteString(1)))))))) - .then( - getTokenInfo(NON_FUNGIBLE_TOKEN).hasTotalSupply(totalSupply), - getAccountBalance(TOKEN_TREASURY).hasTokenBalance(NON_FUNGIBLE_TOKEN, totalSupply), - childRecordsCheck( - FIRST_MINT_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_MINT) - .withStatus(SUCCESS) - .withTotalSupply(2L) - .withSerialNumbers(1L, 2L)) - .gasUsed(704226L)) - .newTotalSupply(2) - .serialNos(Arrays.asList(1L, 2L)))); + return List.of(transferNftAfterNestedMint()); } private HapiSpec transferNftAfterNestedMint() { @@ -557,170 +262,8 @@ private HapiSpec rollbackOnFailedMintAfterFungibleTransfer() { .withSerialNumbers())))); } - private HapiSpec rollbackOnFailedAssociateAfterNonFungibleMint() { - final var nestedMintTxn = "nestedMintTxn"; - - return defaultHapiSpec("RollbackOnFailedAssociateAfterNonFungibleMint") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(ACCOUNT).balance(ONE_HUNDRED_HBARS), - cryptoCreate(RECIPIENT), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(NON_FUNGIBLE_TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .supplyType(TokenSupplyType.INFINITE) - .initialSupply(0) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY), - uploadInitCode(MINT_NFT_CONTRACT, NESTED_MINT_CONTRACT), - contractCreate(MINT_NFT_CONTRACT)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - NESTED_MINT_CONTRACT, - asHeadlongAddress(getNestedContractAddress(MINT_NFT_CONTRACT, spec)), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(NON_FUNGIBLE_TOKEN)))) - .gas(GAS_TO_OFFER), - newKeyNamed(DELEGATE_KEY) - .shape(DELEGATE_CONTRACT_KEY_SHAPE.signedWith(sigs(ON, NESTED_MINT_CONTRACT))), - cryptoUpdate(ACCOUNT).key(DELEGATE_KEY), - contractCall( - NESTED_MINT_CONTRACT, - "revertMintAfterFailedAssociate", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - new byte[][] {TEST_METADATA_1.getBytes()}) - .payingWith(GENESIS) - .alsoSigningWithFullPrefix(MULTI_KEY) - .via(nestedMintTxn) - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - getTxnRecord(nestedMintTxn).andAllChildRecords().logged()))) - .then( - getAccountBalance(TOKEN_TREASURY).hasTokenBalance(NON_FUNGIBLE_TOKEN, 0), - childRecordsCheck( - nestedMintTxn, - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(REVERTED_SUCCESS) - .newTotalSupply(0) - .serialNos(List.of()), - recordWith() - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_TOKEN_ID))))); - } - - private HapiSpec gasCostNotMetSetsInsufficientGasStatusInChildRecord() { - final var amount = 10L; - final var baselineMintWithEnoughGas = "baselineMintWithEnoughGas"; - - final AtomicLong expectedInsufficientGas = new AtomicLong(); - final AtomicReference fungible = new AtomicReference<>(); - - return defaultHapiSpec("gasCostNotMetSetsInsufficientGasStatusInChildRecord") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(ACCOUNT).balance(5 * ONE_HUNDRED_HBARS), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(FUNGIBLE_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(0) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .exposingCreatedIdTo(idLit -> fungible.set(asToken(idLit)))) - .when(uploadInitCode(MINT_CONTRACT), sourcing(() -> contractCreate( - MINT_CONTRACT, HapiParserUtil.asHeadlongAddress(asAddress(fungible.get()))) - .payingWith(ACCOUNT) - .gas(GAS_TO_OFFER))) - .then( - contractCall(MINT_CONTRACT, MINT_FUNGIBLE_TOKEN, BigInteger.valueOf(amount)) - .via(baselineMintWithEnoughGas) - .payingWith(ACCOUNT) - .alsoSigningWithFullPrefix(MULTI_KEY) - .gas(64_000L), - withOpContext((spec, opLog) -> { - final var expectedPrecompileGas = - expectedPrecompileGasFor(spec, TokenMint, TOKEN_FUNGIBLE_COMMON); - final var baselineCostLookup = getTxnRecord(baselineMintWithEnoughGas) - .andAllChildRecords() - .logged() - .assertingNothing(); - allRunFor(spec, baselineCostLookup); - final var baselineGas = baselineCostLookup - .getResponseRecord() - .getContractCallResult() - .getGasUsed(); - expectedInsufficientGas.set(baselineGas - expectedPrecompileGas); - }), - sourcing(() -> contractCall(MINT_CONTRACT, MINT_FUNGIBLE_TOKEN, BigInteger.valueOf(amount)) - .via(FIRST_MINT_TXN) - .payingWith(ACCOUNT) - .alsoSigningWithFullPrefix(MULTI_KEY) - .gas(expectedInsufficientGas.get()) - .hasKnownStatus(INSUFFICIENT_GAS)), - getTxnRecord(FIRST_MINT_TXN).andAllChildRecords().logged(), - getTokenInfo(FUNGIBLE_TOKEN).hasTotalSupply(amount), - getAccountBalance(TOKEN_TREASURY).hasTokenBalance(FUNGIBLE_TOKEN, amount), - childRecordsCheck( - FIRST_MINT_TXN, - INSUFFICIENT_GAS, - recordWith() - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_MINT) - .withStatus(INSUFFICIENT_GAS) - .withTotalSupply(0L) - .withSerialNumbers())))); - } - - private long expectedPrecompileGasFor(final HapiSpec spec, final HederaFunctionality function, final SubType type) { - final var gasThousandthsOfTinycentPrice = spec.fees() - .getCurrentOpFeeData() - .get(ContractCall) - .get(DEFAULT) - .getServicedata() - .getGas(); - final var assetsLoader = new AssetsLoader(); - final BigDecimal hapiUsdPrice; - try { - hapiUsdPrice = assetsLoader.loadCanonicalPrices().get(function).get(type); - } catch (final IOException e) { - throw new UncheckedIOException(e); - } - final var precompileTinycentPrice = hapiUsdPrice - .multiply(BigDecimal.valueOf(1.2)) - .multiply(BigDecimal.valueOf(100 * 100_000_000L)) - .longValueExact(); - return (precompileTinycentPrice * 1000 / gasThousandthsOfTinycentPrice); - } - - @NotNull - private String getNestedContractAddress(final String contract, final HapiSpec spec) { - return AssociatePrecompileSuite.getNestedContractAddress(contract, spec); - } - @Override protected Logger getResultsLogger() { return LOG; } - - @NotNull - @SuppressWarnings("java:S5960") - private CustomSpecAssert assertTxnRecordHasNoTraceabilityEnrichedContractFnResult(final String nestedTransferTxn) { - return assertionsHold((spec, log) -> { - final var subOp = getTxnRecord(nestedTransferTxn); - allRunFor(spec, subOp); - - final var rcd = subOp.getResponseRecord(); - - final var contractCallResult = rcd.getContractCallResult(); - assertEquals(0L, contractCallResult.getGas(), "Result not expected to externalize gas"); - assertEquals(0L, contractCallResult.getAmount(), "Result not expected to externalize amount"); - assertEquals(ByteString.EMPTY, contractCallResult.getFunctionParameters()); - }); - } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractMintHTSV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractMintHTSV1SecurityModelSuite.java new file mode 100644 index 000000000000..2b90c1b6afa2 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ContractMintHTSV1SecurityModelSuite.java @@ -0,0 +1,518 @@ +/* + * Copyright (C) 2021-2023 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.spec.HapiPropertySource.asToken; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +import static com.hedera.services.bdd.spec.assertions.AssertUtils.inOrder; +import static com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts.resultWith; +import static com.hedera.services.bdd.spec.assertions.ContractLogAsserts.logWith; +import static com.hedera.services.bdd.spec.assertions.SomeFungibleTransfers.changingFungibleBalances; +import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith; +import static com.hedera.services.bdd.spec.keys.KeyShape.DELEGATE_CONTRACT; +import static com.hedera.services.bdd.spec.keys.KeyShape.sigs; +import static com.hedera.services.bdd.spec.keys.SigControl.ON; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenNftInfo; +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.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoUpdate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenUpdate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.expectedPrecompileGasFor; +import static com.hedera.services.bdd.suites.contract.Utils.getNestedContractAddress; +import static com.hedera.services.bdd.suites.contract.Utils.parsedToByteString; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.HederaFunctionality.TokenMint; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.*; +import static com.hederahashgraph.api.proto.java.SubType.TOKEN_FUNGIBLE_COMMON; + +import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.keys.KeyShape; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.TokenID; +import com.hederahashgraph.api.proto.java.TokenSupplyType; +import com.hederahashgraph.api.proto.java.TokenType; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse +public class ContractMintHTSV1SecurityModelSuite extends HapiSuite { + + private static final Logger LOG = LogManager.getLogger(ContractMintHTSV1SecurityModelSuite.class); + + private static final long GAS_TO_OFFER = 4_000_000L; + private static final String TOKEN_TREASURY = "treasury"; + private static final KeyShape DELEGATE_CONTRACT_KEY_SHAPE = + KeyShape.threshOf(1, KeyShape.SIMPLE, DELEGATE_CONTRACT); + private static final String DELEGATE_KEY = "DelegateKey"; + private static final String CONTRACT_KEY = "ContractKey"; + private static final String MULTI_KEY = "purpose"; + public static final String MINT_CONTRACT = "MintContract"; + public static final String MINT_NFT_CONTRACT = "MintNFTContract"; + private static final String NESTED_MINT_CONTRACT = "NestedMint"; + private static final String HELLO_WORLD_MINT = "HelloWorldMint"; + private static final String ACCOUNT = "anybody"; + private static final String FUNGIBLE_TOKEN = "fungibleToken"; + private static final String FIRST_MINT_TXN = "firstMintTxn"; + private static final String SECOND_MINT_TXN = "secondMintTxn"; + private static final String NON_FUNGIBLE_TOKEN = "nonFungibleToken"; + private static final String TEST_METADATA_1 = "Test metadata 1"; + private static final String TEST_METADATA_2 = "Test metadata 2"; + private static final String RECIPIENT = "recipient"; + private static final String MINT_FUNGIBLE_TOKEN = "mintFungibleToken"; + public static final String MINT_FUNGIBLE_TOKEN_WITH_EVENT = "mintFungibleTokenWithEvent"; + + public static void main(final String... args) { + new ContractMintHTSV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public List getSpecsInSuite() { + return allOf(positiveSpecs(), negativeSpecs()); + } + + List negativeSpecs() { + return List.of( + rollbackOnFailedAssociateAfterNonFungibleMint(), gasCostNotMetSetsInsufficientGasStatusInChildRecord()); + } + + List positiveSpecs() { + return List.of( + helloWorldFungibleMint(), + helloWorldNftMint(), + happyPathFungibleTokenMint(), + happyPathNonFungibleTokenMint(), + happyPathZeroUnitFungibleTokenMint()); + } + + private HapiSpec happyPathZeroUnitFungibleTokenMint() { + final var amount = 0L; + final var gasUsed = 14085L; + final AtomicReference fungible = new AtomicReference<>(); + + return propertyPreservingHapiSpec("happyPathZeroUnitFungibleTokenMint") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).payingWith(GENESIS), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(FUNGIBLE_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(0) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .exposingCreatedIdTo(idLit -> fungible.set(asToken(idLit))), + uploadInitCode(MINT_CONTRACT), + sourcing(() -> contractCreate( + MINT_CONTRACT, HapiParserUtil.asHeadlongAddress(asAddress(fungible.get()))))) + .when( + contractCall(MINT_CONTRACT, MINT_FUNGIBLE_TOKEN_WITH_EVENT, BigInteger.valueOf(amount)) + .via(FIRST_MINT_TXN) + .gas(GAS_TO_OFFER) + .payingWith(ACCOUNT) + .alsoSigningWithFullPrefix(MULTI_KEY), + getTxnRecord(FIRST_MINT_TXN).andAllChildRecords().logged()) + .then(childRecordsCheck( + FIRST_MINT_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_MINT) + .withStatus(SUCCESS) + .withTotalSupply(0) + .withSerialNumbers()) + .gasUsed(gasUsed)) + .newTotalSupply(0))); + } + + private HapiSpec helloWorldFungibleMint() { + final var amount = 1_234_567L; + final AtomicReference fungible = new AtomicReference<>(); + + return propertyPreservingHapiSpec("helloWorldFungibleMint") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + tokenCreate(FUNGIBLE_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(0) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .exposingCreatedIdTo(idLit -> fungible.set(asToken(idLit))), + uploadInitCode(HELLO_WORLD_MINT)) + .when( + sourcing(() -> contractCreate( + HELLO_WORLD_MINT, HapiParserUtil.asHeadlongAddress(asAddress(fungible.get())))), + contractCall(HELLO_WORLD_MINT, "brrr", BigInteger.valueOf(amount)) + .via(FIRST_MINT_TXN) + .alsoSigningWithFullPrefix(MULTI_KEY), + getTxnRecord(FIRST_MINT_TXN).andAllChildRecords().logged(), + getTokenInfo(FUNGIBLE_TOKEN).hasTotalSupply(amount), + /* And now make the token contract-controlled so no explicit supply sig is required */ + newKeyNamed(CONTRACT_KEY).shape(DELEGATE_CONTRACT.signedWith(HELLO_WORLD_MINT)), + tokenUpdate(FUNGIBLE_TOKEN).supplyKey(CONTRACT_KEY), + getTokenInfo(FUNGIBLE_TOKEN).logged(), + contractCall(HELLO_WORLD_MINT, "brrr", BigInteger.valueOf(amount)) + .via(SECOND_MINT_TXN), + getTxnRecord(SECOND_MINT_TXN).andAllChildRecords().logged(), + getTokenInfo(FUNGIBLE_TOKEN).hasTotalSupply(2 * amount)) + .then(childRecordsCheck( + SECOND_MINT_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_MINT) + .withStatus(SUCCESS) + .withTotalSupply(2469134L) + .withSerialNumbers())) + .newTotalSupply(2469134L) + .tokenTransfers( + changingFungibleBalances().including(FUNGIBLE_TOKEN, DEFAULT_PAYER, amount)))); + } + + private HapiSpec helloWorldNftMint() { + final AtomicReference nonFungible = new AtomicReference<>(); + + return propertyPreservingHapiSpec("helloWorldNftMint") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + tokenCreate(NON_FUNGIBLE_TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .initialSupply(0) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .exposingCreatedIdTo(idLit -> nonFungible.set(asToken(idLit))), + uploadInitCode(HELLO_WORLD_MINT), + sourcing(() -> contractCreate( + HELLO_WORLD_MINT, HapiParserUtil.asHeadlongAddress(asAddress(nonFungible.get()))))) + .when( + contractCall(HELLO_WORLD_MINT, "mint") + .via(FIRST_MINT_TXN) + .gas(GAS_TO_OFFER) + .alsoSigningWithFullPrefix(MULTI_KEY), + getTxnRecord(FIRST_MINT_TXN).andAllChildRecords().logged(), + getTokenInfo(NON_FUNGIBLE_TOKEN).hasTotalSupply(1), + /* And now make the token contract-controlled so no explicit supply sig is required */ + newKeyNamed(CONTRACT_KEY).shape(DELEGATE_CONTRACT.signedWith(HELLO_WORLD_MINT)), + tokenUpdate(NON_FUNGIBLE_TOKEN).supplyKey(CONTRACT_KEY), + getTokenInfo(NON_FUNGIBLE_TOKEN).logged(), + contractCall(HELLO_WORLD_MINT, "mint") + .via(SECOND_MINT_TXN) + .gas(GAS_TO_OFFER), + getTxnRecord(SECOND_MINT_TXN).andAllChildRecords().logged()) + .then( + getTokenInfo(NON_FUNGIBLE_TOKEN).hasTotalSupply(2), + getTokenNftInfo(NON_FUNGIBLE_TOKEN, 2L).logged(), + childRecordsCheck( + FIRST_MINT_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_MINT) + .withStatus(SUCCESS) + .withTotalSupply(1) + .withSerialNumbers(1))) + .newTotalSupply(1) + .serialNos(List.of(1L))), + childRecordsCheck( + SECOND_MINT_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_MINT) + .withStatus(SUCCESS) + .withTotalSupply(2) + .withSerialNumbers(2))) + .newTotalSupply(2) + .serialNos(List.of(2L)))); + } + + private HapiSpec happyPathFungibleTokenMint() { + final var amount = 10L; + final var gasUsed = 14085L; + final AtomicReference fungible = new AtomicReference<>(); + + return propertyPreservingHapiSpec("happyPathFungibleTokenMint") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).payingWith(GENESIS), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(FUNGIBLE_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(0) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .exposingCreatedIdTo(idLit -> fungible.set(asToken(idLit))), + uploadInitCode(MINT_CONTRACT), + sourcing(() -> contractCreate( + MINT_CONTRACT, HapiParserUtil.asHeadlongAddress(asAddress(fungible.get()))))) + .when( + contractCall(MINT_CONTRACT, MINT_FUNGIBLE_TOKEN_WITH_EVENT, BigInteger.valueOf(10)) + .via(FIRST_MINT_TXN) + .gas(GAS_TO_OFFER) + .payingWith(ACCOUNT) + .alsoSigningWithFullPrefix(MULTI_KEY), + getTxnRecord(FIRST_MINT_TXN).andAllChildRecords().logged(), + getTxnRecord(FIRST_MINT_TXN) + .hasPriority(recordWith() + .contractCallResult(resultWith() + .logs(inOrder(logWith() + .noData() + .withTopicsInOrder(List.of( + parsedToByteString(amount), parsedToByteString(0)))))))) + .then( + getTokenInfo(FUNGIBLE_TOKEN).hasTotalSupply(amount), + getAccountBalance(TOKEN_TREASURY).hasTokenBalance(FUNGIBLE_TOKEN, amount), + childRecordsCheck( + FIRST_MINT_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_MINT) + .withStatus(SUCCESS) + .withTotalSupply(10) + .withSerialNumbers()) + .gasUsed(gasUsed)) + .newTotalSupply(10))); + } + + private HapiSpec happyPathNonFungibleTokenMint() { + final var totalSupply = 2; + final AtomicReference nonFungible = new AtomicReference<>(); + + return propertyPreservingHapiSpec("happyPathNonFungibleTokenMint") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(NON_FUNGIBLE_TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyType(TokenSupplyType.INFINITE) + .initialSupply(0) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .exposingCreatedIdTo(idLit -> nonFungible.set(asToken(idLit))), + uploadInitCode(MINT_CONTRACT), + sourcing(() -> contractCreate( + MINT_CONTRACT, HapiParserUtil.asHeadlongAddress(asAddress(nonFungible.get()))))) + .when( + contractCall(MINT_CONTRACT, "mintNonFungibleTokenWithEvent", (Object) + new byte[][] {TEST_METADATA_1.getBytes(), TEST_METADATA_2.getBytes()}) + .via(FIRST_MINT_TXN) + .payingWith(ACCOUNT) + .gas(GAS_TO_OFFER) + .alsoSigningWithFullPrefix(MULTI_KEY), + getTxnRecord(FIRST_MINT_TXN).andAllChildRecords().logged(), + getTxnRecord(FIRST_MINT_TXN) + .hasPriority(recordWith() + .contractCallResult(resultWith() + .logs(inOrder(logWith() + .noData() + .withTopicsInOrder(List.of( + parsedToByteString(totalSupply), + parsedToByteString(1)))))))) + .then( + getTokenInfo(NON_FUNGIBLE_TOKEN).hasTotalSupply(totalSupply), + getAccountBalance(TOKEN_TREASURY).hasTokenBalance(NON_FUNGIBLE_TOKEN, totalSupply), + childRecordsCheck( + FIRST_MINT_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_MINT) + .withStatus(SUCCESS) + .withTotalSupply(2L) + .withSerialNumbers(1L, 2L)) + .gasUsed(704226L)) + .newTotalSupply(2) + .serialNos(Arrays.asList(1L, 2L)))); + } + + private HapiSpec rollbackOnFailedAssociateAfterNonFungibleMint() { + final var nestedMintTxn = "nestedMintTxn"; + + return propertyPreservingHapiSpec("rollbackOnFailedAssociateAfterNonFungibleMint") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(ACCOUNT).balance(ONE_HUNDRED_HBARS), + cryptoCreate(RECIPIENT), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(NON_FUNGIBLE_TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyType(TokenSupplyType.INFINITE) + .initialSupply(0) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY), + uploadInitCode(MINT_NFT_CONTRACT, NESTED_MINT_CONTRACT), + contractCreate(MINT_NFT_CONTRACT)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + NESTED_MINT_CONTRACT, + asHeadlongAddress(getNestedContractAddress(MINT_NFT_CONTRACT, spec)), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(NON_FUNGIBLE_TOKEN)))) + .gas(GAS_TO_OFFER), + newKeyNamed(DELEGATE_KEY) + .shape(DELEGATE_CONTRACT_KEY_SHAPE.signedWith(sigs(ON, NESTED_MINT_CONTRACT))), + cryptoUpdate(ACCOUNT).key(DELEGATE_KEY), + contractCall( + NESTED_MINT_CONTRACT, + "revertMintAfterFailedAssociate", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + new byte[][] {TEST_METADATA_1.getBytes()}) + .payingWith(GENESIS) + .alsoSigningWithFullPrefix(MULTI_KEY) + .via(nestedMintTxn) + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + getTxnRecord(nestedMintTxn).andAllChildRecords().logged()))) + .then( + getAccountBalance(TOKEN_TREASURY).hasTokenBalance(NON_FUNGIBLE_TOKEN, 0), + childRecordsCheck( + nestedMintTxn, + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(REVERTED_SUCCESS) + .newTotalSupply(0) + .serialNos(List.of()), + recordWith() + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_TOKEN_ID))))); + } + + private HapiSpec gasCostNotMetSetsInsufficientGasStatusInChildRecord() { + final var amount = 10L; + final var baselineMintWithEnoughGas = "baselineMintWithEnoughGas"; + + final AtomicLong expectedInsufficientGas = new AtomicLong(); + final AtomicReference fungible = new AtomicReference<>(); + + return propertyPreservingHapiSpec("gasCostNotMetSetsInsufficientGasStatusInChildRecord") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(ACCOUNT).balance(5 * ONE_HUNDRED_HBARS), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(FUNGIBLE_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(0) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .exposingCreatedIdTo(idLit -> fungible.set(asToken(idLit)))) + .when(uploadInitCode(MINT_CONTRACT), sourcing(() -> contractCreate( + MINT_CONTRACT, HapiParserUtil.asHeadlongAddress(asAddress(fungible.get()))) + .payingWith(ACCOUNT) + .gas(GAS_TO_OFFER))) + .then( + contractCall(MINT_CONTRACT, MINT_FUNGIBLE_TOKEN, BigInteger.valueOf(amount)) + .via(baselineMintWithEnoughGas) + .payingWith(ACCOUNT) + .alsoSigningWithFullPrefix(MULTI_KEY) + .gas(64_000L), + withOpContext((spec, opLog) -> { + final var expectedPrecompileGas = + expectedPrecompileGasFor(spec, TokenMint, TOKEN_FUNGIBLE_COMMON); + final var baselineCostLookup = getTxnRecord(baselineMintWithEnoughGas) + .andAllChildRecords() + .logged() + .assertingNothing(); + allRunFor(spec, baselineCostLookup); + final var baselineGas = baselineCostLookup + .getResponseRecord() + .getContractCallResult() + .getGasUsed(); + expectedInsufficientGas.set(baselineGas - expectedPrecompileGas); + }), + sourcing(() -> contractCall(MINT_CONTRACT, MINT_FUNGIBLE_TOKEN, BigInteger.valueOf(amount)) + .via(FIRST_MINT_TXN) + .payingWith(ACCOUNT) + .alsoSigningWithFullPrefix(MULTI_KEY) + .gas(expectedInsufficientGas.get()) + .hasKnownStatus(INSUFFICIENT_GAS)), + getTxnRecord(FIRST_MINT_TXN).andAllChildRecords().logged(), + getTokenInfo(FUNGIBLE_TOKEN).hasTotalSupply(amount), + getAccountBalance(TOKEN_TREASURY).hasTokenBalance(FUNGIBLE_TOKEN, amount), + childRecordsCheck( + FIRST_MINT_TXN, + INSUFFICIENT_GAS, + recordWith() + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_MINT) + .withStatus(INSUFFICIENT_GAS) + .withTotalSupply(0L) + .withSerialNumbers())))); + } + + @Override + protected Logger getResultsLogger() { + return LOG; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CreatePrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CreatePrecompileSuite.java index b62075922980..1e84f8b4717e 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CreatePrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CreatePrecompileSuite.java @@ -16,56 +16,40 @@ package com.hedera.services.bdd.suites.contract.precompile; -import static com.hedera.services.bdd.spec.HapiPropertySource.asTokenString; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; import static com.hedera.services.bdd.spec.assertions.AccountInfoAsserts.changeFromSnapshot; -import static com.hedera.services.bdd.spec.keys.KeyShape.CONTRACT; -import static com.hedera.services.bdd.spec.keys.KeyShape.DELEGATE_CONTRACT; import static com.hedera.services.bdd.spec.keys.KeyShape.ED25519; import static com.hedera.services.bdd.spec.keys.KeyShape.SECP256K1; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; 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.contractCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.balanceSnapshot; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.emptyChildRecordsCheck; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.contract.Utils.asAddress; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_IS_TREASURY; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_TX_FEE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_ACCOUNT_ID; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_EXPIRATION_TIME; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.MISSING_TOKEN_SYMBOL; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; -import com.esaulpaugh.headlong.abi.Address; import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts; -import com.hedera.services.bdd.spec.assertions.ContractInfoAsserts; import com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts; import com.hedera.services.bdd.spec.transactions.contract.HapiEthereumCall; import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; import com.hedera.services.bdd.suites.HapiSuite; import com.hedera.services.bdd.suites.contract.Utils; import com.hederahashgraph.api.proto.java.ResponseCodeEnum; -import com.hederahashgraph.api.proto.java.TokenFreezeStatus; -import com.hederahashgraph.api.proto.java.TokenID; -import com.hederahashgraph.api.proto.java.TokenPauseStatus; -import com.hederahashgraph.api.proto.java.TokenSupplyType; -import com.hederahashgraph.api.proto.java.TokenType; import java.util.List; -import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.lang3.ArrayUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -97,7 +81,6 @@ public class CreatePrecompileSuite extends HapiSuite { public static final String ED25519KEY = "ed25519key"; public static final String ECDSA_KEY = "ecdsa"; public static final String EXISTING_TOKEN = "EXISTING_TOKEN"; - private static final String AUTO_RENEW_ACCOUNT = "autoRenewAccount"; public static final String EXPLICIT_CREATE_RESULT = "Explicit create result is {}"; private static final String CREATE_NFT_WITH_KEYS_AND_EXPIRY_FUNCTION = "createNFTTokenWithKeysAndExpiry"; @@ -118,13 +101,8 @@ public List getSpecsInSuite() { List positiveSpecs() { return List.of( - fungibleTokenCreateHappyPath(), - nonFungibleTokenCreateHappyPath(), - fungibleTokenCreateThenQueryAndTransfer(), - nonFungibleTokenCreateThenQuery(), - inheritsSenderAutoRenewAccountIfAnyForNftCreate(), - inheritsSenderAutoRenewAccountForTokenCreate(), - createTokenWithDefaultExpiryAndEmptyKeys()); + // TODO: where are the security model V2 _positive_ tests? + ); } List negativeSpecs() { @@ -139,479 +117,6 @@ List negativeSpecs() { delegateCallTokenCreateFails()); } - // TEST-001 - private HapiSpec fungibleTokenCreateHappyPath() { - final var tokenCreateContractAsKeyDelegate = "tokenCreateContractAsKeyDelegate"; - final var createTokenNum = new AtomicLong(); - return defaultHapiSpec("fungibleTokenCreateHappyPath") - .given( - newKeyNamed(ED25519KEY).shape(ED25519), - newKeyNamed(ECDSA_KEY).shape(SECP256K1), - newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), - newKeyNamed(CONTRACT_ADMIN_KEY), - cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(ED25519KEY), - cryptoCreate(AUTO_RENEW_ACCOUNT) - .balance(ONE_HUNDRED_HBARS) - .key(ED25519KEY), - cryptoCreate(ACCOUNT_2).balance(ONE_HUNDRED_HBARS).key(ECDSA_KEY), - cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), - uploadInitCode(TOKEN_CREATE_CONTRACT), - contractCreate(TOKEN_CREATE_CONTRACT) - .gas(GAS_TO_OFFER) - .adminKey(CONTRACT_ADMIN_KEY) - .autoRenewAccountId(AUTO_RENEW_ACCOUNT) - .signedBy(CONTRACT_ADMIN_KEY, DEFAULT_PAYER, AUTO_RENEW_ACCOUNT), - getContractInfo(TOKEN_CREATE_CONTRACT) - .has(ContractInfoAsserts.contractWith().autoRenewAccountId(AUTO_RENEW_ACCOUNT)) - .logged()) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_CREATE_CONTRACT, - "createTokenWithKeysAndExpiry", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - AUTO_RENEW_PERIOD, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT_TO_ASSOCIATE)))) - .via(FIRST_CREATE_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .payingWith(ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT_TO_ASSOCIATE_KEY) - .refusingEthConversion() - .exposingResultTo(result -> { - log.info(EXPLICIT_CREATE_RESULT, result[0]); - final var res = (Address) result[0]; - createTokenNum.set(res.value().longValueExact()); - }), - newKeyNamed(TOKEN_CREATE_CONTRACT_AS_KEY).shape(CONTRACT.signedWith(TOKEN_CREATE_CONTRACT)), - newKeyNamed(tokenCreateContractAsKeyDelegate) - .shape(DELEGATE_CONTRACT.signedWith(TOKEN_CREATE_CONTRACT))))) - .then( - getTxnRecord(FIRST_CREATE_TXN).andAllChildRecords().logged(), - getAccountBalance(ACCOUNT).logged(), - getAccountBalance(TOKEN_CREATE_CONTRACT).logged(), - getContractInfo(TOKEN_CREATE_CONTRACT).logged(), - childRecordsCheck( - FIRST_CREATE_TXN, - ResponseCodeEnum.SUCCESS, - TransactionRecordAsserts.recordWith().status(ResponseCodeEnum.SUCCESS), - TransactionRecordAsserts.recordWith().status(ResponseCodeEnum.SUCCESS), - TransactionRecordAsserts.recordWith().status(ResponseCodeEnum.SUCCESS)), - sourcing(() -> - getAccountInfo(ACCOUNT_TO_ASSOCIATE).logged().hasTokenRelationShipCount(1)), - sourcing(() -> getTokenInfo(asTokenString(TokenID.newBuilder() - .setTokenNum(createTokenNum.get()) - .build())) - .logged() - .hasTokenType(TokenType.FUNGIBLE_COMMON) - .hasSymbol(TOKEN_SYMBOL) - .hasName(TOKEN_NAME) - .hasDecimals(8) - .hasTotalSupply(100) - .hasEntityMemo(MEMO) - .hasTreasury(ACCOUNT) - // Token doesn't inherit contract's auto-renew - // account if set in tokenCreate - .hasAutoRenewAccount(ACCOUNT) - .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) - .hasSupplyType(TokenSupplyType.INFINITE) - .searchKeysGlobally() - .hasAdminKey(ED25519KEY) - .hasKycKey(ED25519KEY) - .hasFreezeKey(ECDSA_KEY) - .hasWipeKey(ECDSA_KEY) - .hasSupplyKey(TOKEN_CREATE_CONTRACT_AS_KEY) - .hasFeeScheduleKey(tokenCreateContractAsKeyDelegate) - .hasPauseKey(CONTRACT_ADMIN_KEY) - .hasPauseStatus(TokenPauseStatus.Unpaused)), - cryptoDelete(ACCOUNT).hasKnownStatus(ACCOUNT_IS_TREASURY)); - } - - // TEST-002 - - private HapiSpec inheritsSenderAutoRenewAccountIfAnyForNftCreate() { - final var createdNftTokenNum = new AtomicLong(); - return defaultHapiSpec("inheritsSenderAutoRenewAccountIfAnyForNftCreate") - .given( - newKeyNamed(ED25519KEY).shape(ED25519), - newKeyNamed(ECDSA_KEY), - cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(ED25519KEY), - cryptoCreate(AUTO_RENEW_ACCOUNT) - .balance(ONE_HUNDRED_HBARS) - .key(ED25519KEY), - uploadInitCode(TOKEN_CREATE_CONTRACT)) - .when( - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate(TOKEN_CREATE_CONTRACT) - .autoRenewAccountId(AUTO_RENEW_ACCOUNT) - .gas(GAS_TO_OFFER))), - getContractInfo(TOKEN_CREATE_CONTRACT) - .has(ContractInfoAsserts.contractWith().autoRenewAccountId(AUTO_RENEW_ACCOUNT)) - .logged()) - .then(withOpContext((spec, ignore) -> { - final var subop1 = balanceSnapshot(ACCOUNT_BALANCE, ACCOUNT); - final var subop2 = contractCall( - TOKEN_CREATE_CONTRACT, - CREATE_NFT_WITH_KEYS_AND_EXPIRY_FUNCTION, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - HapiParserUtil.asHeadlongAddress(new byte[20]), - AUTO_RENEW_PERIOD) - .via(FIRST_CREATE_TXN) - .gas(GAS_TO_OFFER) - .payingWith(ACCOUNT) - .sending(DEFAULT_AMOUNT_TO_SEND) - .refusingEthConversion() - .exposingResultTo(result -> { - log.info("Explicit create result is" + " {}", result[0]); - final var res = (Address) result[0]; - createdNftTokenNum.set(res.value().longValueExact()); - }) - .hasKnownStatus(SUCCESS); - - allRunFor( - spec, - subop1, - subop2, - childRecordsCheck( - FIRST_CREATE_TXN, - SUCCESS, - TransactionRecordAsserts.recordWith().status(SUCCESS))); - - final var nftInfo = getTokenInfo(asTokenString(TokenID.newBuilder() - .setTokenNum(createdNftTokenNum.get()) - .build())) - .hasAutoRenewAccount(AUTO_RENEW_ACCOUNT) - .logged(); - - allRunFor(spec, nftInfo); - })); - } - - private HapiSpec inheritsSenderAutoRenewAccountForTokenCreate() { - final var createTokenNum = new AtomicLong(); - return defaultHapiSpec("inheritsSenderAutoRenewAccountForTokenCreate") - .given( - newKeyNamed(ED25519KEY).shape(ED25519), - newKeyNamed(ECDSA_KEY).shape(SECP256K1), - newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), - newKeyNamed(CONTRACT_ADMIN_KEY), - cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(ED25519KEY), - cryptoCreate(AUTO_RENEW_ACCOUNT) - .balance(ONE_HUNDRED_HBARS) - .key(ED25519KEY), - cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), - uploadInitCode(TOKEN_CREATE_CONTRACT), - contractCreate(TOKEN_CREATE_CONTRACT) - .gas(GAS_TO_OFFER) - .adminKey(CONTRACT_ADMIN_KEY) - .autoRenewAccountId(AUTO_RENEW_ACCOUNT) // inherits if the tokenCreateOp doesn't - // have - // autoRenewAccount - .signedBy(CONTRACT_ADMIN_KEY, DEFAULT_PAYER, AUTO_RENEW_ACCOUNT), - getContractInfo(TOKEN_CREATE_CONTRACT) - .has(ContractInfoAsserts.contractWith().autoRenewAccountId(AUTO_RENEW_ACCOUNT)) - .logged()) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_CREATE_CONTRACT, - "createTokenWithKeysAndExpiry", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), - HapiParserUtil.asHeadlongAddress(new byte[20]), // set empty - // autoRenewAccount - AUTO_RENEW_PERIOD, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT_TO_ASSOCIATE)))) - .via(FIRST_CREATE_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .payingWith(ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT_TO_ASSOCIATE_KEY) - .refusingEthConversion() - .exposingResultTo(result -> { - log.info(EXPLICIT_CREATE_RESULT, result[0]); - final var res = (Address) result[0]; - createTokenNum.set(res.value().longValueExact()); - })))) - .then(sourcing(() -> getTokenInfo(asTokenString(TokenID.newBuilder() - .setTokenNum(createTokenNum.get()) - .build())) - .logged() - .hasAutoRenewAccount(AUTO_RENEW_ACCOUNT) - .hasPauseStatus(TokenPauseStatus.Unpaused))); - } - - // TEST-003 & TEST-019 - private HapiSpec nonFungibleTokenCreateHappyPath() { - final var createdTokenNum = new AtomicLong(); - return defaultHapiSpec("nonFungibleTokenCreateHappyPath") - .given( - newKeyNamed(ED25519KEY).shape(ED25519), - cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(ED25519KEY), - uploadInitCode(TOKEN_CREATE_CONTRACT), - getAccountInfo(DEFAULT_CONTRACT_SENDER).savingSnapshot(DEFAULT_CONTRACT_SENDER)) - .when(withOpContext((spec, opLog) -> - allRunFor(spec, contractCreate(TOKEN_CREATE_CONTRACT).gas(GAS_TO_OFFER)))) - .then(withOpContext((spec, ignore) -> { - final var subop1 = balanceSnapshot(ACCOUNT_BALANCE, ACCOUNT); - final var subop2 = contractCall( - TOKEN_CREATE_CONTRACT, - CREATE_NFT_WITH_KEYS_AND_EXPIRY_FUNCTION, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - AUTO_RENEW_PERIOD) - .via(FIRST_CREATE_TXN) - .gas(GAS_TO_OFFER) - .payingWith(ACCOUNT) - .sending(DEFAULT_AMOUNT_TO_SEND) - .exposingResultTo(result -> { - log.info("Explicit create result is" + " {}", result[0]); - final var res = (Address) result[0]; - createdTokenNum.set(res.value().longValueExact()); - }) - .refusingEthConversion() - .hasKnownStatus(SUCCESS); - final var subop3 = getTxnRecord(FIRST_CREATE_TXN); - allRunFor( - spec, - subop1, - subop2, - subop3, - childRecordsCheck( - FIRST_CREATE_TXN, - SUCCESS, - TransactionRecordAsserts.recordWith().status(SUCCESS))); - - final var delta = subop3.getResponseRecord().getTransactionFee(); - final var effectivePayer = ACCOUNT; - final var subop4 = getAccountBalance(effectivePayer) - .hasTinyBars(changeFromSnapshot(ACCOUNT_BALANCE, -(delta + DEFAULT_AMOUNT_TO_SEND))); - final var contractBalanceCheck = getContractInfo(TOKEN_CREATE_CONTRACT) - .has(ContractInfoAsserts.contractWith() - .balanceGreaterThan(0L) - .balanceLessThan(DEFAULT_AMOUNT_TO_SEND)); - final var getAccountTokenBalance = getAccountBalance(ACCOUNT) - .hasTokenBalance( - asTokenString(TokenID.newBuilder() - .setTokenNum(createdTokenNum.get()) - .build()), - 0); - final var tokenInfo = getTokenInfo(asTokenString(TokenID.newBuilder() - .setTokenNum(createdTokenNum.get()) - .build())) - .hasTokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .hasSymbol(TOKEN_SYMBOL) - .hasName(TOKEN_NAME) - .hasDecimals(0) - .hasTotalSupply(0) - .hasEntityMemo(MEMO) - .hasTreasury(ACCOUNT) - .hasAutoRenewAccount(ACCOUNT) - .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) - .hasSupplyType(TokenSupplyType.FINITE) - .hasFreezeDefault(TokenFreezeStatus.Frozen) - .hasMaxSupply(10) - .searchKeysGlobally() - .hasAdminKey(ED25519KEY) - .hasSupplyKey(ED25519KEY) - .hasPauseKey(ED25519KEY) - .hasFreezeKey(ED25519KEY) - .hasKycKey(ED25519KEY) - .hasFeeScheduleKey(ED25519KEY) - .hasWipeKey(ED25519KEY) - .hasPauseStatus(TokenPauseStatus.Unpaused) - .logged(); - allRunFor(spec, subop4, getAccountTokenBalance, tokenInfo, contractBalanceCheck); - })); - } - - // TEST-005 - private HapiSpec fungibleTokenCreateThenQueryAndTransfer() { - final var createdTokenNum = new AtomicLong(); - return defaultHapiSpec("fungibleTokenCreateThenQueryAndTransfer") - .given( - newKeyNamed(ED25519KEY).shape(ED25519), - cryptoCreate(ACCOUNT) - .balance(ONE_MILLION_HBARS) - .key(ED25519KEY) - .maxAutomaticTokenAssociations(1), - uploadInitCode(TOKEN_CREATE_CONTRACT), - contractCreate(TOKEN_CREATE_CONTRACT).gas(GAS_TO_OFFER)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_CREATE_CONTRACT, - "createTokenThenQueryAndTransfer", - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - AUTO_RENEW_PERIOD) - .via(FIRST_CREATE_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .payingWith(ACCOUNT) - .refusingEthConversion() - .exposingResultTo(result -> { - log.info(EXPLICIT_CREATE_RESULT, result[0]); - final var res = (Address) result[0]; - createdTokenNum.set(res.value().longValueExact()); - }), - newKeyNamed(TOKEN_CREATE_CONTRACT_AS_KEY).shape(CONTRACT.signedWith(TOKEN_CREATE_CONTRACT))))) - .then( - getTxnRecord(FIRST_CREATE_TXN).andAllChildRecords().logged(), - getAccountBalance(ACCOUNT).logged(), - getAccountBalance(TOKEN_CREATE_CONTRACT).logged(), - getContractInfo(TOKEN_CREATE_CONTRACT).logged(), - childRecordsCheck( - FIRST_CREATE_TXN, - ResponseCodeEnum.SUCCESS, - TransactionRecordAsserts.recordWith().status(SUCCESS), - TransactionRecordAsserts.recordWith().status(SUCCESS), - TransactionRecordAsserts.recordWith().status(SUCCESS), - TransactionRecordAsserts.recordWith().status(SUCCESS)), - sourcing(() -> getAccountBalance(ACCOUNT) - .hasTokenBalance( - asTokenString(TokenID.newBuilder() - .setTokenNum(createdTokenNum.get()) - .build()), - 20)), - sourcing(() -> getAccountBalance(TOKEN_CREATE_CONTRACT) - .hasTokenBalance( - asTokenString(TokenID.newBuilder() - .setTokenNum(createdTokenNum.get()) - .build()), - 10)), - sourcing(() -> getTokenInfo(asTokenString(TokenID.newBuilder() - .setTokenNum(createdTokenNum.get()) - .build())) - .hasTokenType(TokenType.FUNGIBLE_COMMON) - .hasSymbol(TOKEN_SYMBOL) - .hasName(TOKEN_NAME) - .hasDecimals(8) - .hasTotalSupply(30) - .hasEntityMemo(MEMO) - .hasTreasury(TOKEN_CREATE_CONTRACT) - .hasAutoRenewAccount(ACCOUNT) - .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) - .hasSupplyType(TokenSupplyType.INFINITE) - .searchKeysGlobally() - .hasAdminKey(ED25519KEY) - .hasSupplyKey(TOKEN_CREATE_CONTRACT_AS_KEY) - .hasPauseKey(TOKEN_CREATE_CONTRACT_AS_KEY) - .hasPauseStatus(TokenPauseStatus.Unpaused) - .logged())); - } - - // TEST-006 - private HapiSpec nonFungibleTokenCreateThenQuery() { - final var createdTokenNum = new AtomicLong(); - return defaultHapiSpec("nonFungibleTokenCreateThenQuery") - .given( - cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS), - uploadInitCode(TOKEN_CREATE_CONTRACT), - contractCreate(TOKEN_CREATE_CONTRACT).gas(GAS_TO_OFFER)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_CREATE_CONTRACT, - "createNonFungibleTokenThenQuery", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - AUTO_RENEW_PERIOD) - .via(FIRST_CREATE_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .payingWith(ACCOUNT) - .refusingEthConversion() - .exposingResultTo(result -> { - log.info(EXPLICIT_CREATE_RESULT, result[0]); - final var res = (Address) result[0]; - createdTokenNum.set(res.value().longValueExact()); - }), - newKeyNamed(TOKEN_CREATE_CONTRACT_AS_KEY).shape(CONTRACT.signedWith(TOKEN_CREATE_CONTRACT))))) - .then( - getTxnRecord(FIRST_CREATE_TXN).andAllChildRecords().logged(), - getAccountBalance(ACCOUNT).logged(), - getAccountBalance(TOKEN_CREATE_CONTRACT).logged(), - getContractInfo(TOKEN_CREATE_CONTRACT).logged(), - childRecordsCheck( - FIRST_CREATE_TXN, - ResponseCodeEnum.SUCCESS, - TransactionRecordAsserts.recordWith().status(SUCCESS), - TransactionRecordAsserts.recordWith().status(SUCCESS), - TransactionRecordAsserts.recordWith().status(SUCCESS)), - sourcing(() -> getAccountBalance(TOKEN_CREATE_CONTRACT) - .hasTokenBalance( - asTokenString(TokenID.newBuilder() - .setTokenNum(createdTokenNum.get()) - .build()), - 0)), - sourcing(() -> getTokenInfo(asTokenString(TokenID.newBuilder() - .setTokenNum(createdTokenNum.get()) - .build())) - .hasTokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .hasSymbol(TOKEN_SYMBOL) - .hasName(TOKEN_NAME) - .hasDecimals(0) - .hasTotalSupply(0) - .hasEntityMemo(MEMO) - .hasTreasury(TOKEN_CREATE_CONTRACT) - .hasAutoRenewAccount(ACCOUNT) - .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) - .hasSupplyType(TokenSupplyType.INFINITE) - .searchKeysGlobally() - .hasAdminKey(TOKEN_CREATE_CONTRACT_AS_KEY) - .hasSupplyKey(TOKEN_CREATE_CONTRACT_AS_KEY) - .hasPauseStatus(TokenPauseStatus.PauseNotApplicable) - .logged())); - } - // TEST-007 & TEST-016 private HapiSpec tokenCreateWithEmptyKeysReverts() { return defaultHapiSpec("tokenCreateWithEmptyKeysReverts") @@ -930,50 +435,6 @@ private HapiSpec delegateCallTokenCreateFails() { getContractInfo(TOKEN_CREATE_CONTRACT)); } - private HapiSpec createTokenWithDefaultExpiryAndEmptyKeys() { - final var tokenCreateContractAsKeyDelegate = "createTokenWithDefaultExpiryAndEmptyKeys"; - final var createTokenNum = new AtomicLong(); - return defaultHapiSpec(tokenCreateContractAsKeyDelegate) - .given( - newKeyNamed(ED25519KEY).shape(ED25519), - newKeyNamed(ECDSA_KEY).shape(SECP256K1), - newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), - newKeyNamed(CONTRACT_ADMIN_KEY), - cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(ED25519KEY), - cryptoCreate(AUTO_RENEW_ACCOUNT) - .balance(ONE_HUNDRED_HBARS) - .key(ED25519KEY), - cryptoCreate(ACCOUNT_2).balance(ONE_HUNDRED_HBARS).key(ECDSA_KEY), - cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), - uploadInitCode(TOKEN_CREATE_CONTRACT), - contractCreate(TOKEN_CREATE_CONTRACT) - .gas(GAS_TO_OFFER) - .adminKey(CONTRACT_ADMIN_KEY) - .autoRenewAccountId(AUTO_RENEW_ACCOUNT) - .signedBy(CONTRACT_ADMIN_KEY, DEFAULT_PAYER, AUTO_RENEW_ACCOUNT), - getContractInfo(TOKEN_CREATE_CONTRACT) - .has(ContractInfoAsserts.contractWith().autoRenewAccountId(AUTO_RENEW_ACCOUNT)) - .logged()) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall(TOKEN_CREATE_CONTRACT, tokenCreateContractAsKeyDelegate) - .via(FIRST_CREATE_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .payingWith(ACCOUNT) - .refusingEthConversion() - .alsoSigningWithFullPrefix(ACCOUNT_TO_ASSOCIATE_KEY) - .exposingResultTo(result -> { - log.info(EXPLICIT_CREATE_RESULT, result[0]); - final var res = (Address) result[0]; - createTokenNum.set(res.value().longValueExact()); - }), - newKeyNamed(TOKEN_CREATE_CONTRACT_AS_KEY).shape(CONTRACT.signedWith(TOKEN_CREATE_CONTRACT)), - newKeyNamed(tokenCreateContractAsKeyDelegate) - .shape(DELEGATE_CONTRACT.signedWith(TOKEN_CREATE_CONTRACT))))) - .then(getTxnRecord(FIRST_CREATE_TXN).andAllChildRecords().logged()); - } - @Override protected Logger getResultsLogger() { return log; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CreatePrecompileV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CreatePrecompileV1SecurityModelSuite.java new file mode 100644 index 000000000000..4633278731c3 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CreatePrecompileV1SecurityModelSuite.java @@ -0,0 +1,662 @@ +/* + * Copyright (C) 2022-2023 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.spec.HapiPropertySource.asTokenString; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +import static com.hedera.services.bdd.spec.assertions.AccountInfoAsserts.changeFromSnapshot; +import static com.hedera.services.bdd.spec.keys.KeyShape.CONTRACT; +import static com.hedera.services.bdd.spec.keys.KeyShape.DELEGATE_CONTRACT; +import static com.hedera.services.bdd.spec.keys.KeyShape.ED25519; +import static com.hedera.services.bdd.spec.keys.KeyShape.SECP256K1; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; +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.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.balanceSnapshot; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_IS_TREASURY; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; + +import com.esaulpaugh.headlong.abi.Address; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.assertions.ContractInfoAsserts; +import com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; +import com.hederahashgraph.api.proto.java.TokenFreezeStatus; +import com.hederahashgraph.api.proto.java.TokenID; +import com.hederahashgraph.api.proto.java.TokenPauseStatus; +import com.hederahashgraph.api.proto.java.TokenSupplyType; +import com.hederahashgraph.api.proto.java.TokenType; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +// Some of the test cases cannot be converted to use eth calls, +// since they use admin keys, which are held by the txn payer. +// In the case of an eth txn, we revoke the payers keys and the txn would fail. +// The only way an eth account to create a token is the admin key to be of a contractId type. +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse +public class CreatePrecompileV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(CreatePrecompileV1SecurityModelSuite.class); + + public static final String ACCOUNT_2 = "account2"; + public static final String CONTRACT_ADMIN_KEY = "contractAdminKey"; + public static final String ACCOUNT_TO_ASSOCIATE = "account3"; + public static final String ACCOUNT_TO_ASSOCIATE_KEY = "associateKey"; + public static final String FALSE = "false"; + private static final long GAS_TO_OFFER = 1_000_000L; + public static final long AUTO_RENEW_PERIOD = 8_000_000L; + public static final String TOKEN_SYMBOL = "tokenSymbol"; + public static final String TOKEN_NAME = "tokenName"; + public static final String MEMO = "memo"; + public static final String TOKEN_CREATE_CONTRACT_AS_KEY = "tokenCreateContractAsKey"; + private static final String ACCOUNT = "account"; + public static final String TOKEN_CREATE_CONTRACT = "TokenCreateContract"; + public static final String FIRST_CREATE_TXN = "firstCreateTxn"; + private static final String ACCOUNT_BALANCE = "ACCOUNT_BALANCE"; + public static final long DEFAULT_AMOUNT_TO_SEND = 20 * ONE_HBAR; + public static final String ED25519KEY = "ed25519key"; + public static final String ECDSA_KEY = "ecdsa"; + private static final String AUTO_RENEW_ACCOUNT = "autoRenewAccount"; + public static final String EXPLICIT_CREATE_RESULT = "Explicit create result is {}"; + private static final String CREATE_NFT_WITH_KEYS_AND_EXPIRY_FUNCTION = "createNFTTokenWithKeysAndExpiry"; + + public static void main(String... args) { + new CreatePrecompileV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + // TODO: Fix contract name in TokenCreateContract.sol + @Override + public List getSpecsInSuite() { + return allOf(positiveSpecs(), negativeSpecs()); + } + + List positiveSpecs() { + return List.of( + fungibleTokenCreateHappyPath(), + nonFungibleTokenCreateHappyPath(), + fungibleTokenCreateThenQueryAndTransfer(), + nonFungibleTokenCreateThenQuery(), + inheritsSenderAutoRenewAccountIfAnyForNftCreate(), + inheritsSenderAutoRenewAccountForTokenCreate(), + createTokenWithDefaultExpiryAndEmptyKeys()); + } + + List negativeSpecs() { + // TODO: Where are the security model v1 _negative_ tests? + return List.of(); + } + + // TEST-001 + private HapiSpec fungibleTokenCreateHappyPath() { + final var tokenCreateContractAsKeyDelegate = "tokenCreateContractAsKeyDelegate"; + final var createTokenNum = new AtomicLong(); + return propertyPreservingHapiSpec("fungibleTokenCreateHappyPath") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ED25519KEY).shape(ED25519), + newKeyNamed(ECDSA_KEY).shape(SECP256K1), + newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), + newKeyNamed(CONTRACT_ADMIN_KEY), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(ED25519KEY), + cryptoCreate(AUTO_RENEW_ACCOUNT) + .balance(ONE_HUNDRED_HBARS) + .key(ED25519KEY), + cryptoCreate(ACCOUNT_2).balance(ONE_HUNDRED_HBARS).key(ECDSA_KEY), + cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), + uploadInitCode(TOKEN_CREATE_CONTRACT), + contractCreate(TOKEN_CREATE_CONTRACT) + .gas(GAS_TO_OFFER) + .adminKey(CONTRACT_ADMIN_KEY) + .autoRenewAccountId(AUTO_RENEW_ACCOUNT) + .signedBy(CONTRACT_ADMIN_KEY, DEFAULT_PAYER, AUTO_RENEW_ACCOUNT), + getContractInfo(TOKEN_CREATE_CONTRACT) + .has(ContractInfoAsserts.contractWith().autoRenewAccountId(AUTO_RENEW_ACCOUNT)) + .logged()) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_CREATE_CONTRACT, + "createTokenWithKeysAndExpiry", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT_TO_ASSOCIATE)))) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT_TO_ASSOCIATE_KEY) + .refusingEthConversion() + .exposingResultTo(result -> { + log.info(EXPLICIT_CREATE_RESULT, result[0]); + final var res = (Address) result[0]; + createTokenNum.set(res.value().longValueExact()); + }), + newKeyNamed(TOKEN_CREATE_CONTRACT_AS_KEY).shape(CONTRACT.signedWith(TOKEN_CREATE_CONTRACT)), + newKeyNamed(tokenCreateContractAsKeyDelegate) + .shape(DELEGATE_CONTRACT.signedWith(TOKEN_CREATE_CONTRACT))))) + .then( + getTxnRecord(FIRST_CREATE_TXN).andAllChildRecords().logged(), + getAccountBalance(ACCOUNT).logged(), + getAccountBalance(TOKEN_CREATE_CONTRACT).logged(), + getContractInfo(TOKEN_CREATE_CONTRACT).logged(), + childRecordsCheck( + FIRST_CREATE_TXN, + ResponseCodeEnum.SUCCESS, + TransactionRecordAsserts.recordWith().status(ResponseCodeEnum.SUCCESS), + TransactionRecordAsserts.recordWith().status(ResponseCodeEnum.SUCCESS), + TransactionRecordAsserts.recordWith().status(ResponseCodeEnum.SUCCESS)), + sourcing(() -> + getAccountInfo(ACCOUNT_TO_ASSOCIATE).logged().hasTokenRelationShipCount(1)), + sourcing(() -> getTokenInfo(asTokenString(TokenID.newBuilder() + .setTokenNum(createTokenNum.get()) + .build())) + .logged() + .hasTokenType(TokenType.FUNGIBLE_COMMON) + .hasSymbol(TOKEN_SYMBOL) + .hasName(TOKEN_NAME) + .hasDecimals(8) + .hasTotalSupply(100) + .hasEntityMemo(MEMO) + .hasTreasury(ACCOUNT) + // Token doesn't inherit contract's auto-renew + // account if set in tokenCreate + .hasAutoRenewAccount(ACCOUNT) + .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) + .hasSupplyType(TokenSupplyType.INFINITE) + .searchKeysGlobally() + .hasAdminKey(ED25519KEY) + .hasKycKey(ED25519KEY) + .hasFreezeKey(ECDSA_KEY) + .hasWipeKey(ECDSA_KEY) + .hasSupplyKey(TOKEN_CREATE_CONTRACT_AS_KEY) + .hasFeeScheduleKey(tokenCreateContractAsKeyDelegate) + .hasPauseKey(CONTRACT_ADMIN_KEY) + .hasPauseStatus(TokenPauseStatus.Unpaused)), + cryptoDelete(ACCOUNT).hasKnownStatus(ACCOUNT_IS_TREASURY)); + } + + // TEST-002 + + private HapiSpec inheritsSenderAutoRenewAccountIfAnyForNftCreate() { + final var createdNftTokenNum = new AtomicLong(); + return propertyPreservingHapiSpec("inheritsSenderAutoRenewAccountIfAnyForNftCreate") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ED25519KEY).shape(ED25519), + newKeyNamed(ECDSA_KEY), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(ED25519KEY), + cryptoCreate(AUTO_RENEW_ACCOUNT) + .balance(ONE_HUNDRED_HBARS) + .key(ED25519KEY), + uploadInitCode(TOKEN_CREATE_CONTRACT)) + .when( + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate(TOKEN_CREATE_CONTRACT) + .autoRenewAccountId(AUTO_RENEW_ACCOUNT) + .gas(GAS_TO_OFFER))), + getContractInfo(TOKEN_CREATE_CONTRACT) + .has(ContractInfoAsserts.contractWith().autoRenewAccountId(AUTO_RENEW_ACCOUNT)) + .logged()) + .then(withOpContext((spec, ignore) -> { + final var subop1 = balanceSnapshot(ACCOUNT_BALANCE, ACCOUNT); + final var subop2 = contractCall( + TOKEN_CREATE_CONTRACT, + CREATE_NFT_WITH_KEYS_AND_EXPIRY_FUNCTION, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + HapiParserUtil.asHeadlongAddress(new byte[20]), + AUTO_RENEW_PERIOD) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .payingWith(ACCOUNT) + .sending(DEFAULT_AMOUNT_TO_SEND) + .refusingEthConversion() + .exposingResultTo(result -> { + log.info("Explicit create result is" + " {}", result[0]); + final var res = (Address) result[0]; + createdNftTokenNum.set(res.value().longValueExact()); + }) + .hasKnownStatus(SUCCESS); + + allRunFor( + spec, + subop1, + subop2, + childRecordsCheck( + FIRST_CREATE_TXN, + SUCCESS, + TransactionRecordAsserts.recordWith().status(SUCCESS))); + + final var nftInfo = getTokenInfo(asTokenString(TokenID.newBuilder() + .setTokenNum(createdNftTokenNum.get()) + .build())) + .hasAutoRenewAccount(AUTO_RENEW_ACCOUNT) + .logged(); + + allRunFor(spec, nftInfo); + })); + } + + private HapiSpec inheritsSenderAutoRenewAccountForTokenCreate() { + final var createTokenNum = new AtomicLong(); + return propertyPreservingHapiSpec("inheritsSenderAutoRenewAccountForTokenCreate") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ED25519KEY).shape(ED25519), + newKeyNamed(ECDSA_KEY).shape(SECP256K1), + newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), + newKeyNamed(CONTRACT_ADMIN_KEY), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(ED25519KEY), + cryptoCreate(AUTO_RENEW_ACCOUNT) + .balance(ONE_HUNDRED_HBARS) + .key(ED25519KEY), + cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), + uploadInitCode(TOKEN_CREATE_CONTRACT), + contractCreate(TOKEN_CREATE_CONTRACT) + .gas(GAS_TO_OFFER) + .adminKey(CONTRACT_ADMIN_KEY) + .autoRenewAccountId(AUTO_RENEW_ACCOUNT) // inherits if the tokenCreateOp doesn't + // have + // autoRenewAccount + .signedBy(CONTRACT_ADMIN_KEY, DEFAULT_PAYER, AUTO_RENEW_ACCOUNT), + getContractInfo(TOKEN_CREATE_CONTRACT) + .has(ContractInfoAsserts.contractWith().autoRenewAccountId(AUTO_RENEW_ACCOUNT)) + .logged()) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_CREATE_CONTRACT, + "createTokenWithKeysAndExpiry", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress(new byte[20]), // set empty + // autoRenewAccount + AUTO_RENEW_PERIOD, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT_TO_ASSOCIATE)))) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT_TO_ASSOCIATE_KEY) + .refusingEthConversion() + .exposingResultTo(result -> { + log.info(EXPLICIT_CREATE_RESULT, result[0]); + final var res = (Address) result[0]; + createTokenNum.set(res.value().longValueExact()); + })))) + .then(sourcing(() -> getTokenInfo(asTokenString(TokenID.newBuilder() + .setTokenNum(createTokenNum.get()) + .build())) + .logged() + .hasAutoRenewAccount(AUTO_RENEW_ACCOUNT) + .hasPauseStatus(TokenPauseStatus.Unpaused))); + } + + // TEST-003 & TEST-019 + private HapiSpec nonFungibleTokenCreateHappyPath() { + final var createdTokenNum = new AtomicLong(); + return propertyPreservingHapiSpec("nonFungibleTokenCreateHappyPath") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ED25519KEY).shape(ED25519), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(ED25519KEY), + uploadInitCode(TOKEN_CREATE_CONTRACT), + getAccountInfo(DEFAULT_CONTRACT_SENDER).savingSnapshot(DEFAULT_CONTRACT_SENDER)) + .when(withOpContext((spec, opLog) -> + allRunFor(spec, contractCreate(TOKEN_CREATE_CONTRACT).gas(GAS_TO_OFFER)))) + .then(withOpContext((spec, ignore) -> { + final var subop1 = balanceSnapshot(ACCOUNT_BALANCE, ACCOUNT); + final var subop2 = contractCall( + TOKEN_CREATE_CONTRACT, + CREATE_NFT_WITH_KEYS_AND_EXPIRY_FUNCTION, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .payingWith(ACCOUNT) + .sending(DEFAULT_AMOUNT_TO_SEND) + .exposingResultTo(result -> { + log.info("Explicit create result is" + " {}", result[0]); + final var res = (Address) result[0]; + createdTokenNum.set(res.value().longValueExact()); + }) + .refusingEthConversion() + .hasKnownStatus(SUCCESS); + final var subop3 = getTxnRecord(FIRST_CREATE_TXN); + allRunFor( + spec, + subop1, + subop2, + subop3, + childRecordsCheck( + FIRST_CREATE_TXN, + SUCCESS, + TransactionRecordAsserts.recordWith().status(SUCCESS))); + + final var delta = subop3.getResponseRecord().getTransactionFee(); + final var effectivePayer = ACCOUNT; + final var subop4 = getAccountBalance(effectivePayer) + .hasTinyBars(changeFromSnapshot(ACCOUNT_BALANCE, -(delta + DEFAULT_AMOUNT_TO_SEND))); + final var contractBalanceCheck = getContractInfo(TOKEN_CREATE_CONTRACT) + .has(ContractInfoAsserts.contractWith() + .balanceGreaterThan(0L) + .balanceLessThan(DEFAULT_AMOUNT_TO_SEND)); + final var getAccountTokenBalance = getAccountBalance(ACCOUNT) + .hasTokenBalance( + asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build()), + 0); + final var tokenInfo = getTokenInfo(asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build())) + .hasTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .hasSymbol(TOKEN_SYMBOL) + .hasName(TOKEN_NAME) + .hasDecimals(0) + .hasTotalSupply(0) + .hasEntityMemo(MEMO) + .hasTreasury(ACCOUNT) + .hasAutoRenewAccount(ACCOUNT) + .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) + .hasSupplyType(TokenSupplyType.FINITE) + .hasFreezeDefault(TokenFreezeStatus.Frozen) + .hasMaxSupply(10) + .searchKeysGlobally() + .hasAdminKey(ED25519KEY) + .hasSupplyKey(ED25519KEY) + .hasPauseKey(ED25519KEY) + .hasFreezeKey(ED25519KEY) + .hasKycKey(ED25519KEY) + .hasFeeScheduleKey(ED25519KEY) + .hasWipeKey(ED25519KEY) + .hasPauseStatus(TokenPauseStatus.Unpaused) + .logged(); + allRunFor(spec, subop4, getAccountTokenBalance, tokenInfo, contractBalanceCheck); + })); + } + + // TEST-005 + private HapiSpec fungibleTokenCreateThenQueryAndTransfer() { + final var createdTokenNum = new AtomicLong(); + return propertyPreservingHapiSpec("fungibleTokenCreateThenQueryAndTransfer") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ED25519KEY).shape(ED25519), + cryptoCreate(ACCOUNT) + .balance(ONE_MILLION_HBARS) + .key(ED25519KEY) + .maxAutomaticTokenAssociations(1), + uploadInitCode(TOKEN_CREATE_CONTRACT), + contractCreate(TOKEN_CREATE_CONTRACT).gas(GAS_TO_OFFER)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_CREATE_CONTRACT, + "createTokenThenQueryAndTransfer", + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(ACCOUNT) + .refusingEthConversion() + .exposingResultTo(result -> { + log.info(EXPLICIT_CREATE_RESULT, result[0]); + final var res = (Address) result[0]; + createdTokenNum.set(res.value().longValueExact()); + }), + newKeyNamed(TOKEN_CREATE_CONTRACT_AS_KEY).shape(CONTRACT.signedWith(TOKEN_CREATE_CONTRACT))))) + .then( + getTxnRecord(FIRST_CREATE_TXN).andAllChildRecords().logged(), + getAccountBalance(ACCOUNT).logged(), + getAccountBalance(TOKEN_CREATE_CONTRACT).logged(), + getContractInfo(TOKEN_CREATE_CONTRACT).logged(), + childRecordsCheck( + FIRST_CREATE_TXN, + ResponseCodeEnum.SUCCESS, + TransactionRecordAsserts.recordWith().status(SUCCESS), + TransactionRecordAsserts.recordWith().status(SUCCESS), + TransactionRecordAsserts.recordWith().status(SUCCESS), + TransactionRecordAsserts.recordWith().status(SUCCESS)), + sourcing(() -> getAccountBalance(ACCOUNT) + .hasTokenBalance( + asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build()), + 20)), + sourcing(() -> getAccountBalance(TOKEN_CREATE_CONTRACT) + .hasTokenBalance( + asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build()), + 10)), + sourcing(() -> getTokenInfo(asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build())) + .hasTokenType(TokenType.FUNGIBLE_COMMON) + .hasSymbol(TOKEN_SYMBOL) + .hasName(TOKEN_NAME) + .hasDecimals(8) + .hasTotalSupply(30) + .hasEntityMemo(MEMO) + .hasTreasury(TOKEN_CREATE_CONTRACT) + .hasAutoRenewAccount(ACCOUNT) + .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) + .hasSupplyType(TokenSupplyType.INFINITE) + .searchKeysGlobally() + .hasAdminKey(ED25519KEY) + .hasSupplyKey(TOKEN_CREATE_CONTRACT_AS_KEY) + .hasPauseKey(TOKEN_CREATE_CONTRACT_AS_KEY) + .hasPauseStatus(TokenPauseStatus.Unpaused) + .logged())); + } + + // TEST-006 + private HapiSpec nonFungibleTokenCreateThenQuery() { + final var createdTokenNum = new AtomicLong(); + return propertyPreservingHapiSpec("nonFungibleTokenCreateThenQuery") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS), + uploadInitCode(TOKEN_CREATE_CONTRACT), + contractCreate(TOKEN_CREATE_CONTRACT).gas(GAS_TO_OFFER)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_CREATE_CONTRACT, + "createNonFungibleTokenThenQuery", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(ACCOUNT) + .refusingEthConversion() + .exposingResultTo(result -> { + log.info(EXPLICIT_CREATE_RESULT, result[0]); + final var res = (Address) result[0]; + createdTokenNum.set(res.value().longValueExact()); + }), + newKeyNamed(TOKEN_CREATE_CONTRACT_AS_KEY).shape(CONTRACT.signedWith(TOKEN_CREATE_CONTRACT))))) + .then( + getTxnRecord(FIRST_CREATE_TXN).andAllChildRecords().logged(), + getAccountBalance(ACCOUNT).logged(), + getAccountBalance(TOKEN_CREATE_CONTRACT).logged(), + getContractInfo(TOKEN_CREATE_CONTRACT).logged(), + childRecordsCheck( + FIRST_CREATE_TXN, + ResponseCodeEnum.SUCCESS, + TransactionRecordAsserts.recordWith().status(SUCCESS), + TransactionRecordAsserts.recordWith().status(SUCCESS), + TransactionRecordAsserts.recordWith().status(SUCCESS)), + sourcing(() -> getAccountBalance(TOKEN_CREATE_CONTRACT) + .hasTokenBalance( + asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build()), + 0)), + sourcing(() -> getTokenInfo(asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build())) + .hasTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .hasSymbol(TOKEN_SYMBOL) + .hasName(TOKEN_NAME) + .hasDecimals(0) + .hasTotalSupply(0) + .hasEntityMemo(MEMO) + .hasTreasury(TOKEN_CREATE_CONTRACT) + .hasAutoRenewAccount(ACCOUNT) + .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) + .hasSupplyType(TokenSupplyType.INFINITE) + .searchKeysGlobally() + .hasAdminKey(TOKEN_CREATE_CONTRACT_AS_KEY) + .hasSupplyKey(TOKEN_CREATE_CONTRACT_AS_KEY) + .hasPauseStatus(TokenPauseStatus.PauseNotApplicable) + .logged())); + } + + private HapiSpec createTokenWithDefaultExpiryAndEmptyKeys() { + final var tokenCreateContractAsKeyDelegate = "createTokenWithDefaultExpiryAndEmptyKeys"; + final var createTokenNum = new AtomicLong(); + return propertyPreservingHapiSpec("createTokenWithDefaultExpiryAndEmptyKeys") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ED25519KEY).shape(ED25519), + newKeyNamed(ECDSA_KEY).shape(SECP256K1), + newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), + newKeyNamed(CONTRACT_ADMIN_KEY), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(ED25519KEY), + cryptoCreate(AUTO_RENEW_ACCOUNT) + .balance(ONE_HUNDRED_HBARS) + .key(ED25519KEY), + cryptoCreate(ACCOUNT_2).balance(ONE_HUNDRED_HBARS).key(ECDSA_KEY), + cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), + uploadInitCode(TOKEN_CREATE_CONTRACT), + contractCreate(TOKEN_CREATE_CONTRACT) + .gas(GAS_TO_OFFER) + .adminKey(CONTRACT_ADMIN_KEY) + .autoRenewAccountId(AUTO_RENEW_ACCOUNT) + .signedBy(CONTRACT_ADMIN_KEY, DEFAULT_PAYER, AUTO_RENEW_ACCOUNT), + getContractInfo(TOKEN_CREATE_CONTRACT) + .has(ContractInfoAsserts.contractWith().autoRenewAccountId(AUTO_RENEW_ACCOUNT)) + .logged()) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall(TOKEN_CREATE_CONTRACT, tokenCreateContractAsKeyDelegate) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(ACCOUNT) + .refusingEthConversion() + .alsoSigningWithFullPrefix(ACCOUNT_TO_ASSOCIATE_KEY) + .exposingResultTo(result -> { + log.info(EXPLICIT_CREATE_RESULT, result[0]); + final var res = (Address) result[0]; + createTokenNum.set(res.value().longValueExact()); + }), + newKeyNamed(TOKEN_CREATE_CONTRACT_AS_KEY).shape(CONTRACT.signedWith(TOKEN_CREATE_CONTRACT)), + newKeyNamed(tokenCreateContractAsKeyDelegate) + .shape(DELEGATE_CONTRACT.signedWith(TOKEN_CREATE_CONTRACT))))) + .then(getTxnRecord(FIRST_CREATE_TXN).andAllChildRecords().logged()); + } + + @Override + protected Logger getResultsLogger() { + return log; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CryptoTransferHTSSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CryptoTransferHTSSuite.java index a58b9b16bca1..065bef71f174 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CryptoTransferHTSSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CryptoTransferHTSSuite.java @@ -28,7 +28,6 @@ import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountDetails; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; @@ -65,7 +64,6 @@ import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.AMOUNT_EXCEEDS_ALLOWANCE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SPENDER_DOES_NOT_HAVE_ALLOWANCE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; @@ -76,7 +74,6 @@ import com.hedera.node.app.hapi.utils.ByteStringUtils; import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; import com.hedera.services.bdd.spec.HapiSpec; -import com.hedera.services.bdd.spec.assertions.ContractInfoAsserts; import com.hedera.services.bdd.spec.assertions.NonFungibleTransfers; import com.hedera.services.bdd.spec.assertions.SomeFungibleTransfers; import com.hedera.services.bdd.spec.keys.KeyShape; @@ -118,10 +115,6 @@ public class CryptoTransferHTSSuite extends HapiSuite { private static final ByteString META2 = ByteStringUtils.wrapUnsafely("meta2".getBytes()); private static final ByteString META3 = ByteStringUtils.wrapUnsafely("meta3".getBytes()); private static final ByteString META4 = ByteStringUtils.wrapUnsafely("meta4".getBytes()); - private static final ByteString META5 = ByteStringUtils.wrapUnsafely("meta5".getBytes()); - private static final ByteString META6 = ByteStringUtils.wrapUnsafely("meta6".getBytes()); - private static final ByteString META7 = ByteStringUtils.wrapUnsafely("meta7".getBytes()); - private static final ByteString META8 = ByteStringUtils.wrapUnsafely("meta8".getBytes()); private static final String NFT_TOKEN_WITH_FIXED_HBAR_FEE = "nftTokenWithFixedHbarFee"; private static final String NFT_TOKEN_WITH_FIXED_TOKEN_FEE = "nftTokenWithFixedTokenFee"; private static final String NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK = @@ -147,20 +140,15 @@ public boolean canRunConcurrent() { @Override public List getSpecsInSuite() { return List.of( - nonNestedCryptoTransferForFungibleToken(), nonNestedCryptoTransferForFungibleTokenWithMultipleReceivers(), nonNestedCryptoTransferForNonFungibleToken(), nonNestedCryptoTransferForMultipleNonFungibleTokens(), nonNestedCryptoTransferForFungibleAndNonFungibleToken(), nonNestedCryptoTransferForFungibleTokenWithMultipleSendersAndReceiversAndNonFungibleTokens(), repeatedTokenIdsAreAutomaticallyConsolidated(), - activeContractInFrameIsVerifiedWithoutNeedForSignature(), hapiTransferFromForFungibleToken(), hapiTransferFromForNFT(), - cryptoTransferNFTsWithCustomFeesMixedScenario(), hapiTransferFromForNFTWithCustomFeesWithoutApproveFails(), - hapiTransferFromForNFTWithCustomFeesWithApproveForAll(), - hapiTransferFromForNFTWithCustomFeesWithBothApproveForAllAndAssignedSpender(), hapiTransferFromForFungibleTokenWithCustomFeesWithoutApproveFails(), hapiTransferFromForFungibleTokenWithCustomFeesWithBothApproveForAllAndAssignedSpender()); } @@ -513,82 +501,6 @@ private HapiSpec repeatedTokenIdsAreAutomaticallyConsolidated() { .including(FUNGIBLE_TOKEN, RECEIVER, 2 * toSendEachTuple)))); } - private HapiSpec nonNestedCryptoTransferForFungibleToken() { - final var cryptoTransferTxn = CRYPTO_TRANSFER_TXN; - - return defaultHapiSpec("NonNestedCryptoTransferForFungibleToken") - .given( - cryptoCreate(SENDER).balance(10 * ONE_HUNDRED_HBARS), - cryptoCreate(RECEIVER).balance(2 * ONE_HUNDRED_HBARS).receiverSigRequired(true), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(FUNGIBLE_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(TOTAL_SUPPLY) - .treasury(TOKEN_TREASURY), - tokenAssociate(SENDER, List.of(FUNGIBLE_TOKEN)), - tokenAssociate(RECEIVER, List.of(FUNGIBLE_TOKEN)), - cryptoTransfer(moving(200, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, SENDER)), - uploadInitCode(CONTRACT), - contractCreate(CONTRACT).maxAutomaticTokenAssociations(1), - getContractInfo(CONTRACT) - .has(ContractInfoAsserts.contractWith().maxAutoAssociations(1)) - .logged()) - .when( - withOpContext((spec, opLog) -> { - final var token = spec.registry().getTokenID(FUNGIBLE_TOKEN); - final var sender = spec.registry().getAccountID(SENDER); - final var receiver = spec.registry().getAccountID(RECEIVER); - final var amountToBeSent = 50L; - - allRunFor( - spec, - newKeyNamed(DELEGATE_KEY) - .shape(DELEGATE_CONTRACT_KEY_SHAPE.signedWith(sigs(ON, CONTRACT))), - cryptoUpdate(SENDER).key(DELEGATE_KEY), - cryptoUpdate(RECEIVER).key(DELEGATE_KEY), - contractCall(CONTRACT, TRANSFER_MULTIPLE_TOKENS, (Object) new Tuple[] { - tokenTransferList() - .forToken(token) - .withAccountAmounts( - accountAmount(sender, -amountToBeSent), - accountAmount(receiver, amountToBeSent)) - .build() - }) - .payingWith(GENESIS) - .via(cryptoTransferTxn) - .gas(GAS_TO_OFFER), - contractCall(CONTRACT, TRANSFER_MULTIPLE_TOKENS, (Object) new Tuple[] { - tokenTransferList() - .forToken(token) - .withAccountAmounts( - accountAmount(sender, -0L), accountAmount(receiver, 0L)) - .build() - }) - .payingWith(GENESIS) - .via("cryptoTransferZero") - .gas(GAS_TO_OFFER)); - }), - getTxnRecord(cryptoTransferTxn).andAllChildRecords().logged(), - getTxnRecord("cryptoTransferZero").andAllChildRecords().logged()) - .then( - getTokenInfo(FUNGIBLE_TOKEN).hasTotalSupply(TOTAL_SUPPLY), - getAccountBalance(RECEIVER).hasTokenBalance(FUNGIBLE_TOKEN, 50), - getAccountBalance(SENDER).hasTokenBalance(FUNGIBLE_TOKEN, 150), - getTokenInfo(FUNGIBLE_TOKEN).logged(), - childRecordsCheck( - cryptoTransferTxn, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)) - .gasUsed(14085L)) - .tokenTransfers(SomeFungibleTransfers.changingFungibleBalances() - .including(FUNGIBLE_TOKEN, SENDER, -50) - .including(FUNGIBLE_TOKEN, RECEIVER, 50)))); - } - private HapiSpec nonNestedCryptoTransferForFungibleTokenWithMultipleReceivers() { final var cryptoTransferTxn = CRYPTO_TRANSFER_TXN; @@ -1028,178 +940,6 @@ private HapiSpec nonNestedCryptoTransferForFungibleTokenWithMultipleSendersAndRe .including(NFT_TOKEN, SENDER2, RECEIVER2, 2L)))); } - private HapiSpec activeContractInFrameIsVerifiedWithoutNeedForSignature() { - final var revertedFungibleTransferTxn = "revertedFungibleTransferTxn"; - final var successfulFungibleTransferTxn = "successfulFungibleTransferTxn"; - final var revertedNftTransferTxn = "revertedNftTransferTxn"; - final var successfulNftTransferTxn = "successfulNftTransferTxn"; - final var senderStartBalance = 200L; - final var receiverStartBalance = 0L; - final var toSendEachTuple = 50L; - final var multiKey = MULTI_KEY; - final var senderKey = "senderKey"; - final var contractKey = "contractAdminKey"; - - return defaultHapiSpec("ActiveContractIsVerifiedWithoutCheckingSignatures") - .given( - newKeyNamed(multiKey), - newKeyNamed(senderKey), - newKeyNamed(contractKey), - cryptoCreate(SENDER).balance(10 * ONE_HUNDRED_HBARS).key(senderKey), - cryptoCreate(RECEIVER).balance(2 * ONE_HUNDRED_HBARS), - cryptoCreate(TOKEN_TREASURY), - uploadInitCode(CONTRACT), - contractCreate(CONTRACT) - .payingWith(GENESIS) - .adminKey(contractKey) - .gas(GAS_TO_OFFER), - tokenCreate(FUNGIBLE_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(TOTAL_SUPPLY) - .treasury(TOKEN_TREASURY), - tokenCreate(NFT_TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .adminKey(multiKey) - .supplyKey(multiKey) - .supplyType(TokenSupplyType.INFINITE) - .initialSupply(0) - .treasury(TOKEN_TREASURY), - mintToken(NFT_TOKEN, List.of(metadata(FIRST_MEMO), metadata(SECOND_MEMO))), - tokenAssociate(SENDER, List.of(FUNGIBLE_TOKEN, NFT_TOKEN)), - tokenAssociate(RECEIVER, List.of(FUNGIBLE_TOKEN, NFT_TOKEN)), - tokenAssociate(CONTRACT, List.of(FUNGIBLE_TOKEN, NFT_TOKEN)), - cryptoTransfer( - moving(senderStartBalance, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, SENDER)), - cryptoTransfer(movingUnique(NFT_TOKEN, 1L).between(TOKEN_TREASURY, SENDER)), - cryptoTransfer( - moving(senderStartBalance, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, CONTRACT), - movingUnique(NFT_TOKEN, 2L).between(TOKEN_TREASURY, CONTRACT))) - .when(withOpContext((spec, opLog) -> { - final var token = spec.registry().getTokenID(FUNGIBLE_TOKEN); - final var nftToken = spec.registry().getTokenID(NFT_TOKEN); - final var sender = spec.registry().getAccountID(SENDER); - final var receiver = spec.registry().getAccountID(RECEIVER); - final var contractId = spec.registry().getAccountID(CONTRACT); - allRunFor( - spec, - contractCall(CONTRACT, TRANSFER_MULTIPLE_TOKENS, (Object) new Tuple[] { - tokenTransferList() - .forToken(token) - .withAccountAmounts( - accountAmount(contractId, -toSendEachTuple), - accountAmount(receiver, toSendEachTuple)) - .build(), - tokenTransferList() - .forToken(token) - .withAccountAmounts( - accountAmount(sender, -toSendEachTuple), - accountAmount(receiver, toSendEachTuple)) - .build() - }) - .payingWith(GENESIS) - .via(revertedFungibleTransferTxn) - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall(CONTRACT, TRANSFER_MULTIPLE_TOKENS, (Object) new Tuple[] { - tokenTransferList() - .forToken(token) - .withAccountAmounts( - accountAmount(contractId, -toSendEachTuple), - accountAmount(receiver, toSendEachTuple)) - .build(), - tokenTransferList() - .forToken(token) - .withAccountAmounts( - accountAmount(sender, -toSendEachTuple), - accountAmount(receiver, toSendEachTuple)) - .build() - }) - .payingWith(GENESIS) - .alsoSigningWithFullPrefix(senderKey) - .via(successfulFungibleTransferTxn) - .gas(GAS_TO_OFFER) - .hasKnownStatus(SUCCESS), - contractCall(CONTRACT, TRANSFER_MULTIPLE_TOKENS, (Object) new Tuple[] { - tokenTransferList() - .forToken(nftToken) - .withNftTransfers(nftTransfer(contractId, receiver, 2L)) - .build(), - tokenTransferList() - .forToken(nftToken) - .withNftTransfers(nftTransfer(sender, receiver, 1L)) - .build() - }) - .payingWith(GENESIS) - .via(revertedNftTransferTxn) - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall(CONTRACT, TRANSFER_MULTIPLE_TOKENS, (Object) new Tuple[] { - tokenTransferList() - .forToken(nftToken) - .withNftTransfers(nftTransfer(contractId, receiver, 2L)) - .build(), - tokenTransferList() - .forToken(nftToken) - .withNftTransfers(nftTransfer(sender, receiver, 1L)) - .build() - }) - .payingWith(GENESIS) - .via(successfulNftTransferTxn) - .alsoSigningWithFullPrefix(senderKey) - .gas(GAS_TO_OFFER) - .hasKnownStatus(SUCCESS)); - })) - .then( - getAccountBalance(RECEIVER) - .hasTokenBalance(FUNGIBLE_TOKEN, receiverStartBalance + 2 * toSendEachTuple) - .hasTokenBalance(NFT_TOKEN, 2L), - getAccountBalance(SENDER) - .hasTokenBalance(FUNGIBLE_TOKEN, senderStartBalance - toSendEachTuple) - .hasTokenBalance(NFT_TOKEN, 0L), - getAccountBalance(CONTRACT) - .hasTokenBalance(FUNGIBLE_TOKEN, senderStartBalance - toSendEachTuple) - .hasTokenBalance(NFT_TOKEN, 0L), - childRecordsCheck( - revertedFungibleTransferTxn, - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .withStatus(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))), - childRecordsCheck( - successfulFungibleTransferTxn, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))) - .tokenTransfers(SomeFungibleTransfers.changingFungibleBalances() - .including(FUNGIBLE_TOKEN, SENDER, -toSendEachTuple) - .including(FUNGIBLE_TOKEN, CONTRACT, -toSendEachTuple) - .including(FUNGIBLE_TOKEN, RECEIVER, 2 * toSendEachTuple))), - childRecordsCheck( - revertedNftTransferTxn, - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .withStatus(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))), - childRecordsCheck( - successfulNftTransferTxn, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))) - .tokenTransfers(NonFungibleTransfers.changingNFTBalances() - .including(NFT_TOKEN, SENDER, RECEIVER, 1L) - .including(NFT_TOKEN, CONTRACT, RECEIVER, 2L)))); - } - private HapiSpec hapiTransferFromForNFTWithCustomFeesWithoutApproveFails() { return defaultHapiSpec("HapiTransferFromForNFTWithCustomFeesWithoutApproveFails") .given( @@ -1342,462 +1082,6 @@ private HapiSpec hapiTransferFromForNFTWithCustomFeesWithoutApproveFails() { .then(); } - private HapiSpec cryptoTransferNFTsWithCustomFeesMixedScenario() { - final var SPENDER_SIGNATURE = "spenderSignature"; - return defaultHapiSpec("CryptoTransferNFTsWithCustomFeesMixedScenario") - .given( - newKeyNamed(MULTI_KEY), - newKeyNamed(RECEIVER_SIGNATURE), - newKeyNamed(SPENDER_SIGNATURE), - uploadInitCode(CONTRACT), - contractCreate(CONTRACT), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(OWNER) - .balance(ONE_HUNDRED_HBARS) - .maxAutomaticTokenAssociations(5) - .key(MULTI_KEY), - cryptoCreate(SENDER).balance(ONE_HUNDRED_HBARS), - cryptoCreate(RECEIVER).balance(ONE_HUNDRED_HBARS).key(RECEIVER_SIGNATURE), - tokenCreate(NFT_TOKEN_WITH_FIXED_HBAR_FEE) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(OWNER) - .initialSupply(0L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .withCustom(fixedHbarFee(1, OWNER)), - tokenCreate(FUNGIBLE_TOKEN_FEE) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .initialSupply(1000L), - tokenAssociate(CONTRACT, FUNGIBLE_TOKEN_FEE), - tokenAssociate(OWNER, FUNGIBLE_TOKEN_FEE), - tokenAssociate(RECEIVER, FUNGIBLE_TOKEN_FEE), - tokenCreate(NFT_TOKEN_WITH_FIXED_TOKEN_FEE) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(OWNER) - .initialSupply(0L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .withCustom(fixedHtsFee(1, FUNGIBLE_TOKEN_FEE, OWNER)), - tokenCreate(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(OWNER) - .initialSupply(0L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .withCustom( - royaltyFeeWithFallback(1, 2, fixedHbarFeeInheritingRoyaltyCollector(1), OWNER)), - tokenCreate(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(OWNER) - .initialSupply(0L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .withCustom(royaltyFeeWithFallback( - 1, 2, fixedHtsFeeInheritingRoyaltyCollector(1, FUNGIBLE_TOKEN_FEE), OWNER)), - tokenAssociate( - CONTRACT, - List.of( - NFT_TOKEN_WITH_FIXED_HBAR_FEE, - NFT_TOKEN_WITH_FIXED_TOKEN_FEE, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)), - tokenAssociate( - RECEIVER, - List.of( - NFT_TOKEN_WITH_FIXED_HBAR_FEE, - NFT_TOKEN_WITH_FIXED_TOKEN_FEE, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)), - mintToken(NFT_TOKEN_WITH_FIXED_HBAR_FEE, List.of(META1, META2)), - mintToken(NFT_TOKEN_WITH_FIXED_TOKEN_FEE, List.of(META3, META4)), - mintToken(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, List.of(META5, META6)), - mintToken(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, List.of(META7, META8)), - cryptoTransfer( - movingUnique(NFT_TOKEN_WITH_FIXED_HBAR_FEE, 1L).between(OWNER, CONTRACT)), - cryptoTransfer( - movingUnique(NFT_TOKEN_WITH_FIXED_TOKEN_FEE, 1L).between(OWNER, CONTRACT)), - cryptoTransfer(movingUnique(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, 1L) - .between(OWNER, CONTRACT)), - cryptoTransfer(movingUnique(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, 1L) - .between(OWNER, CONTRACT)), - cryptoTransfer(moving(1L, FUNGIBLE_TOKEN_FEE).between(TOKEN_TREASURY, CONTRACT)), - cryptoTransfer(moving(1L, FUNGIBLE_TOKEN_FEE).between(TOKEN_TREASURY, RECEIVER)), - cryptoTransfer(TokenMovement.movingHbar(100L).between(OWNER, CONTRACT))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - CONTRACT, - TRANSFER_MULTIPLE_TOKENS, - tokenTransferLists() - .withTokenTransferList( - tokenTransferList() - .forToken(spec.registry() - .getTokenID(NFT_TOKEN_WITH_FIXED_HBAR_FEE)) - .withNftTransfers(nftTransfer( - spec.registry() - .getAccountID(CONTRACT), - spec.registry() - .getAccountID(RECEIVER), - 1L)) - .build(), - tokenTransferList() - .forToken(spec.registry() - .getTokenID(NFT_TOKEN_WITH_FIXED_TOKEN_FEE)) - .withNftTransfers(nftTransfer( - spec.registry() - .getAccountID(CONTRACT), - spec.registry() - .getAccountID(RECEIVER), - 1L)) - .build(), - tokenTransferList() - .forToken( - spec.registry() - .getTokenID( - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK)) - .withNftTransfers(nftTransfer( - spec.registry() - .getAccountID(CONTRACT), - spec.registry() - .getAccountID(RECEIVER), - 1L)) - .build(), - tokenTransferList() - .forToken( - spec.registry() - .getTokenID( - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)) - .withNftTransfers(nftTransfer( - spec.registry() - .getAccountID(CONTRACT), - spec.registry() - .getAccountID(RECEIVER), - 1L)) - .build()) - .build()) - .payingWith(GENESIS) - .alsoSigningWithFullPrefix(RECEIVER_SIGNATURE) - .gas(1_000_000L)))) - .then(); - } - - private HapiSpec hapiTransferFromForNFTWithCustomFeesWithApproveForAll() { - return defaultHapiSpec("HapiTransferFromForNFTWithCustomFeesWithApproveForAll") - .given( - newKeyNamed(MULTI_KEY), - newKeyNamed(RECEIVER_SIGNATURE), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(OWNER) - .balance(ONE_HUNDRED_HBARS) - .maxAutomaticTokenAssociations(5) - .key(MULTI_KEY), - cryptoCreate(SENDER).balance(ONE_HUNDRED_HBARS), - cryptoCreate(RECEIVER).balance(ONE_HUNDRED_HBARS).key(RECEIVER_SIGNATURE), - tokenCreate(NFT_TOKEN_WITH_FIXED_HBAR_FEE) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(OWNER) - .initialSupply(0L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .withCustom(fixedHbarFee(1, OWNER)), - tokenCreate(FUNGIBLE_TOKEN_FEE) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .initialSupply(1000L), - tokenAssociate(SENDER, FUNGIBLE_TOKEN_FEE), - tokenAssociate(OWNER, FUNGIBLE_TOKEN_FEE), - tokenAssociate(RECEIVER, FUNGIBLE_TOKEN_FEE), - tokenCreate(NFT_TOKEN_WITH_FIXED_TOKEN_FEE) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(OWNER) - .initialSupply(0L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .withCustom(fixedHtsFee(1, FUNGIBLE_TOKEN_FEE, OWNER)), - tokenCreate(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(OWNER) - .initialSupply(0L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .withCustom( - royaltyFeeWithFallback(1, 2, fixedHbarFeeInheritingRoyaltyCollector(1), OWNER)), - tokenCreate(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(OWNER) - .initialSupply(0L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .withCustom(royaltyFeeWithFallback( - 1, 2, fixedHtsFeeInheritingRoyaltyCollector(1, FUNGIBLE_TOKEN_FEE), OWNER)), - tokenAssociate( - SENDER, - List.of( - NFT_TOKEN_WITH_FIXED_HBAR_FEE, - NFT_TOKEN_WITH_FIXED_TOKEN_FEE, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)), - tokenAssociate( - RECEIVER, - List.of( - NFT_TOKEN_WITH_FIXED_HBAR_FEE, - NFT_TOKEN_WITH_FIXED_TOKEN_FEE, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)), - mintToken(NFT_TOKEN_WITH_FIXED_HBAR_FEE, List.of(META1, META2)), - mintToken(NFT_TOKEN_WITH_FIXED_TOKEN_FEE, List.of(META3, META4)), - mintToken(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, List.of(META5, META6)), - mintToken(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, List.of(META7, META8)), - cryptoTransfer( - movingUnique(NFT_TOKEN_WITH_FIXED_HBAR_FEE, 1L).between(OWNER, SENDER)), - cryptoTransfer( - movingUnique(NFT_TOKEN_WITH_FIXED_TOKEN_FEE, 1L).between(OWNER, SENDER)), - cryptoTransfer(movingUnique(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, 1L) - .between(OWNER, SENDER)), - cryptoTransfer(movingUnique(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, 1L) - .between(OWNER, SENDER)), - uploadInitCode(HTS_TRANSFER_FROM_CONTRACT), - contractCreate(HTS_TRANSFER_FROM_CONTRACT), - cryptoTransfer(moving(1L, FUNGIBLE_TOKEN_FEE).between(TOKEN_TREASURY, SENDER)), - cryptoTransfer(moving(1L, FUNGIBLE_TOKEN_FEE).between(TOKEN_TREASURY, RECEIVER)), - cryptoApproveAllowance() - .payingWith(DEFAULT_PAYER) - .addNftAllowance( - SENDER, - NFT_TOKEN_WITH_FIXED_HBAR_FEE, - HTS_TRANSFER_FROM_CONTRACT, - true, - List.of()) - .addNftAllowance( - SENDER, - NFT_TOKEN_WITH_FIXED_TOKEN_FEE, - HTS_TRANSFER_FROM_CONTRACT, - true, - List.of()) - .addNftAllowance( - SENDER, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, - HTS_TRANSFER_FROM_CONTRACT, - true, - List.of()) - .addNftAllowance( - SENDER, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, - HTS_TRANSFER_FROM_CONTRACT, - true, - List.of()) - .via(APPROVE_TXN) - .signedBy(DEFAULT_PAYER, SENDER)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - HTS_TRANSFER_FROM_CONTRACT, - HTS_TRANSFER_FROM_NFT, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(NFT_TOKEN_WITH_FIXED_HBAR_FEE))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(SENDER))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(RECEIVER))), - BigInteger.valueOf(1L)) - .payingWith(GENESIS), - contractCall( - HTS_TRANSFER_FROM_CONTRACT, - HTS_TRANSFER_FROM_NFT, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(NFT_TOKEN_WITH_FIXED_TOKEN_FEE))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(SENDER))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(RECEIVER))), - BigInteger.valueOf(1L)) - .payingWith(GENESIS), - contractCall( - HTS_TRANSFER_FROM_CONTRACT, - HTS_TRANSFER_FROM_NFT, - HapiParserUtil.asHeadlongAddress(asAddress(spec.registry() - .getTokenID(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(SENDER))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(RECEIVER))), - BigInteger.valueOf(1L)) - .payingWith(GENESIS) - .alsoSigningWithFullPrefix(RECEIVER_SIGNATURE), - contractCall( - HTS_TRANSFER_FROM_CONTRACT, - HTS_TRANSFER_FROM_NFT, - HapiParserUtil.asHeadlongAddress(asAddress(spec.registry() - .getTokenID(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(SENDER))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(RECEIVER))), - BigInteger.valueOf(1L)) - .payingWith(GENESIS) - .alsoSigningWithFullPrefix(RECEIVER_SIGNATURE)))) - .then(); - } - - private HapiSpec hapiTransferFromForNFTWithCustomFeesWithBothApproveForAllAndAssignedSpender() { - return defaultHapiSpec("HapiTransferFromForNFTWithCustomFeesWithBothApproveForAllAndAssignedSpender") - .given( - newKeyNamed(MULTI_KEY), - newKeyNamed(RECEIVER_SIGNATURE), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(OWNER) - .balance(ONE_HUNDRED_HBARS) - .maxAutomaticTokenAssociations(5) - .key(MULTI_KEY), - cryptoCreate(SENDER).balance(ONE_HUNDRED_HBARS), - cryptoCreate(RECEIVER).balance(ONE_HUNDRED_HBARS).key(RECEIVER_SIGNATURE), - tokenCreate(NFT_TOKEN_WITH_FIXED_HBAR_FEE) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(OWNER) - .initialSupply(0L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .withCustom(fixedHbarFee(1, OWNER)), - tokenCreate(FUNGIBLE_TOKEN_FEE) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .initialSupply(1000L), - tokenAssociate(SENDER, FUNGIBLE_TOKEN_FEE), - tokenAssociate(OWNER, FUNGIBLE_TOKEN_FEE), - tokenAssociate(RECEIVER, FUNGIBLE_TOKEN_FEE), - tokenCreate(NFT_TOKEN_WITH_FIXED_TOKEN_FEE) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(OWNER) - .initialSupply(0L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .withCustom(fixedHtsFee(1, FUNGIBLE_TOKEN_FEE, OWNER)), - tokenCreate(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(OWNER) - .initialSupply(0L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .withCustom( - royaltyFeeWithFallback(1, 2, fixedHbarFeeInheritingRoyaltyCollector(1), OWNER)), - tokenCreate(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(OWNER) - .initialSupply(0L) - .supplyKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .withCustom(royaltyFeeWithFallback( - 1, 2, fixedHtsFeeInheritingRoyaltyCollector(1, FUNGIBLE_TOKEN_FEE), OWNER)), - tokenAssociate( - SENDER, - List.of( - NFT_TOKEN_WITH_FIXED_HBAR_FEE, - NFT_TOKEN_WITH_FIXED_TOKEN_FEE, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)), - tokenAssociate( - RECEIVER, - List.of( - NFT_TOKEN_WITH_FIXED_HBAR_FEE, - NFT_TOKEN_WITH_FIXED_TOKEN_FEE, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)), - mintToken(NFT_TOKEN_WITH_FIXED_HBAR_FEE, List.of(META1, META2)), - mintToken(NFT_TOKEN_WITH_FIXED_TOKEN_FEE, List.of(META3, META4)), - mintToken(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, List.of(META5, META6)), - mintToken(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, List.of(META7, META8)), - cryptoTransfer( - movingUnique(NFT_TOKEN_WITH_FIXED_HBAR_FEE, 1L).between(OWNER, SENDER)), - cryptoTransfer( - movingUnique(NFT_TOKEN_WITH_FIXED_TOKEN_FEE, 1L).between(OWNER, SENDER)), - cryptoTransfer(movingUnique(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, 1L) - .between(OWNER, SENDER)), - cryptoTransfer(movingUnique(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, 1L) - .between(OWNER, SENDER)), - uploadInitCode(HTS_TRANSFER_FROM_CONTRACT), - contractCreate(HTS_TRANSFER_FROM_CONTRACT), - cryptoTransfer(moving(1L, FUNGIBLE_TOKEN_FEE).between(TOKEN_TREASURY, SENDER)), - cryptoTransfer(moving(1L, FUNGIBLE_TOKEN_FEE).between(TOKEN_TREASURY, RECEIVER)), - cryptoApproveAllowance() - .payingWith(DEFAULT_PAYER) - .addNftAllowance( - SENDER, - NFT_TOKEN_WITH_FIXED_HBAR_FEE, - HTS_TRANSFER_FROM_CONTRACT, - true, - List.of(1L)) - .addNftAllowance( - SENDER, - NFT_TOKEN_WITH_FIXED_TOKEN_FEE, - HTS_TRANSFER_FROM_CONTRACT, - true, - List.of(1L)) - .addNftAllowance( - SENDER, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, - HTS_TRANSFER_FROM_CONTRACT, - true, - List.of(1L)) - .addNftAllowance( - SENDER, - NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, - HTS_TRANSFER_FROM_CONTRACT, - true, - List.of(1L)) - .via(APPROVE_TXN) - .signedBy(DEFAULT_PAYER, SENDER)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - HTS_TRANSFER_FROM_CONTRACT, - HTS_TRANSFER_FROM_NFT, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(NFT_TOKEN_WITH_FIXED_HBAR_FEE))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(SENDER))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(RECEIVER))), - BigInteger.valueOf(1L)) - .payingWith(GENESIS), - contractCall( - HTS_TRANSFER_FROM_CONTRACT, - HTS_TRANSFER_FROM_NFT, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(NFT_TOKEN_WITH_FIXED_TOKEN_FEE))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(SENDER))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(RECEIVER))), - BigInteger.valueOf(1L)) - .payingWith(GENESIS), - contractCall( - HTS_TRANSFER_FROM_CONTRACT, - HTS_TRANSFER_FROM_NFT, - HapiParserUtil.asHeadlongAddress(asAddress(spec.registry() - .getTokenID(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(SENDER))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(RECEIVER))), - BigInteger.valueOf(1L)) - .payingWith(GENESIS) - .alsoSigningWithFullPrefix(RECEIVER_SIGNATURE), - contractCall( - HTS_TRANSFER_FROM_CONTRACT, - HTS_TRANSFER_FROM_NFT, - HapiParserUtil.asHeadlongAddress(asAddress(spec.registry() - .getTokenID(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(SENDER))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(RECEIVER))), - BigInteger.valueOf(1L)) - .payingWith(GENESIS) - .alsoSigningWithFullPrefix(RECEIVER_SIGNATURE)))) - .then(); - } - private HapiSpec hapiTransferFromForFungibleTokenWithCustomFeesWithoutApproveFails() { final var FUNGIBLE_TOKEN_WITH_FIXED_HBAR_FEE = "fungibleTokenWithFixedHbarFee"; final var FUNGIBLE_TOKEN_WITH_FIXED_TOKEN_FEE = "fungibleTokenWithFixedTokenFee"; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CryptoTransferHTSV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CryptoTransferHTSV1SecurityModelSuite.java new file mode 100644 index 000000000000..0fc2b308f8ed --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CryptoTransferHTSV1SecurityModelSuite.java @@ -0,0 +1,858 @@ +/* + * Copyright (C) 2021-2023 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.spec.HapiSpec.propertyPreservingHapiSpec; +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.keys.KeyShape.DELEGATE_CONTRACT; +import static com.hedera.services.bdd.spec.keys.KeyShape.sigs; +import static com.hedera.services.bdd.spec.keys.SigControl.ON; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; +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.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoApproveAllowance; +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.cryptoUpdate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; +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.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFeeInheritingRoyaltyCollector; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFeeInheritingRoyaltyCollector; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.royaltyFeeWithFallback; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +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.accountAmount; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.nftTransfer; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.tokenTransferList; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.tokenTransferLists; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.utils.MiscEETUtils.metadata; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; +import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; + +import com.esaulpaugh.headlong.abi.Tuple; +import com.google.protobuf.ByteString; +import com.hedera.node.app.hapi.utils.ByteStringUtils; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.assertions.NonFungibleTransfers; +import com.hedera.services.bdd.spec.assertions.SomeFungibleTransfers; +import com.hedera.services.bdd.spec.keys.KeyShape; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.spec.transactions.token.TokenMovement; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.TokenSupplyType; +import com.hederahashgraph.api.proto.java.TokenType; +import java.math.BigInteger; +import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse +public class CryptoTransferHTSV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(CryptoTransferHTSV1SecurityModelSuite.class); + + private static final long GAS_TO_OFFER = 4_000_000L; + public static final long TOTAL_SUPPLY = 1_000; + private static final String FUNGIBLE_TOKEN = "TokenA"; + private static final String NFT_TOKEN = "Token_NFT"; + + private static final String RECEIVER = "receiver"; + private static final String SENDER = "sender"; + private static final KeyShape DELEGATE_CONTRACT_KEY_SHAPE = + KeyShape.threshOf(1, KeyShape.SIMPLE, DELEGATE_CONTRACT); + + public static final String DELEGATE_KEY = "contractKey"; + private static final String CONTRACT = "CryptoTransfer"; + private static final String MULTI_KEY = "purpose"; + private static final String HTS_TRANSFER_FROM_CONTRACT = "HtsTransferFrom"; + private static final String OWNER = "Owner"; + private static final String HTS_TRANSFER_FROM_NFT = "htsTransferFromNFT"; + public static final String TRANSFER_MULTIPLE_TOKENS = "transferMultipleTokens"; + private static final ByteString META1 = ByteStringUtils.wrapUnsafely("meta1".getBytes()); + private static final ByteString META2 = ByteStringUtils.wrapUnsafely("meta2".getBytes()); + private static final ByteString META3 = ByteStringUtils.wrapUnsafely("meta3".getBytes()); + private static final ByteString META4 = ByteStringUtils.wrapUnsafely("meta4".getBytes()); + private static final ByteString META5 = ByteStringUtils.wrapUnsafely("meta5".getBytes()); + private static final ByteString META6 = ByteStringUtils.wrapUnsafely("meta6".getBytes()); + private static final ByteString META7 = ByteStringUtils.wrapUnsafely("meta7".getBytes()); + private static final ByteString META8 = ByteStringUtils.wrapUnsafely("meta8".getBytes()); + private static final String NFT_TOKEN_WITH_FIXED_HBAR_FEE = "nftTokenWithFixedHbarFee"; + private static final String NFT_TOKEN_WITH_FIXED_TOKEN_FEE = "nftTokenWithFixedTokenFee"; + private static final String NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK = + "nftTokenWithRoyaltyFeeWithHbarFallback"; + private static final String NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK = + "nftTokenWithRoyaltyFeeWithTokenFallback"; + private static final String FUNGIBLE_TOKEN_FEE = "fungibleTokenFee"; + private static final String RECEIVER_SIGNATURE = "receiverSignature"; + private static final String APPROVE_TXN = "approveTxn"; + private static final String FIRST_MEMO = "firstMemo"; + private static final String SECOND_MEMO = "secondMemo"; + private static final String CRYPTO_TRANSFER_TXN = "cryptoTransferTxn"; + + public static void main(final String... args) { + new CryptoTransferHTSV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + public List getSpecsInSuite() { + return List.of( + nonNestedCryptoTransferForFungibleToken(), + activeContractInFrameIsVerifiedWithoutNeedForSignature(), + cryptoTransferNFTsWithCustomFeesMixedScenario(), + hapiTransferFromForNFTWithCustomFeesWithApproveForAll(), + hapiTransferFromForNFTWithCustomFeesWithBothApproveForAllAndAssignedSpender()); + } + + private HapiSpec nonNestedCryptoTransferForFungibleToken() { + final var cryptoTransferTxn = CRYPTO_TRANSFER_TXN; + + return propertyPreservingHapiSpec("nonNestedCryptoTransferForFungibleToken") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(SENDER).balance(10 * ONE_HUNDRED_HBARS), + cryptoCreate(RECEIVER).balance(2 * ONE_HUNDRED_HBARS).receiverSigRequired(true), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(FUNGIBLE_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(TOTAL_SUPPLY) + .treasury(TOKEN_TREASURY), + tokenAssociate(SENDER, List.of(FUNGIBLE_TOKEN)), + tokenAssociate(RECEIVER, List.of(FUNGIBLE_TOKEN)), + cryptoTransfer(moving(200, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, SENDER)), + uploadInitCode(CONTRACT), + contractCreate(CONTRACT)) + .when( + withOpContext((spec, opLog) -> { + final var token = spec.registry().getTokenID(FUNGIBLE_TOKEN); + final var sender = spec.registry().getAccountID(SENDER); + final var receiver = spec.registry().getAccountID(RECEIVER); + final var amountToBeSent = 50L; + + allRunFor( + spec, + newKeyNamed(DELEGATE_KEY) + .shape(DELEGATE_CONTRACT_KEY_SHAPE.signedWith(sigs(ON, CONTRACT))), + cryptoUpdate(SENDER).key(DELEGATE_KEY), + cryptoUpdate(RECEIVER).key(DELEGATE_KEY), + contractCall(CONTRACT, TRANSFER_MULTIPLE_TOKENS, (Object) new Tuple[] { + tokenTransferList() + .forToken(token) + .withAccountAmounts( + accountAmount(sender, -amountToBeSent), + accountAmount(receiver, amountToBeSent)) + .build() + }) + .payingWith(GENESIS) + .via(cryptoTransferTxn) + .gas(GAS_TO_OFFER), + contractCall(CONTRACT, TRANSFER_MULTIPLE_TOKENS, (Object) new Tuple[] { + tokenTransferList() + .forToken(token) + .withAccountAmounts( + accountAmount(sender, -0L), accountAmount(receiver, 0L)) + .build() + }) + .payingWith(GENESIS) + .via("cryptoTransferZero") + .gas(GAS_TO_OFFER)); + }), + getTxnRecord(cryptoTransferTxn).andAllChildRecords().logged(), + getTxnRecord("cryptoTransferZero").andAllChildRecords().logged()) + .then( + getTokenInfo(FUNGIBLE_TOKEN).hasTotalSupply(TOTAL_SUPPLY), + getAccountBalance(RECEIVER).hasTokenBalance(FUNGIBLE_TOKEN, 50), + getAccountBalance(SENDER).hasTokenBalance(FUNGIBLE_TOKEN, 150), + getTokenInfo(FUNGIBLE_TOKEN).logged(), + childRecordsCheck( + cryptoTransferTxn, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)) + .gasUsed(14085L)) + .tokenTransfers(SomeFungibleTransfers.changingFungibleBalances() + .including(FUNGIBLE_TOKEN, SENDER, -50) + .including(FUNGIBLE_TOKEN, RECEIVER, 50)))); + } + + private HapiSpec activeContractInFrameIsVerifiedWithoutNeedForSignature() { + final var revertedFungibleTransferTxn = "revertedFungibleTransferTxn"; + final var successfulFungibleTransferTxn = "successfulFungibleTransferTxn"; + final var revertedNftTransferTxn = "revertedNftTransferTxn"; + final var successfulNftTransferTxn = "successfulNftTransferTxn"; + final var senderStartBalance = 200L; + final var receiverStartBalance = 0L; + final var toSendEachTuple = 50L; + final var multiKey = MULTI_KEY; + final var senderKey = "senderKey"; + final var contractKey = "contractAdminKey"; + + return propertyPreservingHapiSpec("activeContractInFrameIsVerifiedWithoutNeedForSignature") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(multiKey), + newKeyNamed(senderKey), + newKeyNamed(contractKey), + cryptoCreate(SENDER).balance(10 * ONE_HUNDRED_HBARS).key(senderKey), + cryptoCreate(RECEIVER).balance(2 * ONE_HUNDRED_HBARS), + cryptoCreate(TOKEN_TREASURY), + uploadInitCode(CONTRACT), + contractCreate(CONTRACT) + .payingWith(GENESIS) + .adminKey(contractKey) + .gas(GAS_TO_OFFER), + tokenCreate(FUNGIBLE_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(TOTAL_SUPPLY) + .treasury(TOKEN_TREASURY), + tokenCreate(NFT_TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .adminKey(multiKey) + .supplyKey(multiKey) + .supplyType(TokenSupplyType.INFINITE) + .initialSupply(0) + .treasury(TOKEN_TREASURY), + mintToken(NFT_TOKEN, List.of(metadata(FIRST_MEMO), metadata(SECOND_MEMO))), + tokenAssociate(SENDER, List.of(FUNGIBLE_TOKEN, NFT_TOKEN)), + tokenAssociate(RECEIVER, List.of(FUNGIBLE_TOKEN, NFT_TOKEN)), + tokenAssociate(CONTRACT, List.of(FUNGIBLE_TOKEN, NFT_TOKEN)), + cryptoTransfer( + moving(senderStartBalance, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, SENDER)), + cryptoTransfer(movingUnique(NFT_TOKEN, 1L).between(TOKEN_TREASURY, SENDER)), + cryptoTransfer( + moving(senderStartBalance, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, CONTRACT), + movingUnique(NFT_TOKEN, 2L).between(TOKEN_TREASURY, CONTRACT))) + .when(withOpContext((spec, opLog) -> { + final var token = spec.registry().getTokenID(FUNGIBLE_TOKEN); + final var nftToken = spec.registry().getTokenID(NFT_TOKEN); + final var sender = spec.registry().getAccountID(SENDER); + final var receiver = spec.registry().getAccountID(RECEIVER); + final var contractId = spec.registry().getAccountID(CONTRACT); + allRunFor( + spec, + contractCall(CONTRACT, TRANSFER_MULTIPLE_TOKENS, (Object) new Tuple[] { + tokenTransferList() + .forToken(token) + .withAccountAmounts( + accountAmount(contractId, -toSendEachTuple), + accountAmount(receiver, toSendEachTuple)) + .build(), + tokenTransferList() + .forToken(token) + .withAccountAmounts( + accountAmount(sender, -toSendEachTuple), + accountAmount(receiver, toSendEachTuple)) + .build() + }) + .payingWith(GENESIS) + .via(revertedFungibleTransferTxn) + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall(CONTRACT, TRANSFER_MULTIPLE_TOKENS, (Object) new Tuple[] { + tokenTransferList() + .forToken(token) + .withAccountAmounts( + accountAmount(contractId, -toSendEachTuple), + accountAmount(receiver, toSendEachTuple)) + .build(), + tokenTransferList() + .forToken(token) + .withAccountAmounts( + accountAmount(sender, -toSendEachTuple), + accountAmount(receiver, toSendEachTuple)) + .build() + }) + .payingWith(GENESIS) + .alsoSigningWithFullPrefix(senderKey) + .via(successfulFungibleTransferTxn) + .gas(GAS_TO_OFFER) + .hasKnownStatus(SUCCESS), + contractCall(CONTRACT, TRANSFER_MULTIPLE_TOKENS, (Object) new Tuple[] { + tokenTransferList() + .forToken(nftToken) + .withNftTransfers(nftTransfer(contractId, receiver, 2L)) + .build(), + tokenTransferList() + .forToken(nftToken) + .withNftTransfers(nftTransfer(sender, receiver, 1L)) + .build() + }) + .payingWith(GENESIS) + .via(revertedNftTransferTxn) + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall(CONTRACT, TRANSFER_MULTIPLE_TOKENS, (Object) new Tuple[] { + tokenTransferList() + .forToken(nftToken) + .withNftTransfers(nftTransfer(contractId, receiver, 2L)) + .build(), + tokenTransferList() + .forToken(nftToken) + .withNftTransfers(nftTransfer(sender, receiver, 1L)) + .build() + }) + .payingWith(GENESIS) + .via(successfulNftTransferTxn) + .alsoSigningWithFullPrefix(senderKey) + .gas(GAS_TO_OFFER) + .hasKnownStatus(SUCCESS)); + })) + .then( + getAccountBalance(RECEIVER) + .hasTokenBalance(FUNGIBLE_TOKEN, receiverStartBalance + 2 * toSendEachTuple) + .hasTokenBalance(NFT_TOKEN, 2L), + getAccountBalance(SENDER) + .hasTokenBalance(FUNGIBLE_TOKEN, senderStartBalance - toSendEachTuple) + .hasTokenBalance(NFT_TOKEN, 0L), + getAccountBalance(CONTRACT) + .hasTokenBalance(FUNGIBLE_TOKEN, senderStartBalance - toSendEachTuple) + .hasTokenBalance(NFT_TOKEN, 0L), + childRecordsCheck( + revertedFungibleTransferTxn, + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .withStatus(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))), + childRecordsCheck( + successfulFungibleTransferTxn, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))) + .tokenTransfers(SomeFungibleTransfers.changingFungibleBalances() + .including(FUNGIBLE_TOKEN, SENDER, -toSendEachTuple) + .including(FUNGIBLE_TOKEN, CONTRACT, -toSendEachTuple) + .including(FUNGIBLE_TOKEN, RECEIVER, 2 * toSendEachTuple))), + childRecordsCheck( + revertedNftTransferTxn, + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .withStatus(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))), + childRecordsCheck( + successfulNftTransferTxn, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))) + .tokenTransfers(NonFungibleTransfers.changingNFTBalances() + .including(NFT_TOKEN, SENDER, RECEIVER, 1L) + .including(NFT_TOKEN, CONTRACT, RECEIVER, 2L)))); + } + + private HapiSpec cryptoTransferNFTsWithCustomFeesMixedScenario() { + final var SPENDER_SIGNATURE = "spenderSignature"; + return propertyPreservingHapiSpec("cryptoTransferNFTsWithCustomFeesMixedScenario") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + newKeyNamed(RECEIVER_SIGNATURE), + newKeyNamed(SPENDER_SIGNATURE), + uploadInitCode(CONTRACT), + contractCreate(CONTRACT), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(OWNER) + .balance(ONE_HUNDRED_HBARS) + .maxAutomaticTokenAssociations(5) + .key(MULTI_KEY), + cryptoCreate(SENDER).balance(ONE_HUNDRED_HBARS), + cryptoCreate(RECEIVER).balance(ONE_HUNDRED_HBARS).key(RECEIVER_SIGNATURE), + tokenCreate(NFT_TOKEN_WITH_FIXED_HBAR_FEE) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(OWNER) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .withCustom(fixedHbarFee(1, OWNER)), + tokenCreate(FUNGIBLE_TOKEN_FEE) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .initialSupply(1000L), + tokenAssociate(CONTRACT, FUNGIBLE_TOKEN_FEE), + tokenAssociate(OWNER, FUNGIBLE_TOKEN_FEE), + tokenAssociate(RECEIVER, FUNGIBLE_TOKEN_FEE), + tokenCreate(NFT_TOKEN_WITH_FIXED_TOKEN_FEE) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(OWNER) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .withCustom(fixedHtsFee(1, FUNGIBLE_TOKEN_FEE, OWNER)), + tokenCreate(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(OWNER) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .withCustom( + royaltyFeeWithFallback(1, 2, fixedHbarFeeInheritingRoyaltyCollector(1), OWNER)), + tokenCreate(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(OWNER) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .withCustom(royaltyFeeWithFallback( + 1, 2, fixedHtsFeeInheritingRoyaltyCollector(1, FUNGIBLE_TOKEN_FEE), OWNER)), + tokenAssociate( + CONTRACT, + List.of( + NFT_TOKEN_WITH_FIXED_HBAR_FEE, + NFT_TOKEN_WITH_FIXED_TOKEN_FEE, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)), + tokenAssociate( + RECEIVER, + List.of( + NFT_TOKEN_WITH_FIXED_HBAR_FEE, + NFT_TOKEN_WITH_FIXED_TOKEN_FEE, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)), + mintToken(NFT_TOKEN_WITH_FIXED_HBAR_FEE, List.of(META1, META2)), + mintToken(NFT_TOKEN_WITH_FIXED_TOKEN_FEE, List.of(META3, META4)), + mintToken(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, List.of(META5, META6)), + mintToken(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, List.of(META7, META8)), + cryptoTransfer( + movingUnique(NFT_TOKEN_WITH_FIXED_HBAR_FEE, 1L).between(OWNER, CONTRACT)), + cryptoTransfer( + movingUnique(NFT_TOKEN_WITH_FIXED_TOKEN_FEE, 1L).between(OWNER, CONTRACT)), + cryptoTransfer(movingUnique(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, 1L) + .between(OWNER, CONTRACT)), + cryptoTransfer(movingUnique(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, 1L) + .between(OWNER, CONTRACT)), + cryptoTransfer(moving(1L, FUNGIBLE_TOKEN_FEE).between(TOKEN_TREASURY, CONTRACT)), + cryptoTransfer(moving(1L, FUNGIBLE_TOKEN_FEE).between(TOKEN_TREASURY, RECEIVER)), + cryptoTransfer(TokenMovement.movingHbar(100L).between(OWNER, CONTRACT))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + CONTRACT, + TRANSFER_MULTIPLE_TOKENS, + tokenTransferLists() + .withTokenTransferList( + tokenTransferList() + .forToken(spec.registry() + .getTokenID(NFT_TOKEN_WITH_FIXED_HBAR_FEE)) + .withNftTransfers(nftTransfer( + spec.registry() + .getAccountID(CONTRACT), + spec.registry() + .getAccountID(RECEIVER), + 1L)) + .build(), + tokenTransferList() + .forToken(spec.registry() + .getTokenID(NFT_TOKEN_WITH_FIXED_TOKEN_FEE)) + .withNftTransfers(nftTransfer( + spec.registry() + .getAccountID(CONTRACT), + spec.registry() + .getAccountID(RECEIVER), + 1L)) + .build(), + tokenTransferList() + .forToken( + spec.registry() + .getTokenID( + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK)) + .withNftTransfers(nftTransfer( + spec.registry() + .getAccountID(CONTRACT), + spec.registry() + .getAccountID(RECEIVER), + 1L)) + .build(), + tokenTransferList() + .forToken( + spec.registry() + .getTokenID( + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)) + .withNftTransfers(nftTransfer( + spec.registry() + .getAccountID(CONTRACT), + spec.registry() + .getAccountID(RECEIVER), + 1L)) + .build()) + .build()) + .payingWith(GENESIS) + .alsoSigningWithFullPrefix(RECEIVER_SIGNATURE) + .gas(1_000_000L)))) + .then(); + } + + private HapiSpec hapiTransferFromForNFTWithCustomFeesWithApproveForAll() { + return propertyPreservingHapiSpec("hapiTransferFromForNFTWithCustomFeesWithApproveForAll") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + newKeyNamed(RECEIVER_SIGNATURE), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(OWNER) + .balance(ONE_HUNDRED_HBARS) + .maxAutomaticTokenAssociations(5) + .key(MULTI_KEY), + cryptoCreate(SENDER).balance(ONE_HUNDRED_HBARS), + cryptoCreate(RECEIVER).balance(ONE_HUNDRED_HBARS).key(RECEIVER_SIGNATURE), + tokenCreate(NFT_TOKEN_WITH_FIXED_HBAR_FEE) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(OWNER) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .withCustom(fixedHbarFee(1, OWNER)), + tokenCreate(FUNGIBLE_TOKEN_FEE) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .initialSupply(1000L), + tokenAssociate(SENDER, FUNGIBLE_TOKEN_FEE), + tokenAssociate(OWNER, FUNGIBLE_TOKEN_FEE), + tokenAssociate(RECEIVER, FUNGIBLE_TOKEN_FEE), + tokenCreate(NFT_TOKEN_WITH_FIXED_TOKEN_FEE) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(OWNER) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .withCustom(fixedHtsFee(1, FUNGIBLE_TOKEN_FEE, OWNER)), + tokenCreate(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(OWNER) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .withCustom( + royaltyFeeWithFallback(1, 2, fixedHbarFeeInheritingRoyaltyCollector(1), OWNER)), + tokenCreate(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(OWNER) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .withCustom(royaltyFeeWithFallback( + 1, 2, fixedHtsFeeInheritingRoyaltyCollector(1, FUNGIBLE_TOKEN_FEE), OWNER)), + tokenAssociate( + SENDER, + List.of( + NFT_TOKEN_WITH_FIXED_HBAR_FEE, + NFT_TOKEN_WITH_FIXED_TOKEN_FEE, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)), + tokenAssociate( + RECEIVER, + List.of( + NFT_TOKEN_WITH_FIXED_HBAR_FEE, + NFT_TOKEN_WITH_FIXED_TOKEN_FEE, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)), + mintToken(NFT_TOKEN_WITH_FIXED_HBAR_FEE, List.of(META1, META2)), + mintToken(NFT_TOKEN_WITH_FIXED_TOKEN_FEE, List.of(META3, META4)), + mintToken(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, List.of(META5, META6)), + mintToken(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, List.of(META7, META8)), + cryptoTransfer( + movingUnique(NFT_TOKEN_WITH_FIXED_HBAR_FEE, 1L).between(OWNER, SENDER)), + cryptoTransfer( + movingUnique(NFT_TOKEN_WITH_FIXED_TOKEN_FEE, 1L).between(OWNER, SENDER)), + cryptoTransfer(movingUnique(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, 1L) + .between(OWNER, SENDER)), + cryptoTransfer(movingUnique(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, 1L) + .between(OWNER, SENDER)), + uploadInitCode(HTS_TRANSFER_FROM_CONTRACT), + contractCreate(HTS_TRANSFER_FROM_CONTRACT), + cryptoTransfer(moving(1L, FUNGIBLE_TOKEN_FEE).between(TOKEN_TREASURY, SENDER)), + cryptoTransfer(moving(1L, FUNGIBLE_TOKEN_FEE).between(TOKEN_TREASURY, RECEIVER)), + cryptoApproveAllowance() + .payingWith(DEFAULT_PAYER) + .addNftAllowance( + SENDER, + NFT_TOKEN_WITH_FIXED_HBAR_FEE, + HTS_TRANSFER_FROM_CONTRACT, + true, + List.of()) + .addNftAllowance( + SENDER, + NFT_TOKEN_WITH_FIXED_TOKEN_FEE, + HTS_TRANSFER_FROM_CONTRACT, + true, + List.of()) + .addNftAllowance( + SENDER, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, + HTS_TRANSFER_FROM_CONTRACT, + true, + List.of()) + .addNftAllowance( + SENDER, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, + HTS_TRANSFER_FROM_CONTRACT, + true, + List.of()) + .via(APPROVE_TXN) + .signedBy(DEFAULT_PAYER, SENDER)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + HTS_TRANSFER_FROM_CONTRACT, + HTS_TRANSFER_FROM_NFT, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(NFT_TOKEN_WITH_FIXED_HBAR_FEE))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(SENDER))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(RECEIVER))), + BigInteger.valueOf(1L)) + .payingWith(GENESIS), + contractCall( + HTS_TRANSFER_FROM_CONTRACT, + HTS_TRANSFER_FROM_NFT, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(NFT_TOKEN_WITH_FIXED_TOKEN_FEE))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(SENDER))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(RECEIVER))), + BigInteger.valueOf(1L)) + .payingWith(GENESIS), + contractCall( + HTS_TRANSFER_FROM_CONTRACT, + HTS_TRANSFER_FROM_NFT, + HapiParserUtil.asHeadlongAddress(asAddress(spec.registry() + .getTokenID(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(SENDER))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(RECEIVER))), + BigInteger.valueOf(1L)) + .payingWith(GENESIS) + .alsoSigningWithFullPrefix(RECEIVER_SIGNATURE), + contractCall( + HTS_TRANSFER_FROM_CONTRACT, + HTS_TRANSFER_FROM_NFT, + HapiParserUtil.asHeadlongAddress(asAddress(spec.registry() + .getTokenID(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(SENDER))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(RECEIVER))), + BigInteger.valueOf(1L)) + .payingWith(GENESIS) + .alsoSigningWithFullPrefix(RECEIVER_SIGNATURE)))) + .then(); + } + + private HapiSpec hapiTransferFromForNFTWithCustomFeesWithBothApproveForAllAndAssignedSpender() { + return propertyPreservingHapiSpec("hapiTransferFromForNFTWithCustomFeesWithBothApproveForAllAndAssignedSpender") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + newKeyNamed(RECEIVER_SIGNATURE), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(OWNER) + .balance(ONE_HUNDRED_HBARS) + .maxAutomaticTokenAssociations(5) + .key(MULTI_KEY), + cryptoCreate(SENDER).balance(ONE_HUNDRED_HBARS), + cryptoCreate(RECEIVER).balance(ONE_HUNDRED_HBARS).key(RECEIVER_SIGNATURE), + tokenCreate(NFT_TOKEN_WITH_FIXED_HBAR_FEE) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(OWNER) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .withCustom(fixedHbarFee(1, OWNER)), + tokenCreate(FUNGIBLE_TOKEN_FEE) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .initialSupply(1000L), + tokenAssociate(SENDER, FUNGIBLE_TOKEN_FEE), + tokenAssociate(OWNER, FUNGIBLE_TOKEN_FEE), + tokenAssociate(RECEIVER, FUNGIBLE_TOKEN_FEE), + tokenCreate(NFT_TOKEN_WITH_FIXED_TOKEN_FEE) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(OWNER) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .withCustom(fixedHtsFee(1, FUNGIBLE_TOKEN_FEE, OWNER)), + tokenCreate(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(OWNER) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .withCustom( + royaltyFeeWithFallback(1, 2, fixedHbarFeeInheritingRoyaltyCollector(1), OWNER)), + tokenCreate(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(OWNER) + .initialSupply(0L) + .supplyKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .withCustom(royaltyFeeWithFallback( + 1, 2, fixedHtsFeeInheritingRoyaltyCollector(1, FUNGIBLE_TOKEN_FEE), OWNER)), + tokenAssociate( + SENDER, + List.of( + NFT_TOKEN_WITH_FIXED_HBAR_FEE, + NFT_TOKEN_WITH_FIXED_TOKEN_FEE, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)), + tokenAssociate( + RECEIVER, + List.of( + NFT_TOKEN_WITH_FIXED_HBAR_FEE, + NFT_TOKEN_WITH_FIXED_TOKEN_FEE, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK)), + mintToken(NFT_TOKEN_WITH_FIXED_HBAR_FEE, List.of(META1, META2)), + mintToken(NFT_TOKEN_WITH_FIXED_TOKEN_FEE, List.of(META3, META4)), + mintToken(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, List.of(META5, META6)), + mintToken(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, List.of(META7, META8)), + cryptoTransfer( + movingUnique(NFT_TOKEN_WITH_FIXED_HBAR_FEE, 1L).between(OWNER, SENDER)), + cryptoTransfer( + movingUnique(NFT_TOKEN_WITH_FIXED_TOKEN_FEE, 1L).between(OWNER, SENDER)), + cryptoTransfer(movingUnique(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, 1L) + .between(OWNER, SENDER)), + cryptoTransfer(movingUnique(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, 1L) + .between(OWNER, SENDER)), + uploadInitCode(HTS_TRANSFER_FROM_CONTRACT), + contractCreate(HTS_TRANSFER_FROM_CONTRACT), + cryptoTransfer(moving(1L, FUNGIBLE_TOKEN_FEE).between(TOKEN_TREASURY, SENDER)), + cryptoTransfer(moving(1L, FUNGIBLE_TOKEN_FEE).between(TOKEN_TREASURY, RECEIVER)), + cryptoApproveAllowance() + .payingWith(DEFAULT_PAYER) + .addNftAllowance( + SENDER, + NFT_TOKEN_WITH_FIXED_HBAR_FEE, + HTS_TRANSFER_FROM_CONTRACT, + true, + List.of(1L)) + .addNftAllowance( + SENDER, + NFT_TOKEN_WITH_FIXED_TOKEN_FEE, + HTS_TRANSFER_FROM_CONTRACT, + true, + List.of(1L)) + .addNftAllowance( + SENDER, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK, + HTS_TRANSFER_FROM_CONTRACT, + true, + List.of(1L)) + .addNftAllowance( + SENDER, + NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK, + HTS_TRANSFER_FROM_CONTRACT, + true, + List.of(1L)) + .via(APPROVE_TXN) + .signedBy(DEFAULT_PAYER, SENDER)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + HTS_TRANSFER_FROM_CONTRACT, + HTS_TRANSFER_FROM_NFT, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(NFT_TOKEN_WITH_FIXED_HBAR_FEE))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(SENDER))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(RECEIVER))), + BigInteger.valueOf(1L)) + .payingWith(GENESIS), + contractCall( + HTS_TRANSFER_FROM_CONTRACT, + HTS_TRANSFER_FROM_NFT, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(NFT_TOKEN_WITH_FIXED_TOKEN_FEE))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(SENDER))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(RECEIVER))), + BigInteger.valueOf(1L)) + .payingWith(GENESIS), + contractCall( + HTS_TRANSFER_FROM_CONTRACT, + HTS_TRANSFER_FROM_NFT, + HapiParserUtil.asHeadlongAddress(asAddress(spec.registry() + .getTokenID(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_HBAR_FALLBACK))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(SENDER))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(RECEIVER))), + BigInteger.valueOf(1L)) + .payingWith(GENESIS) + .alsoSigningWithFullPrefix(RECEIVER_SIGNATURE), + contractCall( + HTS_TRANSFER_FROM_CONTRACT, + HTS_TRANSFER_FROM_NFT, + HapiParserUtil.asHeadlongAddress(asAddress(spec.registry() + .getTokenID(NFT_TOKEN_WITH_ROYALTY_FEE_WITH_TOKEN_FALLBACK))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(SENDER))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(RECEIVER))), + BigInteger.valueOf(1L)) + .payingWith(GENESIS) + .alsoSigningWithFullPrefix(RECEIVER_SIGNATURE)))) + .then(); + } + + @Override + protected Logger getResultsLogger() { + return log; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DelegatePrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DelegatePrecompileSuite.java index a7916189841a..3e9bacb1e70c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DelegatePrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DelegatePrecompileSuite.java @@ -43,6 +43,7 @@ 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.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.getNestedContractAddress; import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; @@ -60,7 +61,6 @@ import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; public class DelegatePrecompileSuite extends HapiSuite { private static final Logger log = LogManager.getLogger(DelegatePrecompileSuite.class); @@ -246,11 +246,6 @@ OUTER_CONTRACT, asHeadlongAddress(getNestedContractAddress(NESTED_CONTRACT, spec getAccountBalance(TOKEN_TREASURY).hasTokenBalance(VANILLA_TOKEN, 51)); } - @NotNull - private String getNestedContractAddress(final String outerContract, final HapiSpec spec) { - return AssociatePrecompileSuite.getNestedContractAddress(outerContract, spec); - } - @Override protected Logger getResultsLogger() { return log; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DeleteTokenPrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DeleteTokenPrecompileSuite.java index 8278a20d88ef..8fdd5b5049dd 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DeleteTokenPrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DeleteTokenPrecompileSuite.java @@ -16,57 +16,15 @@ package com.hedera.services.bdd.suites.contract.precompile; -import static com.google.protobuf.ByteString.copyFromUtf8; -import static com.hedera.services.bdd.spec.HapiPropertySource.asToken; -import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; -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.getTokenInfo; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; -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.cryptoUpdate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; -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.spec.transactions.TxnVerbs.uploadInitCode; -import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; -import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; -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.childRecordsCheck; -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.asHexedAddress; -import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; -import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_WAS_DELETED; -import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; -import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; - import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.suites.HapiSuite; -import com.hederahashgraph.api.proto.java.AccountID; -import com.hederahashgraph.api.proto.java.TokenID; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class DeleteTokenPrecompileSuite extends HapiSuite { private static final Logger log = LogManager.getLogger(DeleteTokenPrecompileSuite.class); - private static final long GAS_TO_OFFER = 4_000_000L; - public static final String DELETE_TOKEN_CONTRACT = "DeleteTokenContract"; - public static final String TOKEN_DELETE_FUNCTION = "tokenDelete"; - private static final String ACCOUNT = "anybody"; - private static final String MULTI_KEY = "purpose"; - private static final String DELETE_TXN = "deleteTxn"; - final AtomicReference accountID = new AtomicReference<>(); - public static void main(String... args) { new DeleteTokenPrecompileSuite().runSuiteAsync(); } @@ -78,112 +36,7 @@ public boolean canRunConcurrent() { @Override public List getSpecsInSuite() { - return List.of(deleteFungibleTokenWithNegativeCases(), deleteNftTokenWithNegativeCases()); - } - - private HapiSpec deleteFungibleTokenWithNegativeCases() { - final AtomicReference vanillaTokenID = new AtomicReference<>(); - final var tokenAlreadyDeletedTxn = "tokenAlreadyDeletedTxn"; - - return defaultHapiSpec("deleteFungibleTokenWithNegativeCases") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(ACCOUNT) - .key(MULTI_KEY) - .balance(100 * ONE_HBAR) - .exposingCreatedIdTo(accountID::set), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))) - .initialSupply(1110), - uploadInitCode(DELETE_TOKEN_CONTRACT), - contractCreate(DELETE_TOKEN_CONTRACT), - tokenAssociate(ACCOUNT, VANILLA_TOKEN), - cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - DELETE_TOKEN_CONTRACT, - TOKEN_DELETE_FUNCTION, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .gas(GAS_TO_OFFER) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via(DELETE_TXN), - getTokenInfo(VANILLA_TOKEN).isDeleted().logged(), - cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT)) - .hasKnownStatus(TOKEN_WAS_DELETED), - contractCall( - DELETE_TOKEN_CONTRACT, - TOKEN_DELETE_FUNCTION, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .gas(GAS_TO_OFFER) - .via(tokenAlreadyDeletedTxn) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) - .then(childRecordsCheck( - tokenAlreadyDeletedTxn, - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(TOKEN_WAS_DELETED) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(TOKEN_WAS_DELETED))))); - } - - private HapiSpec deleteNftTokenWithNegativeCases() { - final AtomicReference vanillaTokenID = new AtomicReference<>(); - final var notAnAdminTxn = "notAnAdminTxn"; - - return defaultHapiSpec("deleteNftTokenWithNegativeCases") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(ACCOUNT).balance(100 * ONE_HBAR).exposingCreatedIdTo(accountID::set), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(VANILLA_TOKEN) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))) - .initialSupply(0), - mintToken(VANILLA_TOKEN, List.of(copyFromUtf8("First!"))), - uploadInitCode(DELETE_TOKEN_CONTRACT), - contractCreate(DELETE_TOKEN_CONTRACT), - tokenAssociate(ACCOUNT, VANILLA_TOKEN), - cryptoTransfer(movingUnique(VANILLA_TOKEN, 1L).between(TOKEN_TREASURY, ACCOUNT))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - DELETE_TOKEN_CONTRACT, - TOKEN_DELETE_FUNCTION, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .gas(GAS_TO_OFFER) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via(notAnAdminTxn) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - cryptoUpdate(ACCOUNT).key(MULTI_KEY), - contractCall( - DELETE_TOKEN_CONTRACT, - TOKEN_DELETE_FUNCTION, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER), - getTokenInfo(VANILLA_TOKEN).isDeleted().logged()))) - .then(childRecordsCheck( - notAnAdminTxn, - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_SIGNATURE) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_SIGNATURE))))); + return List.of(); } @Override diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DeleteTokenPrecompileV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DeleteTokenPrecompileV1SecurityModelSuite.java new file mode 100644 index 000000000000..e176dc5b4aca --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DeleteTokenPrecompileV1SecurityModelSuite.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2022-2023 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.google.protobuf.ByteString.copyFromUtf8; +import static com.hedera.services.bdd.spec.HapiPropertySource.asToken; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +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.getTokenInfo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +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.cryptoUpdate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; +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.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +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.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asHexedAddress; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_WAS_DELETED; +import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; +import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; + +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.TokenID; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class DeleteTokenPrecompileV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(DeleteTokenPrecompileV1SecurityModelSuite.class); + + private static final long GAS_TO_OFFER = 4_000_000L; + public static final String DELETE_TOKEN_CONTRACT = "DeleteTokenContract"; + public static final String TOKEN_DELETE_FUNCTION = "tokenDelete"; + private static final String ACCOUNT = "anybody"; + private static final String MULTI_KEY = "purpose"; + private static final String DELETE_TXN = "deleteTxn"; + final AtomicReference accountID = new AtomicReference<>(); + + public static void main(String... args) { + new DeleteTokenPrecompileV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return true; + } + + @Override + public List getSpecsInSuite() { + return List.of(deleteFungibleTokenWithNegativeCases(), deleteNftTokenWithNegativeCases()); + } + + private HapiSpec deleteFungibleTokenWithNegativeCases() { + final AtomicReference vanillaTokenID = new AtomicReference<>(); + final var tokenAlreadyDeletedTxn = "tokenAlreadyDeletedTxn"; + + return propertyPreservingHapiSpec("deleteFungibleTokenWithNegativeCases") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenDelete", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(ACCOUNT) + .key(MULTI_KEY) + .balance(100 * ONE_HBAR) + .exposingCreatedIdTo(accountID::set), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))) + .initialSupply(1110), + uploadInitCode(DELETE_TOKEN_CONTRACT), + contractCreate(DELETE_TOKEN_CONTRACT), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + DELETE_TOKEN_CONTRACT, + TOKEN_DELETE_FUNCTION, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .gas(GAS_TO_OFFER) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via(DELETE_TXN), + getTokenInfo(VANILLA_TOKEN).isDeleted().logged(), + cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT)) + .hasKnownStatus(TOKEN_WAS_DELETED), + contractCall( + DELETE_TOKEN_CONTRACT, + TOKEN_DELETE_FUNCTION, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .gas(GAS_TO_OFFER) + .via(tokenAlreadyDeletedTxn) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) + .then(childRecordsCheck( + tokenAlreadyDeletedTxn, + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(TOKEN_WAS_DELETED) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(TOKEN_WAS_DELETED))))); + } + + private HapiSpec deleteNftTokenWithNegativeCases() { + final AtomicReference vanillaTokenID = new AtomicReference<>(); + final var notAnAdminTxn = "notAnAdminTxn"; + + return propertyPreservingHapiSpec("deleteNftTokenWithNegativeCases") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenDelete", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(ACCOUNT).balance(100 * ONE_HBAR).exposingCreatedIdTo(accountID::set), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(VANILLA_TOKEN) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))) + .initialSupply(0), + mintToken(VANILLA_TOKEN, List.of(copyFromUtf8("First!"))), + uploadInitCode(DELETE_TOKEN_CONTRACT), + contractCreate(DELETE_TOKEN_CONTRACT), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + cryptoTransfer(movingUnique(VANILLA_TOKEN, 1L).between(TOKEN_TREASURY, ACCOUNT))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + DELETE_TOKEN_CONTRACT, + TOKEN_DELETE_FUNCTION, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .gas(GAS_TO_OFFER) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via(notAnAdminTxn) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + cryptoUpdate(ACCOUNT).key(MULTI_KEY), + contractCall( + DELETE_TOKEN_CONTRACT, + TOKEN_DELETE_FUNCTION, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER), + getTokenInfo(VANILLA_TOKEN).isDeleted().logged()))) + .then(childRecordsCheck( + notAnAdminTxn, + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_SIGNATURE) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_SIGNATURE))))); + } + + @Override + protected Logger getResultsLogger() { + return log; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DissociatePrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DissociatePrecompileSuite.java index 7a788317b84b..9ddfdf9964c9 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DissociatePrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DissociatePrecompileSuite.java @@ -16,69 +16,15 @@ package com.hedera.services.bdd.suites.contract.precompile; -import static com.hedera.services.bdd.spec.HapiPropertySource.asDotDelimitedLongArray; -import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; -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.getAccountBalance; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; -import static com.hedera.services.bdd.spec.queries.crypto.ExpectedTokenRel.relationshipWith; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; -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.mintToken; -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.spec.transactions.TxnVerbs.tokenDelete; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenFreeze; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenUnfreeze; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; -import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; -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.childRecordsCheck; -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.asAddress; -import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.FREEZABLE_TOKEN_ON_BY_DEFAULT; -import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.KNOWABLE_TOKEN; -import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.TBD_TOKEN; -import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; -import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; -import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; - -import com.esaulpaugh.headlong.abi.Address; -import com.google.protobuf.ByteString; import com.hedera.services.bdd.spec.HapiSpec; -import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; import com.hedera.services.bdd.suites.HapiSuite; -import com.hederahashgraph.api.proto.java.AccountID; -import com.hederahashgraph.api.proto.java.ResponseCodeEnum; -import com.hederahashgraph.api.proto.java.TokenID; -import com.hederahashgraph.api.proto.java.TokenType; -import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; public class DissociatePrecompileSuite extends HapiSuite { private static final Logger log = LogManager.getLogger(DissociatePrecompileSuite.class); - private static final long GAS_TO_OFFER = 2_000_000L; - - private static final long TOTAL_SUPPLY = 1_000; - private static final String TOKEN_TREASURY = "treasury"; - private static final String OUTER_CONTRACT = "NestedAssociateDissociate"; - private static final String NESTED_CONTRACT = "AssociateDissociate"; - private static final String CONTRACT = "AssociateDissociate"; - private static final String ACCOUNT = "anybody"; - private static final String MULTI_KEY = "Multi key"; - public static void main(String... args) { new DissociatePrecompileSuite().runSuiteAsync(); } @@ -90,303 +36,11 @@ public boolean canRunConcurrent() { @Override public List getSpecsInSuite() { - return List.of( - dissociatePrecompileHasExpectedSemanticsForDeletedTokens(), - nestedDissociateWorksAsExpected(), - multiplePrecompileDissociationWithSigsForFungibleWorks()); - } - - /* -- Not specifically required in the HTS Precompile Test Plan -- */ - public HapiSpec dissociatePrecompileHasExpectedSemanticsForDeletedTokens() { - final var tbdUniqToken = "UniqToBeDeleted"; - final var zeroBalanceFrozen = "0bFrozen"; - final var zeroBalanceUnfrozen = "0bUnfrozen"; - final var nonZeroBalanceFrozen = "1bFrozen"; - final var nonZeroBalanceUnfrozen = "1bUnfrozen"; - final var initialSupply = 100L; - final var nonZeroXfer = 10L; - final var firstMeta = ByteString.copyFrom("FIRST".getBytes(StandardCharsets.UTF_8)); - final var secondMeta = ByteString.copyFrom("SECOND".getBytes(StandardCharsets.UTF_8)); - final var thirdMeta = ByteString.copyFrom("THIRD".getBytes(StandardCharsets.UTF_8)); - - final AtomicReference accountID = new AtomicReference<>(); - final AtomicReference treasuryID = new AtomicReference<>(); - final AtomicReference zeroBalanceFrozenID = new AtomicReference<>(); - final AtomicReference zeroBalanceUnfrozenID = new AtomicReference<>(); - final AtomicReference nonZeroBalanceFrozenID = new AtomicReference<>(); - final AtomicReference nonZeroBalanceUnfrozenID = new AtomicReference<>(); - final AtomicReference tbdTokenID = new AtomicReference<>(); - final AtomicReference tbdUniqueTokenID = new AtomicReference<>(); - - return defaultHapiSpec("DissociatePrecompileHasExpectedSemanticsForDeletedTokens") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).exposingCreatedIdTo(accountID::set), - cryptoCreate(TOKEN_TREASURY) - .balance(10 * ONE_HUNDRED_HBARS) - .exposingCreatedIdTo(treasuryID::set), - tokenCreate(TBD_TOKEN) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .initialSupply(initialSupply) - .treasury(TOKEN_TREASURY) - .freezeKey(MULTI_KEY) - .freezeDefault(true) - .exposingCreatedIdTo(id -> tbdTokenID.set(asToken(id))), - tokenCreate(tbdUniqToken) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .initialSupply(0) - .exposingCreatedIdTo(id -> tbdUniqueTokenID.set(asToken(id))), - cryptoCreate(zeroBalanceFrozen) - .balance(10 * ONE_HUNDRED_HBARS) - .exposingCreatedIdTo(zeroBalanceFrozenID::set), - cryptoCreate(zeroBalanceUnfrozen) - .balance(10 * ONE_HUNDRED_HBARS) - .exposingCreatedIdTo(zeroBalanceUnfrozenID::set), - cryptoCreate(nonZeroBalanceFrozen) - .balance(10 * ONE_HUNDRED_HBARS) - .exposingCreatedIdTo(nonZeroBalanceFrozenID::set), - cryptoCreate(nonZeroBalanceUnfrozen) - .balance(10 * ONE_HUNDRED_HBARS) - .exposingCreatedIdTo(nonZeroBalanceUnfrozenID::set), - uploadInitCode(CONTRACT), - contractCreate(CONTRACT)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - tokenAssociate(zeroBalanceFrozen, TBD_TOKEN), - tokenAssociate(zeroBalanceUnfrozen, TBD_TOKEN), - tokenAssociate(nonZeroBalanceFrozen, TBD_TOKEN), - tokenAssociate(nonZeroBalanceUnfrozen, TBD_TOKEN), - mintToken(tbdUniqToken, List.of(firstMeta, secondMeta, thirdMeta)), - getAccountInfo(TOKEN_TREASURY).hasOwnedNfts(3), - tokenUnfreeze(TBD_TOKEN, zeroBalanceUnfrozen), - tokenUnfreeze(TBD_TOKEN, nonZeroBalanceUnfrozen), - tokenUnfreeze(TBD_TOKEN, nonZeroBalanceFrozen), - cryptoTransfer(moving(nonZeroXfer, TBD_TOKEN).between(TOKEN_TREASURY, nonZeroBalanceFrozen)), - cryptoTransfer(moving(nonZeroXfer, TBD_TOKEN).between(TOKEN_TREASURY, nonZeroBalanceUnfrozen)), - tokenFreeze(TBD_TOKEN, nonZeroBalanceFrozen), - getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TBD_TOKEN, initialSupply - 2 * nonZeroXfer), - tokenDelete(TBD_TOKEN), - tokenDelete(tbdUniqToken), - contractCall( - CONTRACT, - "tokenDissociate", - HapiParserUtil.asHeadlongAddress(asAddress(zeroBalanceFrozenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(tbdTokenID.get()))) - .alsoSigningWithFullPrefix(zeroBalanceFrozen) - .gas(GAS_TO_OFFER) - .via("dissociateZeroBalanceFrozenTxn"), - getTxnRecord("dissociateZeroBalanceFrozenTxn") - .andAllChildRecords() - .logged(), - contractCall( - CONTRACT, - "tokenDissociate", - HapiParserUtil.asHeadlongAddress(asAddress(zeroBalanceUnfrozenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(tbdTokenID.get()))) - .alsoSigningWithFullPrefix(zeroBalanceUnfrozen) - .gas(GAS_TO_OFFER) - .via("dissociateZeroBalanceUnfrozenTxn"), - getTxnRecord("dissociateZeroBalanceUnfrozenTxn") - .andAllChildRecords() - .logged(), - contractCall( - CONTRACT, - "tokenDissociate", - HapiParserUtil.asHeadlongAddress(asAddress(nonZeroBalanceFrozenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(tbdTokenID.get()))) - .alsoSigningWithFullPrefix(nonZeroBalanceFrozen) - .gas(GAS_TO_OFFER) - .via("dissociateNonZeroBalanceFrozenTxn"), - getTxnRecord("dissociateNonZeroBalanceFrozenTxn") - .andAllChildRecords() - .logged(), - contractCall( - CONTRACT, - "tokenDissociate", - HapiParserUtil.asHeadlongAddress(asAddress(nonZeroBalanceUnfrozenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(tbdTokenID.get()))) - .alsoSigningWithFullPrefix(nonZeroBalanceUnfrozen) - .gas(GAS_TO_OFFER) - .via("dissociateNonZeroBalanceUnfrozenTxn"), - getTxnRecord("dissociateNonZeroBalanceUnfrozenTxn") - .andAllChildRecords() - .logged(), - contractCall( - CONTRACT, - "tokenDissociate", - HapiParserUtil.asHeadlongAddress(asAddress(treasuryID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(tbdUniqueTokenID.get()))) - .alsoSigningWithFullPrefix(TOKEN_TREASURY) - .gas(GAS_TO_OFFER)))) - .then( - childRecordsCheck( - "dissociateZeroBalanceFrozenTxn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)))), - childRecordsCheck( - "dissociateZeroBalanceUnfrozenTxn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)))), - childRecordsCheck( - "dissociateNonZeroBalanceFrozenTxn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)))), - childRecordsCheck( - "dissociateNonZeroBalanceUnfrozenTxn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)))), - getAccountInfo(zeroBalanceFrozen).hasNoTokenRelationship(TBD_TOKEN), - getAccountInfo(zeroBalanceUnfrozen).hasNoTokenRelationship(TBD_TOKEN), - getAccountInfo(nonZeroBalanceFrozen).hasNoTokenRelationship(TBD_TOKEN), - getAccountInfo(nonZeroBalanceUnfrozen).hasNoTokenRelationship(TBD_TOKEN), - getAccountInfo(TOKEN_TREASURY) - .hasToken(relationshipWith(TBD_TOKEN)) - .hasNoTokenRelationship(tbdUniqToken) - .hasOwnedNfts(0), - getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TBD_TOKEN, initialSupply - 2 * nonZeroXfer)); - } - - /* -- Not specifically required in the HTS Precompile Test Plan -- */ - private HapiSpec nestedDissociateWorksAsExpected() { - final AtomicReference accountID = new AtomicReference<>(); - final AtomicReference vanillaTokenID = new AtomicReference<>(); - - return defaultHapiSpec("nestedDissociateWorksAsExpected") - .given( - cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).exposingCreatedIdTo(accountID::set), - cryptoCreate(TOKEN_TREASURY).balance(0L), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - uploadInitCode(OUTER_CONTRACT, NESTED_CONTRACT), - contractCreate(NESTED_CONTRACT)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCreate( - OUTER_CONTRACT, asHeadlongAddress(getNestedContractAddress(NESTED_CONTRACT, spec))), - tokenAssociate(ACCOUNT, VANILLA_TOKEN), - contractCall( - OUTER_CONTRACT, - "dissociateAssociateContractCall", - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get()))) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("nestedDissociateTxn") - .gas(3_000_000L) - .hasKnownStatus(ResponseCodeEnum.SUCCESS), - getTxnRecord("nestedDissociateTxn").andAllChildRecords().logged()))) - .then( - childRecordsCheck( - "nestedDissociateTxn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)))), - getAccountInfo(ACCOUNT).hasToken(relationshipWith(VANILLA_TOKEN))); - } - - /* -- HSCS-PREC-007 from HTS Precompile Test Plan -- */ - public HapiSpec multiplePrecompileDissociationWithSigsForFungibleWorks() { - final AtomicReference knowableTokenTokenID = new AtomicReference<>(); - final AtomicReference vanillaTokenID = new AtomicReference<>(); - final AtomicReference accountID = new AtomicReference<>(); - final AtomicReference treasuryID = new AtomicReference<>(); - - return defaultHapiSpec("multiplePrecompileDissociationWithSigsForFungibleWorks") - .given( - cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).exposingCreatedIdTo(accountID::set), - cryptoCreate(TOKEN_TREASURY).balance(0L).exposingCreatedIdTo(treasuryID::set), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .initialSupply(TOTAL_SUPPLY) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - tokenCreate(KNOWABLE_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .initialSupply(TOTAL_SUPPLY) - .exposingCreatedIdTo(id -> knowableTokenTokenID.set(asToken(id))), - uploadInitCode(CONTRACT), - contractCreate(CONTRACT)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - tokenAssociate(ACCOUNT, List.of(VANILLA_TOKEN, KNOWABLE_TOKEN)), - getAccountInfo(ACCOUNT).hasToken(relationshipWith(VANILLA_TOKEN)), - getAccountInfo(ACCOUNT).hasToken(relationshipWith(KNOWABLE_TOKEN)), - contractCall( - CONTRACT, - "tokensDissociate", - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - new Address[] { - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(knowableTokenTokenID.get())) - }) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("multipleDissociationTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(SUCCESS), - getTxnRecord("multipleDissociationTxn") - .andAllChildRecords() - .logged()))) - .then( - childRecordsCheck( - "multipleDissociationTxn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)))), - getAccountInfo(ACCOUNT).hasNoTokenRelationship(FREEZABLE_TOKEN_ON_BY_DEFAULT), - getAccountInfo(ACCOUNT).hasNoTokenRelationship(KNOWABLE_TOKEN)); + return List.of(); } @Override protected Logger getResultsLogger() { return log; } - - /* --- Helpers --- */ - - private static TokenID asToken(String v) { - long[] nativeParts = asDotDelimitedLongArray(v); - return TokenID.newBuilder() - .setShardNum(nativeParts[0]) - .setRealmNum(nativeParts[1]) - .setTokenNum(nativeParts[2]) - .build(); - } - - @NotNull - private String getNestedContractAddress(final String outerContract, final HapiSpec spec) { - return AssociatePrecompileSuite.getNestedContractAddress(outerContract, spec); - } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DissociatePrecompileV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DissociatePrecompileV1SecurityModelSuite.java new file mode 100644 index 000000000000..537c6ff8a853 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DissociatePrecompileV1SecurityModelSuite.java @@ -0,0 +1,397 @@ +/* + * Copyright (C) 2021-2023 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.spec.HapiPropertySource.asDotDelimitedLongArray; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +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.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; +import static com.hedera.services.bdd.spec.queries.crypto.ExpectedTokenRel.relationshipWith; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +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.mintToken; +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.spec.transactions.TxnVerbs.tokenDelete; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenFreeze; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenUnfreeze; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; +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.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.getNestedContractAddress; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.FREEZABLE_TOKEN_ON_BY_DEFAULT; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.KNOWABLE_TOKEN; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.TBD_TOKEN; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; + +import com.esaulpaugh.headlong.abi.Address; +import com.google.protobuf.ByteString; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; +import com.hederahashgraph.api.proto.java.TokenID; +import com.hederahashgraph.api.proto.java.TokenType; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse +public class DissociatePrecompileV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(DissociatePrecompileV1SecurityModelSuite.class); + + private static final long GAS_TO_OFFER = 2_000_000L; + + private static final long TOTAL_SUPPLY = 1_000; + private static final String TOKEN_TREASURY = "treasury"; + private static final String OUTER_CONTRACT = "NestedAssociateDissociate"; + private static final String NESTED_CONTRACT = "AssociateDissociate"; + private static final String CONTRACT = "AssociateDissociate"; + private static final String ACCOUNT = "anybody"; + private static final String MULTI_KEY = "Multi key"; + + public static void main(String... args) { + new DissociatePrecompileV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + public List getSpecsInSuite() { + return List.of( + dissociatePrecompileHasExpectedSemanticsForDeletedTokens(), + nestedDissociateWorksAsExpected(), + multiplePrecompileDissociationWithSigsForFungibleWorks()); + } + + /* -- Not specifically required in the HTS Precompile Test Plan -- */ + public HapiSpec dissociatePrecompileHasExpectedSemanticsForDeletedTokens() { + final var tbdUniqToken = "UniqToBeDeleted"; + final var zeroBalanceFrozen = "0bFrozen"; + final var zeroBalanceUnfrozen = "0bUnfrozen"; + final var nonZeroBalanceFrozen = "1bFrozen"; + final var nonZeroBalanceUnfrozen = "1bUnfrozen"; + final var initialSupply = 100L; + final var nonZeroXfer = 10L; + final var firstMeta = ByteString.copyFrom("FIRST".getBytes(StandardCharsets.UTF_8)); + final var secondMeta = ByteString.copyFrom("SECOND".getBytes(StandardCharsets.UTF_8)); + final var thirdMeta = ByteString.copyFrom("THIRD".getBytes(StandardCharsets.UTF_8)); + + final AtomicReference accountID = new AtomicReference<>(); + final AtomicReference treasuryID = new AtomicReference<>(); + final AtomicReference zeroBalanceFrozenID = new AtomicReference<>(); + final AtomicReference zeroBalanceUnfrozenID = new AtomicReference<>(); + final AtomicReference nonZeroBalanceFrozenID = new AtomicReference<>(); + final AtomicReference nonZeroBalanceUnfrozenID = new AtomicReference<>(); + final AtomicReference tbdTokenID = new AtomicReference<>(); + final AtomicReference tbdUniqueTokenID = new AtomicReference<>(); + + return propertyPreservingHapiSpec("dissociatePrecompileHasExpectedSemanticsForDeletedTokens") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).exposingCreatedIdTo(accountID::set), + cryptoCreate(TOKEN_TREASURY) + .balance(10 * ONE_HUNDRED_HBARS) + .exposingCreatedIdTo(treasuryID::set), + tokenCreate(TBD_TOKEN) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .initialSupply(initialSupply) + .treasury(TOKEN_TREASURY) + .freezeKey(MULTI_KEY) + .freezeDefault(true) + .exposingCreatedIdTo(id -> tbdTokenID.set(asToken(id))), + tokenCreate(tbdUniqToken) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .initialSupply(0) + .exposingCreatedIdTo(id -> tbdUniqueTokenID.set(asToken(id))), + cryptoCreate(zeroBalanceFrozen) + .balance(10 * ONE_HUNDRED_HBARS) + .exposingCreatedIdTo(zeroBalanceFrozenID::set), + cryptoCreate(zeroBalanceUnfrozen) + .balance(10 * ONE_HUNDRED_HBARS) + .exposingCreatedIdTo(zeroBalanceUnfrozenID::set), + cryptoCreate(nonZeroBalanceFrozen) + .balance(10 * ONE_HUNDRED_HBARS) + .exposingCreatedIdTo(nonZeroBalanceFrozenID::set), + cryptoCreate(nonZeroBalanceUnfrozen) + .balance(10 * ONE_HUNDRED_HBARS) + .exposingCreatedIdTo(nonZeroBalanceUnfrozenID::set), + uploadInitCode(CONTRACT), + contractCreate(CONTRACT)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + tokenAssociate(zeroBalanceFrozen, TBD_TOKEN), + tokenAssociate(zeroBalanceUnfrozen, TBD_TOKEN), + tokenAssociate(nonZeroBalanceFrozen, TBD_TOKEN), + tokenAssociate(nonZeroBalanceUnfrozen, TBD_TOKEN), + mintToken(tbdUniqToken, List.of(firstMeta, secondMeta, thirdMeta)), + getAccountInfo(TOKEN_TREASURY).hasOwnedNfts(3), + tokenUnfreeze(TBD_TOKEN, zeroBalanceUnfrozen), + tokenUnfreeze(TBD_TOKEN, nonZeroBalanceUnfrozen), + tokenUnfreeze(TBD_TOKEN, nonZeroBalanceFrozen), + cryptoTransfer(moving(nonZeroXfer, TBD_TOKEN).between(TOKEN_TREASURY, nonZeroBalanceFrozen)), + cryptoTransfer(moving(nonZeroXfer, TBD_TOKEN).between(TOKEN_TREASURY, nonZeroBalanceUnfrozen)), + tokenFreeze(TBD_TOKEN, nonZeroBalanceFrozen), + getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TBD_TOKEN, initialSupply - 2 * nonZeroXfer), + tokenDelete(TBD_TOKEN), + tokenDelete(tbdUniqToken), + contractCall( + CONTRACT, + "tokenDissociate", + HapiParserUtil.asHeadlongAddress(asAddress(zeroBalanceFrozenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(tbdTokenID.get()))) + .alsoSigningWithFullPrefix(zeroBalanceFrozen) + .gas(GAS_TO_OFFER) + .via("dissociateZeroBalanceFrozenTxn"), + getTxnRecord("dissociateZeroBalanceFrozenTxn") + .andAllChildRecords() + .logged(), + contractCall( + CONTRACT, + "tokenDissociate", + HapiParserUtil.asHeadlongAddress(asAddress(zeroBalanceUnfrozenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(tbdTokenID.get()))) + .alsoSigningWithFullPrefix(zeroBalanceUnfrozen) + .gas(GAS_TO_OFFER) + .via("dissociateZeroBalanceUnfrozenTxn"), + getTxnRecord("dissociateZeroBalanceUnfrozenTxn") + .andAllChildRecords() + .logged(), + contractCall( + CONTRACT, + "tokenDissociate", + HapiParserUtil.asHeadlongAddress(asAddress(nonZeroBalanceFrozenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(tbdTokenID.get()))) + .alsoSigningWithFullPrefix(nonZeroBalanceFrozen) + .gas(GAS_TO_OFFER) + .via("dissociateNonZeroBalanceFrozenTxn"), + getTxnRecord("dissociateNonZeroBalanceFrozenTxn") + .andAllChildRecords() + .logged(), + contractCall( + CONTRACT, + "tokenDissociate", + HapiParserUtil.asHeadlongAddress(asAddress(nonZeroBalanceUnfrozenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(tbdTokenID.get()))) + .alsoSigningWithFullPrefix(nonZeroBalanceUnfrozen) + .gas(GAS_TO_OFFER) + .via("dissociateNonZeroBalanceUnfrozenTxn"), + getTxnRecord("dissociateNonZeroBalanceUnfrozenTxn") + .andAllChildRecords() + .logged(), + contractCall( + CONTRACT, + "tokenDissociate", + HapiParserUtil.asHeadlongAddress(asAddress(treasuryID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(tbdUniqueTokenID.get()))) + .alsoSigningWithFullPrefix(TOKEN_TREASURY) + .gas(GAS_TO_OFFER)))) + .then( + childRecordsCheck( + "dissociateZeroBalanceFrozenTxn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)))), + childRecordsCheck( + "dissociateZeroBalanceUnfrozenTxn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)))), + childRecordsCheck( + "dissociateNonZeroBalanceFrozenTxn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)))), + childRecordsCheck( + "dissociateNonZeroBalanceUnfrozenTxn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)))), + getAccountInfo(zeroBalanceFrozen).hasNoTokenRelationship(TBD_TOKEN), + getAccountInfo(zeroBalanceUnfrozen).hasNoTokenRelationship(TBD_TOKEN), + getAccountInfo(nonZeroBalanceFrozen).hasNoTokenRelationship(TBD_TOKEN), + getAccountInfo(nonZeroBalanceUnfrozen).hasNoTokenRelationship(TBD_TOKEN), + getAccountInfo(TOKEN_TREASURY) + .hasToken(relationshipWith(TBD_TOKEN)) + .hasNoTokenRelationship(tbdUniqToken) + .hasOwnedNfts(0), + getAccountBalance(TOKEN_TREASURY).hasTokenBalance(TBD_TOKEN, initialSupply - 2 * nonZeroXfer)); + } + + /* -- Not specifically required in the HTS Precompile Test Plan -- */ + private HapiSpec nestedDissociateWorksAsExpected() { + final AtomicReference accountID = new AtomicReference<>(); + final AtomicReference vanillaTokenID = new AtomicReference<>(); + + return propertyPreservingHapiSpec("nestedDissociateWorksAsExpected") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).exposingCreatedIdTo(accountID::set), + cryptoCreate(TOKEN_TREASURY).balance(0L), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + uploadInitCode(OUTER_CONTRACT, NESTED_CONTRACT), + contractCreate(NESTED_CONTRACT)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCreate( + OUTER_CONTRACT, asHeadlongAddress(getNestedContractAddress(NESTED_CONTRACT, spec))), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + contractCall( + OUTER_CONTRACT, + "dissociateAssociateContractCall", + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get()))) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("nestedDissociateTxn") + .gas(3_000_000L) + .hasKnownStatus(ResponseCodeEnum.SUCCESS), + getTxnRecord("nestedDissociateTxn").andAllChildRecords().logged()))) + .then( + childRecordsCheck( + "nestedDissociateTxn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)))), + getAccountInfo(ACCOUNT).hasToken(relationshipWith(VANILLA_TOKEN))); + } + + /* -- HSCS-PREC-007 from HTS Precompile Test Plan -- */ + public HapiSpec multiplePrecompileDissociationWithSigsForFungibleWorks() { + final AtomicReference knowableTokenTokenID = new AtomicReference<>(); + final AtomicReference vanillaTokenID = new AtomicReference<>(); + final AtomicReference accountID = new AtomicReference<>(); + final AtomicReference treasuryID = new AtomicReference<>(); + + return propertyPreservingHapiSpec("multiplePrecompileDissociationWithSigsForFungibleWorks") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(ACCOUNT).balance(10 * ONE_HUNDRED_HBARS).exposingCreatedIdTo(accountID::set), + cryptoCreate(TOKEN_TREASURY).balance(0L).exposingCreatedIdTo(treasuryID::set), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .initialSupply(TOTAL_SUPPLY) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + tokenCreate(KNOWABLE_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .initialSupply(TOTAL_SUPPLY) + .exposingCreatedIdTo(id -> knowableTokenTokenID.set(asToken(id))), + uploadInitCode(CONTRACT), + contractCreate(CONTRACT)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + tokenAssociate(ACCOUNT, List.of(VANILLA_TOKEN, KNOWABLE_TOKEN)), + getAccountInfo(ACCOUNT).hasToken(relationshipWith(VANILLA_TOKEN)), + getAccountInfo(ACCOUNT).hasToken(relationshipWith(KNOWABLE_TOKEN)), + contractCall( + CONTRACT, + "tokensDissociate", + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + new Address[] { + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(knowableTokenTokenID.get())) + }) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("multipleDissociationTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(SUCCESS), + getTxnRecord("multipleDissociationTxn") + .andAllChildRecords() + .logged()))) + .then( + childRecordsCheck( + "multipleDissociationTxn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)))), + getAccountInfo(ACCOUNT).hasNoTokenRelationship(FREEZABLE_TOKEN_ON_BY_DEFAULT), + getAccountInfo(ACCOUNT).hasNoTokenRelationship(KNOWABLE_TOKEN)); + } + + @Override + protected Logger getResultsLogger() { + return log; + } + + /* --- Helpers --- */ + + private static TokenID asToken(String v) { + long[] nativeParts = asDotDelimitedLongArray(v); + return TokenID.newBuilder() + .setShardNum(nativeParts[0]) + .setRealmNum(nativeParts[1]) + .setTokenNum(nativeParts[2]) + .build(); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileSuite.java index fe69971e4e50..b94e41db1ac8 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileSuite.java @@ -16,9 +16,7 @@ package com.hedera.services.bdd.suites.contract.precompile; -import static com.hedera.services.bdd.spec.HapiPropertySource.asContractString; import static com.hedera.services.bdd.spec.HapiPropertySource.asHexedSolidityAddress; -import static com.hedera.services.bdd.spec.HapiPropertySource.contractIdFromHexedMirrorAddress; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; import static com.hedera.services.bdd.spec.assertions.AccountDetailsAsserts.accountDetailsWith; import static com.hedera.services.bdd.spec.assertions.AssertUtils.inOrder; @@ -59,7 +57,6 @@ import static com.hedera.services.bdd.suites.contract.Utils.asAddress; import static com.hedera.services.bdd.suites.contract.Utils.asHexedAddress; import static com.hedera.services.bdd.suites.contract.Utils.asToken; -import static com.hedera.services.bdd.suites.contract.Utils.captureChildCreate2MetaFor; import static com.hedera.services.bdd.suites.contract.Utils.eventSignatureOf; import static com.hedera.services.bdd.suites.contract.Utils.getABIFor; import static com.hedera.services.bdd.suites.contract.Utils.parsedToByteString; @@ -82,12 +79,10 @@ import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; -import com.esaulpaugh.headlong.abi.Address; import com.google.protobuf.ByteString; import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; import com.hedera.services.bdd.spec.HapiPropertySource; import com.hedera.services.bdd.spec.HapiSpec; -import com.hedera.services.bdd.spec.queries.crypto.ExpectedTokenRel; import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; import com.hedera.services.bdd.spec.transactions.token.TokenMovement; import com.hedera.services.bdd.suites.HapiSuite; @@ -199,9 +194,8 @@ List erc20() { someERC20ApproveAllowanceScenarioInOneCall(), getErc20TokenDecimalsFromErc721TokenFails(), transferErc20TokenReceiverContract(), - transferErc20TokenAliasedSender(), - directCallsWorkForERC20(), - erc20TransferFrom(), + directCallsWorkForErc20(), + erc20TransferFromAllowance(), erc20TransferFromSelf(), getErc20TokenNameExceedingLimits(), transferErc20TokenFromContractWithNoApproval()); @@ -688,90 +682,6 @@ private HapiSpec transferErc20TokenReceiverContract() { getAccountBalance(nestedContract).hasTokenBalance(FUNGIBLE_TOKEN, 2)); } - private HapiSpec transferErc20TokenAliasedSender() { - final var aliasedTransferTxn = "aliasedTransferTxn"; - final var addLiquidityTxn = "addLiquidityTxn"; - final var create2Txn = "create2Txn"; - - final var ACCOUNT_A = "AccountA"; - final var ACCOUNT_B = "AccountB"; - - final var ALIASED_TRANSFER = "AliasedTransfer"; - final byte[][] ALIASED_ADDRESS = new byte[1][1]; - - final AtomicReference childMirror = new AtomicReference<>(); - final AtomicReference childEip1014 = new AtomicReference<>(); - - return defaultHapiSpec("ERC_20_TRANSFER_ALIASED_SENDER") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(OWNER), - cryptoCreate(ACCOUNT), - cryptoCreate(ACCOUNT_A).key(MULTI_KEY).balance(ONE_MILLION_HBARS), - cryptoCreate(ACCOUNT_B).balance(ONE_MILLION_HBARS), - tokenCreate(TOKEN_NAME) - .adminKey(MULTI_KEY) - .initialSupply(10000) - .treasury(ACCOUNT_A), - tokenAssociate(ACCOUNT_B, TOKEN_NAME), - uploadInitCode(ALIASED_TRANSFER), - contractCreate(ALIASED_TRANSFER).gas(300_000), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - ALIASED_TRANSFER, - "deployWithCREATE2", - asHeadlongAddress(asHexedAddress( - spec.registry().getTokenID(TOKEN_NAME)))) - .exposingResultTo(result -> { - final var res = (Address) result[0]; - ALIASED_ADDRESS[0] = res.value().toByteArray(); - }) - .payingWith(ACCOUNT) - .alsoSigningWithFullPrefix(MULTI_KEY) - .via(create2Txn) - .gas(GAS_TO_OFFER) - .hasKnownStatus(SUCCESS)))) - .when( - captureChildCreate2MetaFor(2, 0, "setup", create2Txn, childMirror, childEip1014), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - ALIASED_TRANSFER, - "giveTokensToOperator", - HapiParserUtil.asHeadlongAddress(asAddress( - spec.registry().getTokenID(TOKEN_NAME))), - HapiParserUtil.asHeadlongAddress(asAddress( - spec.registry().getAccountID(ACCOUNT_A))), - 1500L) - .payingWith(ACCOUNT) - .alsoSigningWithFullPrefix(MULTI_KEY) - .via(addLiquidityTxn) - .gas(GAS_TO_OFFER) - .hasKnownStatus(SUCCESS))), - withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - ALIASED_TRANSFER, - TRANSFER, - HapiParserUtil.asHeadlongAddress(asAddress( - spec.registry().getAccountID(ACCOUNT_B))), - BigInteger.valueOf(1000)) - .payingWith(ACCOUNT) - .alsoSigningWithFullPrefix(MULTI_KEY) - .via(aliasedTransferTxn) - .gas(GAS_TO_OFFER) - .hasKnownStatus(SUCCESS)))) - .then( - sourcing(() -> getContractInfo( - asContractString(contractIdFromHexedMirrorAddress(childMirror.get()))) - .hasToken(ExpectedTokenRel.relationshipWith(TOKEN_NAME) - .balance(500)) - .logged()), - getAccountBalance(ACCOUNT_B).hasTokenBalance(TOKEN_NAME, 1000), - getAccountBalance(ACCOUNT_A).hasTokenBalance(TOKEN_NAME, 8500)); - } - private HapiSpec transferErc20TokenFromContractWithNoApproval() { final var transferFromOtherContractWithSignaturesTxn = "transferFromOtherContractWithSignaturesTxn"; final var nestedContract = NESTED_ERC_20_CONTRACT; @@ -1383,7 +1293,7 @@ private HapiSpec getErc721OwnerOfFromErc20TokenFails() { .then(getTxnRecord(invalidOwnerOfTxn).andAllChildRecords().logged()); } - private HapiSpec directCallsWorkForERC20() { + private HapiSpec directCallsWorkForErc20() { final AtomicReference tokenNum = new AtomicReference<>(); final var tokenSymbol = "FDFGF"; @@ -1393,7 +1303,7 @@ private HapiSpec directCallsWorkForERC20() { final var decimalsTxn = "decimalsTxn"; - return defaultHapiSpec("DirectCallsWorkForERC20") + return defaultHapiSpec("directCallsWorkForErc20") .given( newKeyNamed(MULTI_KEY), cryptoCreate(ACCOUNT).balance(100 * ONE_HUNDRED_HBARS), @@ -3022,10 +2932,10 @@ private HapiSpec erc721GetApproved() { getTxnRecord(ALLOWANCE_TXN).andAllChildRecords().logged()); } - private HapiSpec erc20TransferFrom() { + private HapiSpec erc20TransferFromAllowance() { final var allowanceTxn2 = "allowanceTxn2"; - return defaultHapiSpec("ERC_20_ALLOWANCE") + return defaultHapiSpec("erc20TransferFromAllowance") .given( newKeyNamed(MULTI_KEY), cryptoCreate(OWNER).balance(100 * ONE_HUNDRED_HBARS), diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileV1SecurityModelSuite.java new file mode 100644 index 000000000000..b96d2a4149bc --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileV1SecurityModelSuite.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2022-2023 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.spec.HapiPropertySource.asContractString; +import static com.hedera.services.bdd.spec.HapiPropertySource.contractIdFromHexedMirrorAddress; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +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.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; +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.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.asHexedAddress; +import static com.hedera.services.bdd.suites.contract.Utils.captureChildCreate2MetaFor; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; + +import com.esaulpaugh.headlong.abi.Address; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.queries.crypto.ExpectedTokenRel; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.suites.HapiSuite; +import java.math.BigInteger; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class ERCPrecompileV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(ERCPrecompileV1SecurityModelSuite.class); + + private static final long GAS_TO_OFFER = 1_000_000L; + private static final String MULTI_KEY = "purpose"; + private static final String OWNER = "owner"; + private static final String ACCOUNT = "anybody"; + private static final String TOKEN_NAME = "TokenA"; + public static final String TRANSFER = "transfer"; + + public static void main(String... args) { + new ERCPrecompileV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + public List getSpecsInSuite() { + return allOf(erc20(), erc721()); + } + + List erc20() { + return List.of(transferErc20TokenAliasedSender()); + } + + List erc721() { + return List.of(); + } + + private HapiSpec transferErc20TokenAliasedSender() { + final var aliasedTransferTxn = "aliasedTransferTxn"; + final var addLiquidityTxn = "addLiquidityTxn"; + final var create2Txn = "create2Txn"; + + final var account_A = "AccountA"; + final var account_B = "AccountB"; + + final var aliasedTransfer = "AliasedTransfer"; + final byte[][] aliasedAddress = new byte[1][1]; + + final AtomicReference childMirror = new AtomicReference<>(); + final AtomicReference childEip1014 = new AtomicReference<>(); + + return propertyPreservingHapiSpec("transferErc20TokenAliasedSender") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(OWNER), + cryptoCreate(ACCOUNT), + cryptoCreate(account_A).key(MULTI_KEY).balance(ONE_MILLION_HBARS), + cryptoCreate(account_B).balance(ONE_MILLION_HBARS), + tokenCreate(TOKEN_NAME) + .adminKey(MULTI_KEY) + .initialSupply(10000) + .treasury(account_A), + tokenAssociate(account_B, TOKEN_NAME), + uploadInitCode(aliasedTransfer), + contractCreate(aliasedTransfer).gas(300_000), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + aliasedTransfer, + "deployWithCREATE2", + asHeadlongAddress(asHexedAddress( + spec.registry().getTokenID(TOKEN_NAME)))) + .exposingResultTo(result -> { + final var res = (Address) result[0]; + aliasedAddress[0] = res.value().toByteArray(); + }) + .payingWith(ACCOUNT) + .alsoSigningWithFullPrefix(MULTI_KEY) + .via(create2Txn) + .gas(GAS_TO_OFFER) + .hasKnownStatus(SUCCESS)))) + .when( + captureChildCreate2MetaFor(2, 0, "setup", create2Txn, childMirror, childEip1014), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + aliasedTransfer, + "giveTokensToOperator", + HapiParserUtil.asHeadlongAddress(asAddress( + spec.registry().getTokenID(TOKEN_NAME))), + HapiParserUtil.asHeadlongAddress(asAddress( + spec.registry().getAccountID(account_A))), + 1500L) + .payingWith(ACCOUNT) + .alsoSigningWithFullPrefix(MULTI_KEY) + .via(addLiquidityTxn) + .gas(GAS_TO_OFFER) + .hasKnownStatus(SUCCESS))), + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + aliasedTransfer, + TRANSFER, + HapiParserUtil.asHeadlongAddress(asAddress( + spec.registry().getAccountID(account_B))), + BigInteger.valueOf(1000)) + .payingWith(ACCOUNT) + .alsoSigningWithFullPrefix(MULTI_KEY) + .via(aliasedTransferTxn) + .gas(GAS_TO_OFFER) + .hasKnownStatus(SUCCESS)))) + .then( + sourcing(() -> getContractInfo( + asContractString(contractIdFromHexedMirrorAddress(childMirror.get()))) + .hasToken(ExpectedTokenRel.relationshipWith(TOKEN_NAME) + .balance(500)) + .logged()), + getAccountBalance(account_B).hasTokenBalance(TOKEN_NAME, 1000), + getAccountBalance(account_A).hasTokenBalance(TOKEN_NAME, 8500)); + } + + @Override + protected Logger getResultsLogger() { + return log; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/FreezeUnfreezeTokenPrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/FreezeUnfreezeTokenPrecompileSuite.java index 906eb00af878..20ef7f91b0fc 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/FreezeUnfreezeTokenPrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/FreezeUnfreezeTokenPrecompileSuite.java @@ -16,52 +16,35 @@ package com.hedera.services.bdd.suites.contract.precompile; -import static com.google.protobuf.ByteString.copyFromUtf8; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; -import static com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts.isLiteralResult; -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.contractCallLocal; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountDetails; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAliasedAccountInfo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; 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.cryptoUpdate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; 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.spec.transactions.TxnVerbs.uploadInitCode; import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromAccountToAlias; -import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; -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.assertionsHold; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; 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.asAddress; import static com.hedera.services.bdd.suites.contract.Utils.asToken; -import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.KNOWABLE_TOKEN; import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; -import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_TOKEN_ID; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_FREEZE_KEY; import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; -import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; -import com.hedera.node.app.hapi.utils.contracts.ParsingConstants; import com.hedera.services.bdd.spec.HapiSpec; -import com.hedera.services.bdd.spec.queries.crypto.ExpectedTokenRel; import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; import com.hedera.services.bdd.suites.HapiSuite; import com.hederahashgraph.api.proto.java.AccountID; -import com.hederahashgraph.api.proto.java.TokenFreezeStatus; import com.hederahashgraph.api.proto.java.TokenID; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -74,13 +57,7 @@ public class FreezeUnfreezeTokenPrecompileSuite extends HapiSuite { private static final String IS_FROZEN_FUNC = "isTokenFrozen"; public static final String TOKEN_FREEZE_FUNC = "tokenFreeze"; public static final String TOKEN_UNFREEZE_FUNC = "tokenUnfreeze"; - private static final String IS_FROZEN_TXN = "isFrozenTxn"; - private static final String ACCOUNT_HAS_NO_KEY_TXN = "accountHasNoFreezeKey"; - private static final String NO_KEY_FREEZE_TXN = "noKeyFreezeTxn"; - private static final String NO_KEY_UNFREEZE_TXN = "noKeyUnfreezeTxn"; private static final String ACCOUNT = "anybody"; - private static final String ACCOUNT_WITHOUT_KEY = "accountWithoutKey"; - private static final String TOKEN_WITHOUT_KEY = "withoutKey"; private static final String FREEZE_KEY = "freezeKey"; private static final String MULTI_KEY = "purpose"; private static final long GAS_TO_OFFER = 4_000_000L; @@ -102,12 +79,7 @@ protected Logger getResultsLogger() { @Override public List getSpecsInSuite() { - return List.of( - freezeUnfreezeFungibleWithNegativeCases(), - freezeUnfreezeNftsWithNegativeCases(), - isFrozenHappyPathWithLocalCall(), - noTokenIdReverts(), - isFrozenHappyPathWithAliasLocalCall()); + return List.of(isFrozenHappyPathWithAliasLocalCall(), noTokenIdReverts()); } private HapiSpec noTokenIdReverts() { @@ -160,244 +132,6 @@ private HapiSpec noTokenIdReverts() { recordWith().status(INVALID_TOKEN_ID))); } - private HapiSpec freezeUnfreezeFungibleWithNegativeCases() { - final AtomicReference withoutKeyID = new AtomicReference<>(); - final AtomicReference vanillaTokenID = new AtomicReference<>(); - final AtomicReference accountID = new AtomicReference<>(); - return defaultHapiSpec("freezeUnfreezeFungibleWithNegativeCases") - .given( - newKeyNamed(FREEZE_KEY), - newKeyNamed(MULTI_KEY), - cryptoCreate(ACCOUNT).balance(100 * ONE_HBAR).exposingCreatedIdTo(accountID::set), - cryptoCreate(ACCOUNT_WITHOUT_KEY), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(TOKEN_WITHOUT_KEY).exposingCreatedIdTo(id -> withoutKeyID.set(asToken(id))), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .freezeKey(FREEZE_KEY) - .initialSupply(1_000) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - uploadInitCode(FREEZE_CONTRACT), - contractCreate(FREEZE_CONTRACT), - tokenAssociate(ACCOUNT, VANILLA_TOKEN), - cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - FREEZE_CONTRACT, - TOKEN_FREEZE_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) - .logged() - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via(ACCOUNT_HAS_NO_KEY_TXN) - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - FREEZE_CONTRACT, - TOKEN_FREEZE_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(withoutKeyID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) - .logged() - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via(NO_KEY_FREEZE_TXN) - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - FREEZE_CONTRACT, - TOKEN_UNFREEZE_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(withoutKeyID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED) - .via(NO_KEY_UNFREEZE_TXN), - cryptoUpdate(ACCOUNT).key(FREEZE_KEY), - contractCall( - FREEZE_CONTRACT, - TOKEN_FREEZE_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) - .logged() - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER), - getAccountDetails(ACCOUNT) - .hasToken(ExpectedTokenRel.relationshipWith(VANILLA_TOKEN) - .freeze(TokenFreezeStatus.Frozen)), - contractCall( - FREEZE_CONTRACT, - TOKEN_UNFREEZE_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) - .logged() - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER), - getAccountDetails(ACCOUNT) - .hasToken(ExpectedTokenRel.relationshipWith(VANILLA_TOKEN) - .freeze(TokenFreezeStatus.Unfrozen))))) - .then( - childRecordsCheck( - ACCOUNT_HAS_NO_KEY_TXN, - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_SIGNATURE) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), - childRecordsCheck( - NO_KEY_FREEZE_TXN, - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(TOKEN_HAS_NO_FREEZE_KEY) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(TOKEN_HAS_NO_FREEZE_KEY)))), - childRecordsCheck( - NO_KEY_UNFREEZE_TXN, - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(TOKEN_HAS_NO_FREEZE_KEY) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(TOKEN_HAS_NO_FREEZE_KEY))))); - } - - private HapiSpec freezeUnfreezeNftsWithNegativeCases() { - final AtomicReference vanillaTokenID = new AtomicReference<>(); - final AtomicReference accountID = new AtomicReference<>(); - return defaultHapiSpec("freezeUnfreezeNftsWithNegativeCases") - .given( - newKeyNamed(FREEZE_KEY), - newKeyNamed(MULTI_KEY), - cryptoCreate(ACCOUNT).balance(100 * ONE_HBAR).exposingCreatedIdTo(accountID::set), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(KNOWABLE_TOKEN) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(TOKEN_TREASURY) - .freezeKey(FREEZE_KEY) - .supplyKey(MULTI_KEY) - .initialSupply(0) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - mintToken(KNOWABLE_TOKEN, List.of(copyFromUtf8("First!"))), - uploadInitCode(FREEZE_CONTRACT), - contractCreate(FREEZE_CONTRACT), - tokenAssociate(ACCOUNT, KNOWABLE_TOKEN), - cryptoTransfer(movingUnique(KNOWABLE_TOKEN, 1L).between(TOKEN_TREASURY, ACCOUNT))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - FREEZE_CONTRACT, - TOKEN_UNFREEZE_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER) - .via(ACCOUNT_HAS_NO_KEY_TXN) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - cryptoUpdate(ACCOUNT).key(FREEZE_KEY), - contractCall( - FREEZE_CONTRACT, - TOKEN_FREEZE_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER), - getAccountDetails(ACCOUNT) - .hasToken(ExpectedTokenRel.relationshipWith(KNOWABLE_TOKEN) - .freeze(TokenFreezeStatus.Frozen)), - contractCall( - FREEZE_CONTRACT, - TOKEN_UNFREEZE_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER), - contractCall( - FREEZE_CONTRACT, - IS_FROZEN_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) - .logged() - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via(IS_FROZEN_TXN) - .gas(GAS_TO_OFFER)))) - .then( - childRecordsCheck( - ACCOUNT_HAS_NO_KEY_TXN, - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_SIGNATURE) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), - childRecordsCheck( - IS_FROZEN_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(ParsingConstants.FunctionType.HAPI_IS_FROZEN) - .withStatus(SUCCESS) - .withIsFrozen(false))))); - } - - private HapiSpec isFrozenHappyPathWithLocalCall() { - final AtomicReference accountID = new AtomicReference<>(); - final AtomicReference vanillaTokenID = new AtomicReference<>(); - return defaultHapiSpec("isFrozenHappyPathWithLocalCall") - .given( - newKeyNamed(FREEZE_KEY), - newKeyNamed(MULTI_KEY), - cryptoCreate(ACCOUNT) - .balance(100 * ONE_HBAR) - .key(FREEZE_KEY) - .exposingCreatedIdTo(accountID::set), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .freezeKey(FREEZE_KEY) - .initialSupply(1_000) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - uploadInitCode(FREEZE_CONTRACT), - contractCreate(FREEZE_CONTRACT), - tokenAssociate(ACCOUNT, VANILLA_TOKEN), - cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) - .when(assertionsHold((spec, ctxLog) -> { - final var freezeCall = contractCall( - FREEZE_CONTRACT, - TOKEN_FREEZE_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) - .logged() - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .gas(GAS_TO_OFFER); - final var isFrozenLocalCall = contractCallLocal( - FREEZE_CONTRACT, - IS_FROZEN_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) - .has(resultWith() - .resultViaFunctionName( - IS_FROZEN_FUNC, FREEZE_CONTRACT, isLiteralResult(new Object[] {Boolean.TRUE - }))); - allRunFor(spec, freezeCall, isFrozenLocalCall); - })) - .then(); - } - private HapiSpec isFrozenHappyPathWithAliasLocalCall() { final AtomicReference vanillaTokenID = new AtomicReference<>(); final AtomicReference autoCreatedAccountId = new AtomicReference<>(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/FreezeUnfreezeTokenPrecompileV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/FreezeUnfreezeTokenPrecompileV1SecurityModelSuite.java new file mode 100644 index 000000000000..0831799457cf --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/FreezeUnfreezeTokenPrecompileV1SecurityModelSuite.java @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2021-2023 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.google.protobuf.ByteString.copyFromUtf8; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +import static com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts.isLiteralResult; +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.contractCallLocal; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountDetails; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +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.cryptoUpdate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; +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.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +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.assertionsHold; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.asToken; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.KNOWABLE_TOKEN; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_FREEZE_KEY; +import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; +import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; + +import com.hedera.node.app.hapi.utils.contracts.ParsingConstants; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.queries.crypto.ExpectedTokenRel; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.TokenFreezeStatus; +import com.hederahashgraph.api.proto.java.TokenID; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class FreezeUnfreezeTokenPrecompileV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(FreezeUnfreezeTokenPrecompileV1SecurityModelSuite.class); + public static final String FREEZE_CONTRACT = "FreezeUnfreezeContract"; + private static final String IS_FROZEN_FUNC = "isTokenFrozen"; + public static final String TOKEN_FREEZE_FUNC = "tokenFreeze"; + public static final String TOKEN_UNFREEZE_FUNC = "tokenUnfreeze"; + private static final String IS_FROZEN_TXN = "isFrozenTxn"; + private static final String ACCOUNT_HAS_NO_KEY_TXN = "accountHasNoFreezeKey"; + private static final String NO_KEY_FREEZE_TXN = "noKeyFreezeTxn"; + private static final String NO_KEY_UNFREEZE_TXN = "noKeyUnfreezeTxn"; + private static final String ACCOUNT = "anybody"; + private static final String ACCOUNT_WITHOUT_KEY = "accountWithoutKey"; + private static final String TOKEN_WITHOUT_KEY = "withoutKey"; + private static final String FREEZE_KEY = "freezeKey"; + private static final String MULTI_KEY = "purpose"; + private static final long GAS_TO_OFFER = 4_000_000L; + + public static void main(String... args) { + new FreezeUnfreezeTokenPrecompileV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + protected Logger getResultsLogger() { + return log; + } + + @Override + public List getSpecsInSuite() { + return List.of( + freezeUnfreezeFungibleWithNegativeCases(), + freezeUnfreezeNftsWithNegativeCases(), + isFrozenHappyPathWithLocalCall()); + } + + private HapiSpec freezeUnfreezeFungibleWithNegativeCases() { + final AtomicReference withoutKeyID = new AtomicReference<>(); + final AtomicReference vanillaTokenID = new AtomicReference<>(); + final AtomicReference accountID = new AtomicReference<>(); + return propertyPreservingHapiSpec("freezeUnfreezeFungibleWithNegativeCases") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenFreezeAccount,TokenUnfreezeAccount", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(FREEZE_KEY), + newKeyNamed(MULTI_KEY), + cryptoCreate(ACCOUNT).balance(100 * ONE_HBAR).exposingCreatedIdTo(accountID::set), + cryptoCreate(ACCOUNT_WITHOUT_KEY), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(TOKEN_WITHOUT_KEY).exposingCreatedIdTo(id -> withoutKeyID.set(asToken(id))), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .freezeKey(FREEZE_KEY) + .initialSupply(1_000) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + uploadInitCode(FREEZE_CONTRACT), + contractCreate(FREEZE_CONTRACT), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + FREEZE_CONTRACT, + TOKEN_FREEZE_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) + .logged() + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via(ACCOUNT_HAS_NO_KEY_TXN) + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + FREEZE_CONTRACT, + TOKEN_FREEZE_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(withoutKeyID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) + .logged() + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via(NO_KEY_FREEZE_TXN) + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + FREEZE_CONTRACT, + TOKEN_UNFREEZE_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(withoutKeyID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED) + .via(NO_KEY_UNFREEZE_TXN), + cryptoUpdate(ACCOUNT).key(FREEZE_KEY), + contractCall( + FREEZE_CONTRACT, + TOKEN_FREEZE_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) + .logged() + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER), + getAccountDetails(ACCOUNT) + .hasToken(ExpectedTokenRel.relationshipWith(VANILLA_TOKEN) + .freeze(TokenFreezeStatus.Frozen)), + contractCall( + FREEZE_CONTRACT, + TOKEN_UNFREEZE_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) + .logged() + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER), + getAccountDetails(ACCOUNT) + .hasToken(ExpectedTokenRel.relationshipWith(VANILLA_TOKEN) + .freeze(TokenFreezeStatus.Unfrozen))))) + .then( + childRecordsCheck( + ACCOUNT_HAS_NO_KEY_TXN, + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_SIGNATURE) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), + childRecordsCheck( + NO_KEY_FREEZE_TXN, + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(TOKEN_HAS_NO_FREEZE_KEY) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(TOKEN_HAS_NO_FREEZE_KEY)))), + childRecordsCheck( + NO_KEY_UNFREEZE_TXN, + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(TOKEN_HAS_NO_FREEZE_KEY) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(TOKEN_HAS_NO_FREEZE_KEY))))); + } + + private HapiSpec freezeUnfreezeNftsWithNegativeCases() { + final AtomicReference vanillaTokenID = new AtomicReference<>(); + final AtomicReference accountID = new AtomicReference<>(); + return propertyPreservingHapiSpec("freezeUnfreezeNftsWithNegativeCases") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenFreezeAccount,TokenUnfreezeAccount", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(FREEZE_KEY), + newKeyNamed(MULTI_KEY), + cryptoCreate(ACCOUNT).balance(100 * ONE_HBAR).exposingCreatedIdTo(accountID::set), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(KNOWABLE_TOKEN) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(TOKEN_TREASURY) + .freezeKey(FREEZE_KEY) + .supplyKey(MULTI_KEY) + .initialSupply(0) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + mintToken(KNOWABLE_TOKEN, List.of(copyFromUtf8("First!"))), + uploadInitCode(FREEZE_CONTRACT), + contractCreate(FREEZE_CONTRACT), + tokenAssociate(ACCOUNT, KNOWABLE_TOKEN), + cryptoTransfer(movingUnique(KNOWABLE_TOKEN, 1L).between(TOKEN_TREASURY, ACCOUNT))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + FREEZE_CONTRACT, + TOKEN_UNFREEZE_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER) + .via(ACCOUNT_HAS_NO_KEY_TXN) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + cryptoUpdate(ACCOUNT).key(FREEZE_KEY), + contractCall( + FREEZE_CONTRACT, + TOKEN_FREEZE_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER), + getAccountDetails(ACCOUNT) + .hasToken(ExpectedTokenRel.relationshipWith(KNOWABLE_TOKEN) + .freeze(TokenFreezeStatus.Frozen)), + contractCall( + FREEZE_CONTRACT, + TOKEN_UNFREEZE_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER), + contractCall( + FREEZE_CONTRACT, + IS_FROZEN_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) + .logged() + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via(IS_FROZEN_TXN) + .gas(GAS_TO_OFFER)))) + .then( + childRecordsCheck( + ACCOUNT_HAS_NO_KEY_TXN, + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_SIGNATURE) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), + childRecordsCheck( + IS_FROZEN_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(ParsingConstants.FunctionType.HAPI_IS_FROZEN) + .withStatus(SUCCESS) + .withIsFrozen(false))))); + } + + private HapiSpec isFrozenHappyPathWithLocalCall() { + final AtomicReference accountID = new AtomicReference<>(); + final AtomicReference vanillaTokenID = new AtomicReference<>(); + return propertyPreservingHapiSpec("isFrozenHappyPathWithLocalCall") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenFreezeAccount", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(FREEZE_KEY), + newKeyNamed(MULTI_KEY), + cryptoCreate(ACCOUNT) + .balance(100 * ONE_HBAR) + .key(FREEZE_KEY) + .exposingCreatedIdTo(accountID::set), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .freezeKey(FREEZE_KEY) + .initialSupply(1_000) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + uploadInitCode(FREEZE_CONTRACT), + contractCreate(FREEZE_CONTRACT), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) + .when(assertionsHold((spec, ctxLog) -> { + final var freezeCall = contractCall( + FREEZE_CONTRACT, + TOKEN_FREEZE_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) + .logged() + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .gas(GAS_TO_OFFER); + final var isFrozenLocalCall = contractCallLocal( + FREEZE_CONTRACT, + IS_FROZEN_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get()))) + .has(resultWith() + .resultViaFunctionName( + IS_FROZEN_FUNC, FREEZE_CONTRACT, isLiteralResult(new Object[] {Boolean.TRUE + }))); + allRunFor(spec, freezeCall, isFrozenLocalCall); + })) + .then(); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/GrantRevokeKycSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/GrantRevokeKycSuite.java index 1354da9455c7..f61bda1882d3 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/GrantRevokeKycSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/GrantRevokeKycSuite.java @@ -20,7 +20,6 @@ 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.contractCallLocal; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountDetails; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAliasedAccountInfo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; @@ -42,18 +41,14 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_TOKEN_ID; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_KYC_KEY; import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; -import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; import com.hedera.services.bdd.spec.HapiSpec; -import com.hedera.services.bdd.spec.queries.crypto.ExpectedTokenRel; import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; import com.hedera.services.bdd.suites.HapiSuite; import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.TokenID; -import com.hederahashgraph.api.proto.java.TokenKycStatus; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; @@ -97,7 +92,7 @@ List negativeSpecs() { } List positiveSpecs() { - return List.of(grantRevokeKycSpec(), grantRevokeKycSpecWithAliasLocalCall()); + return List.of(grantRevokeKycSpecWithAliasLocalCall()); } private HapiSpec grantRevokeKycFail() { @@ -268,96 +263,6 @@ private HapiSpec grantRevokeKycFail() { htsPrecompileResult().withStatus(INVALID_TOKEN_ID))))); } - private HapiSpec grantRevokeKycSpec() { - final AtomicReference vanillaTokenID = new AtomicReference<>(); - final AtomicReference accountID = new AtomicReference<>(); - final AtomicReference secondAccountID = new AtomicReference<>(); - - return defaultHapiSpec("GrantRevokeKycSpec") - .given( - newKeyNamed(KYC_KEY), - cryptoCreate(ACCOUNT) - .balance(100 * ONE_HBAR) - .key(KYC_KEY) - .exposingCreatedIdTo(accountID::set), - cryptoCreate(SECOND_ACCOUNT).exposingCreatedIdTo(secondAccountID::set), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .kycKey(KYC_KEY) - .initialSupply(1_000) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - uploadInitCode(GRANT_REVOKE_KYC_CONTRACT), - contractCreate(GRANT_REVOKE_KYC_CONTRACT), - tokenAssociate(ACCOUNT, VANILLA_TOKEN), - tokenAssociate(SECOND_ACCOUNT, VANILLA_TOKEN)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - GRANT_REVOKE_KYC_CONTRACT, - TOKEN_GRANT_KYC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(secondAccountID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("GrantKycTx") - .gas(GAS_TO_OFFER), - getAccountDetails(SECOND_ACCOUNT) - .hasToken(ExpectedTokenRel.relationshipWith(VANILLA_TOKEN) - .kyc(TokenKycStatus.Granted)), - contractCallLocal( - GRANT_REVOKE_KYC_CONTRACT, - IS_KYC_GRANTED, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(secondAccountID.get()))), - contractCall( - GRANT_REVOKE_KYC_CONTRACT, - TOKEN_REVOKE_KYC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(secondAccountID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("RevokeKycTx") - .gas(GAS_TO_OFFER), - contractCall( - GRANT_REVOKE_KYC_CONTRACT, - IS_KYC_GRANTED, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(secondAccountID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("IsKycTx") - .gas(GAS_TO_OFFER)))) - .then( - childRecordsCheck( - "GrantKycTx", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)))), - childRecordsCheck( - "RevokeKycTx", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)))), - childRecordsCheck( - "IsKycTx", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_IS_KYC) - .withIsKyc(false) - .withStatus(SUCCESS))))); - } - private HapiSpec grantRevokeKycSpecWithAliasLocalCall() { final AtomicReference vanillaTokenID = new AtomicReference<>(); final AtomicReference autoCreatedAccountId = new AtomicReference<>(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/GrantRevokeKycV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/GrantRevokeKycV1SecurityModelSuite.java new file mode 100644 index 000000000000..b4c53f1fe760 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/GrantRevokeKycV1SecurityModelSuite.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2021-2023 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.spec.HapiSpec.propertyPreservingHapiSpec; +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.contractCallLocal; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountDetails; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +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.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.asToken; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; + +import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.queries.crypto.ExpectedTokenRel; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.TokenID; +import com.hederahashgraph.api.proto.java.TokenKycStatus; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse +public class GrantRevokeKycV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(GrantRevokeKycV1SecurityModelSuite.class); + public static final String GRANT_REVOKE_KYC_CONTRACT = "GrantRevokeKyc"; + private static final String IS_KYC_GRANTED = "isKycGranted"; + public static final String TOKEN_GRANT_KYC = "tokenGrantKyc"; + public static final String TOKEN_REVOKE_KYC = "tokenRevokeKyc"; + + private static final long GAS_TO_OFFER = 4_000_000L; + private static final String ACCOUNT = "anybody"; + public static final String SECOND_ACCOUNT = "anybodySecond"; + private static final String KYC_KEY = "kycKey"; + + public static void main(String... args) { + new GrantRevokeKycV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + protected Logger getResultsLogger() { + return log; + } + + @Override + public List getSpecsInSuite() { + return allOf(positiveSpecs(), negativeSpecs()); + } + + List negativeSpecs() { + return List.of(); + } + + List positiveSpecs() { + return List.of(grantRevokeKycSpec()); + } + + private HapiSpec grantRevokeKycSpec() { + final AtomicReference vanillaTokenID = new AtomicReference<>(); + final AtomicReference accountID = new AtomicReference<>(); + final AtomicReference secondAccountID = new AtomicReference<>(); + + return propertyPreservingHapiSpec("grantRevokeKycSpec") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(KYC_KEY), + cryptoCreate(ACCOUNT) + .balance(100 * ONE_HBAR) + .key(KYC_KEY) + .exposingCreatedIdTo(accountID::set), + cryptoCreate(SECOND_ACCOUNT).exposingCreatedIdTo(secondAccountID::set), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .kycKey(KYC_KEY) + .initialSupply(1_000) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + uploadInitCode(GRANT_REVOKE_KYC_CONTRACT), + contractCreate(GRANT_REVOKE_KYC_CONTRACT), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + tokenAssociate(SECOND_ACCOUNT, VANILLA_TOKEN)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + GRANT_REVOKE_KYC_CONTRACT, + TOKEN_GRANT_KYC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(secondAccountID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("GrantKycTx") + .gas(GAS_TO_OFFER), + getAccountDetails(SECOND_ACCOUNT) + .hasToken(ExpectedTokenRel.relationshipWith(VANILLA_TOKEN) + .kyc(TokenKycStatus.Granted)), + contractCallLocal( + GRANT_REVOKE_KYC_CONTRACT, + IS_KYC_GRANTED, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(secondAccountID.get()))), + contractCall( + GRANT_REVOKE_KYC_CONTRACT, + TOKEN_REVOKE_KYC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(secondAccountID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("RevokeKycTx") + .gas(GAS_TO_OFFER), + contractCall( + GRANT_REVOKE_KYC_CONTRACT, + IS_KYC_GRANTED, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(secondAccountID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("IsKycTx") + .gas(GAS_TO_OFFER)))) + .then( + childRecordsCheck( + "GrantKycTx", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)))), + childRecordsCheck( + "RevokeKycTx", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)))), + childRecordsCheck( + "IsKycTx", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_IS_KYC) + .withIsKyc(false) + .withStatus(SUCCESS))))); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/LazyCreateThroughPrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/LazyCreateThroughPrecompileSuite.java index 1d2ab4992557..4904057441e7 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/LazyCreateThroughPrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/LazyCreateThroughPrecompileSuite.java @@ -24,14 +24,11 @@ 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.keys.KeyShape.ED25519; -import static com.hedera.services.bdd.spec.keys.SigControl.SECP256K1_ON; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountDetails; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAliasedAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAliasedAccountInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getLiteralAliasAccountInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenNftInfo; -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.contractCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoApproveAllowance; @@ -41,23 +38,21 @@ 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.spec.transactions.TxnVerbs.uploadInitCode; -import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fractionalFee; import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; 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.accountAmount; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.accountAmountAlias; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.emptyChildRecordsCheck; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.inParallel; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.nftTransferToAlias; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.tokenTransferList; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.tokenTransferLists; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.transferList; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.headlongFromHexed; +import static com.hedera.services.bdd.suites.contract.Utils.mirrorAddrWith; +import static com.hedera.services.bdd.suites.contract.Utils.nCopiesOfSender; +import static com.hedera.services.bdd.suites.contract.Utils.nNonMirrorAddressFrom; +import static com.hedera.services.bdd.suites.contract.Utils.nonMirrorAddrWith; import static com.hedera.services.bdd.suites.crypto.AutoAccountCreationSuite.LAZY_MEMO; import static com.hedera.services.bdd.suites.file.FileUpdateSuite.CIVILIAN; import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; @@ -65,15 +60,11 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_GAS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_ACCOUNT_ID; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_ALIAS_KEY; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.MAX_CHILD_RECORDS_EXCEEDED; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.NO_REMAINING_AUTOMATIC_ASSOCIATIONS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.REVERTED_SUCCESS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; import static com.swirlds.common.utility.CommonUtils.hex; -import com.esaulpaugh.headlong.abi.Address; -import com.esaulpaugh.headlong.abi.Tuple; import com.google.protobuf.ByteString; import com.hedera.node.app.hapi.utils.ByteStringUtils; import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; @@ -81,18 +72,13 @@ import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.HapiSpecOperation; import com.hedera.services.bdd.spec.assertions.AccountInfoAsserts; -import com.hedera.services.bdd.spec.assertions.ContractInfoAsserts; import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; -import com.hedera.services.bdd.spec.transactions.token.TokenMovement; -import com.hedera.services.bdd.spec.utilops.UtilVerbs; import com.hedera.services.bdd.suites.HapiSuite; import com.hederahashgraph.api.proto.java.TokenSupplyType; import com.hederahashgraph.api.proto.java.TokenType; import java.math.BigInteger; import java.nio.charset.StandardCharsets; -import java.util.Collections; import java.util.List; -import java.util.OptionalLong; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.IntStream; @@ -100,7 +86,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes; -import org.junit.jupiter.api.Assertions; public class LazyCreateThroughPrecompileSuite extends HapiSuite { private static final Logger log = LogManager.getLogger(LazyCreateThroughPrecompileSuite.class); @@ -112,17 +97,9 @@ public class LazyCreateThroughPrecompileSuite extends HapiSuite { private static final String FIRST = "FIRST"; public static final ByteString FIRST_META = ByteString.copyFrom(FIRST.getBytes(StandardCharsets.UTF_8)); public static final ByteString SECOND_META = ByteString.copyFrom(FIRST.getBytes(StandardCharsets.UTF_8)); - private static final String TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT = "PrecompileAliasXfer"; private static final String SPENDER = "spender"; - private static final String TRANSFER_TOKEN_TXN = "transferTokenTxn"; - private static final String TRANSFER_TOKENS_TXN = "transferTokensTxn"; - private static final String TRANSFER_NFT_TXN = "transferNFTTxn"; - private static final String TRANSFER_NFTS_TXN = "transferNFTsTxn"; - private static final String SENDER = "sender"; - private static final long TOTAL_SUPPLY = 1_000; private static final String NFT_TOKEN = "Token_NFT"; private static final String TRANSFER_TXN = "transferTxn"; - private static final String TRANSFER_TXN2 = "transferTxn2"; private static final String NFT_KEY = "nftKey"; private static final String AUTO_CREATION_MODES = "AutoCreationModes"; private static final String CREATION_ATTEMPT = "creationAttempt"; @@ -144,8 +121,6 @@ public class LazyCreateThroughPrecompileSuite extends HapiSuite { private static final String HTS_TRANSFER_FROM = "htsTransferFrom"; private static final String HTS_TRANSFER_FROM_NFT = "htsTransferFromNFT"; private static final String BASE_APPROVE_TXN = "baseApproveTxn"; - private static final String TRANSFER_TXN3 = "transferTxn3"; - private static final Tuple[] EMPTY_TUPLE_ARRAY = new Tuple[] {}; private static final String RECIPIENT = "recipient"; private static final String NOT_ENOUGH_GAS_TXN = "NOT_ENOUGH_GAS_TXN"; private static final String ECDSA_KEY = "abcdECDSAkey"; @@ -167,84 +142,13 @@ protected Logger getResultsLogger() { @Override public List getSpecsInSuite() { return List.of( - cryptoTransferV1LazyCreate(), - cryptoTransferV2LazyCreate(), - transferTokenLazyCreate(), - transferTokensLazyCreate(), - transferNftLazyCreate(), - transferNftsLazyCreate(), erc20TransferLazyCreate(), erc20TransferFromLazyCreate(), erc721TransferFromLazyCreate(), htsTransferFromFungibleTokenLazyCreate(), - htsTransferFromFungibleTokenLazyCreate(), htsTransferFromForNFTLazyCreate(), - hollowAccountSigningReqsStillEnforced(), resourceLimitExceededRevertsAllRecords(), - autoCreationFailsWithMirrorAddress(), - revertedAutoCreationRollsBackEvenIfTopLevelSucceeds(), - canCreateMultipleHollows(), - canCreateViaFungibleWithFractionalFee()); - } - - HapiSpec hollowAccountSigningReqsStillEnforced() { - final var nft = "nft"; - final var nftKey = NFT_KEY; - final var creationAttempt = CREATION_ATTEMPT; - final var creationReversal = "creationReversal"; - final AtomicLong civilianId = new AtomicLong(); - final AtomicReference nftMirrorAddr = new AtomicReference<>(); - - return defaultHapiSpec("HollowAccountSigningReqsStillEnforced") - .given( - newKeyNamed(nftKey), - uploadInitCode(AUTO_CREATION_MODES), - contractCreate(AUTO_CREATION_MODES), - cryptoCreate(CIVILIAN) - .keyShape(ED25519) - .exposingCreatedIdTo(id -> civilianId.set(id.getAccountNum())), - tokenCreate(nft) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .supplyKey(nftKey) - .initialSupply(0) - .treasury(CIVILIAN) - .exposingCreatedIdTo( - idLit -> nftMirrorAddr.set(asHexedSolidityAddress(asToken(idLit)))), - mintToken(nft, List.of(ByteString.copyFromUtf8(ONE_TIME)))) - .when(sourcing(() -> contractCall( - AUTO_CREATION_MODES, - CREATE_DIRECTLY, - headlongFromHexed(nftMirrorAddr.get()), - mirrorAddrWith(civilianId.get()), - nonMirrorAddrWith(civilianId.get() + 4_000_000), - 1L, - false) - .via(creationAttempt) - .gas(GAS_TO_OFFER) - .alsoSigningWithFullPrefix(CIVILIAN))) - .then( - getTxnRecord(creationAttempt).andAllChildRecords().logged(), - sourcing(() -> getLiteralAliasAccountInfo( - hex(Bytes.fromHexString(nonMirrorAddrWith(civilianId.get() + 4_000_000) - .toString()) - .toArray())) - .logged()), - // Now try to reverse the transfer and take the hollow account's NFT - sourcing(() -> contractCall( - AUTO_CREATION_MODES, - CREATE_DIRECTLY, - headlongFromHexed(nftMirrorAddr.get()), - nonMirrorAddrWith(civilianId.get() + 4_000_000), - mirrorAddrWith(civilianId.get()), - 1L, - false) - .via(creationReversal) - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)), - sourcing(() -> childRecordsCheck( - creationReversal, - CONTRACT_REVERT_EXECUTED, - recordWith().status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))); + autoCreationFailsWithMirrorAddress()); } HapiSpec resourceLimitExceededRevertsAllRecords() { @@ -335,777 +239,6 @@ HapiSpec autoCreationFailsWithMirrorAddress() { creationAttempt, CONTRACT_REVERT_EXECUTED, recordWith().status(INVALID_ALIAS_KEY))); } - HapiSpec revertedAutoCreationRollsBackEvenIfTopLevelSucceeds() { - final var nft = "nft"; - final var nftKey = NFT_KEY; - final var creationAttempt = CREATION_ATTEMPT; - final AtomicLong civilianId = new AtomicLong(); - final AtomicReference nftMirrorAddr = new AtomicReference<>(); - - return defaultHapiSpec("RevertedAutoCreationRollsBackEvenIfTopLevelSucceeds") - .given( - newKeyNamed(nftKey), - uploadInitCode(AUTO_CREATION_MODES), - contractCreate(AUTO_CREATION_MODES), - cryptoCreate(CIVILIAN) - .keyShape(ED25519) - .exposingCreatedIdTo(id -> civilianId.set(id.getAccountNum())), - tokenCreate(nft) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .supplyKey(nftKey) - .initialSupply(0) - .treasury(CIVILIAN) - .exposingCreatedIdTo( - idLit -> nftMirrorAddr.set(asHexedSolidityAddress(asToken(idLit)))), - mintToken(nft, List.of(ByteString.copyFromUtf8(ONE_TIME)))) - .when(sourcing(() -> contractCall( - AUTO_CREATION_MODES, - "createIndirectlyRevertingAndRecover", - headlongFromHexed(nftMirrorAddr.get()), - mirrorAddrWith(civilianId.get()), - nonMirrorAddrWith(civilianId.get() + 8_000_000), - 1L) - .via(creationAttempt) - .gas(GAS_TO_OFFER) - .alsoSigningWithFullPrefix(CIVILIAN) - .hasKnownStatus(SUCCESS))) - .then(childRecordsCheck( - creationAttempt, - SUCCESS, - recordWith() - .status(REVERTED_SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS))))); - } - - @SuppressWarnings("java:S5960") - HapiSpec canCreateViaFungibleWithFractionalFee() { - final var ft = "ft"; - final var ftKey = NFT_KEY; - final var creationAttempt = CREATION_ATTEMPT; - final AtomicLong civilianId = new AtomicLong(); - final AtomicReference ftMirrorAddr = new AtomicReference<>(); - final long supply = 100_000_000; - - return defaultHapiSpec("CanCreateViaFungibleWithFractionalFee") - .given( - newKeyNamed(ftKey), - uploadInitCode(AUTO_CREATION_MODES), - contractCreate(AUTO_CREATION_MODES), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(CIVILIAN) - .maxAutomaticTokenAssociations(1) - .keyShape(ED25519) - .exposingCreatedIdTo(id -> civilianId.set(id.getAccountNum())), - // If running locally, ensures the entity 0.0. is an account w/ EVM address - cryptoCreate("somebody").keyShape(SECP256K1_ON).withMatchingEvmAddress(), - tokenCreate(ft) - .tokenType(TokenType.FUNGIBLE_COMMON) - .supplyKey(ftKey) - .initialSupply(supply) - .withCustom(fractionalFee(1L, 20L, 0L, OptionalLong.of(0L), TOKEN_TREASURY)) - .treasury(TOKEN_TREASURY) - .exposingCreatedIdTo(idLit -> ftMirrorAddr.set(asHexedSolidityAddress(asToken(idLit)))), - cryptoTransfer(TokenMovement.moving(supply, ft).between(TOKEN_TREASURY, CIVILIAN))) - .when(withOpContext((spec, opLog) -> { - final var op = contractCall( - AUTO_CREATION_MODES, - "createDirectlyViaFungible", - headlongFromHexed(ftMirrorAddr.get()), - mirrorAddrWith(civilianId.get()), - nonMirrorAddrWith(123, civilianId.get() + 1), - supply) - .via(creationAttempt) - .gas(GAS_TO_OFFER) - .alsoSigningWithFullPrefix(CIVILIAN) - .hasKnownStatusFrom(SUCCESS, CONTRACT_REVERT_EXECUTED); - allRunFor(spec, op); - // If this ContractCall was converted to an EthereumTransaction, then it will - // not be tracking the last receipt and we can't do this extra logging; this is - // fine for now, since the _Eth spec hasn't been flaky - if (op.hasActualStatus() && op.getActualStatus() == CONTRACT_REVERT_EXECUTED) { - final var lookup = getTxnRecord(creationAttempt).andAllChildRecords(); - allRunFor(spec, lookup); - Assertions.fail("canCreateViaFungibleWithFractionalFee() failed w/ record " - + lookup.getResponseRecord() - + " and child records " - + lookup.getChildRecords()); - } - })) - .then(); - } - - HapiSpec canCreateMultipleHollows() { - final var n = 3; - final var nft = "nft"; - final var nftKey = NFT_KEY; - final var creationAttempt = CREATION_ATTEMPT; - final AtomicLong civilianId = new AtomicLong(); - final AtomicReference nftMirrorAddr = new AtomicReference<>(); - - return defaultHapiSpec("CanCreateMultipleHollows") - .given( - newKeyNamed(nftKey), - uploadInitCode(AUTO_CREATION_MODES), - contractCreate(AUTO_CREATION_MODES), - cryptoCreate(CIVILIAN) - .keyShape(ED25519) - .exposingCreatedIdTo(id -> civilianId.set(id.getAccountNum())), - tokenCreate(nft) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .supplyKey(nftKey) - .initialSupply(0) - .treasury(CIVILIAN) - .exposingCreatedIdTo( - idLit -> nftMirrorAddr.set(asHexedSolidityAddress(asToken(idLit)))), - mintToken( - nft, - IntStream.range(0, n) - .mapToObj(i -> ByteString.copyFromUtf8(ONE_TIME + i)) - .toList())) - .when(sourcing(() -> contractCall( - AUTO_CREATION_MODES, - "createSeveralDirectly", - headlongFromHexed(nftMirrorAddr.get()), - nCopiesOfSender(n, mirrorAddrWith(civilianId.get())), - nNonMirrorAddressFrom(n, civilianId.get() + 1_234_567_890L), - LongStream.iterate(1L, l -> l + 1).limit(n).toArray()) - .via(creationAttempt) - .gas(GAS_TO_OFFER) - .alsoSigningWithFullPrefix(CIVILIAN) - .hasKnownStatus(SUCCESS))) - .then(getTxnRecord(creationAttempt).andAllChildRecords().logged()); - } - - private Address[] nCopiesOfSender(final int n, final Address mirrorAddr) { - return Collections.nCopies(n, mirrorAddr).toArray(Address[]::new); - } - - private Address[] nNonMirrorAddressFrom(final int n, final long m) { - return LongStream.range(m, m + n).mapToObj(this::nonMirrorAddrWith).toArray(Address[]::new); - } - - private Address headlongFromHexed(final String addr) { - return Address.wrap(Address.toChecksumAddress("0x" + addr)); - } - - public static Address mirrorAddrWith(final long num) { - return Address.wrap( - Address.toChecksumAddress(new BigInteger(1, HapiPropertySource.asSolidityAddress(0, 0, num)))); - } - - private Address nonMirrorAddrWith(final long num) { - return nonMirrorAddrWith(666, num); - } - - private Address nonMirrorAddrWith(final long seed, final long num) { - return Address.wrap(Address.toChecksumAddress( - new BigInteger(1, HapiPropertySource.asSolidityAddress((int) seed, seed, num)))); - } - - private HapiSpec cryptoTransferV1LazyCreate() { - final var NESTED_LAZY_PRECOMPILE_CONTRACT = "LazyPrecompileTransfers"; - final var FUNGIBLE_TOKEN_2 = "ftnt"; - return defaultHapiSpec("cryptoTransferV1LazyCreate") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(SENDER) - .balance(10 * ONE_HUNDRED_HBARS) - .key(MULTI_KEY) - .maxAutomaticTokenAssociations(5), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(FUNGIBLE_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(TOTAL_SUPPLY) - .treasury(TOKEN_TREASURY), - tokenCreate(FUNGIBLE_TOKEN_2) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(TOTAL_SUPPLY) - .treasury(TOKEN_TREASURY), - tokenCreate(NFT_TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .treasury(SENDER) - .initialSupply(0L) - .supplyKey(MULTI_KEY), - mintToken(NFT_TOKEN, List.of(META1, META2)), - newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), - cryptoTransfer(moving(500, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, SENDER)), - cryptoTransfer(moving(500, FUNGIBLE_TOKEN_2).between(TOKEN_TREASURY, SENDER)), - uploadInitCode(NESTED_LAZY_PRECOMPILE_CONTRACT), - contractCreate(NESTED_LAZY_PRECOMPILE_CONTRACT).maxAutomaticTokenAssociations(1), - getContractInfo(NESTED_LAZY_PRECOMPILE_CONTRACT) - .has(ContractInfoAsserts.contractWith().maxAutoAssociations(1)) - .logged()) - .when(withOpContext((spec, opLog) -> { - final var token = spec.registry().getTokenID(FUNGIBLE_TOKEN); - final var token2 = spec.registry().getTokenID(FUNGIBLE_TOKEN_2); - final var nftToken = spec.registry().getTokenID(NFT_TOKEN); - final var sender = spec.registry().getAccountID(SENDER); - final var ecdsaKey = spec.registry().getKey(ECDSA_KEY); - final var evmAddressBytes = ByteString.copyFrom(recoverAddressFromPubKey( - ecdsaKey.getECDSASecp256K1().toByteArray())); - final var amountToBeSent = 50L; - final var transferFn = "cryptoTransferV1LazyCreate"; - allRunFor( - spec, - contractCall( - NESTED_LAZY_PRECOMPILE_CONTRACT, - transferFn, - tokenTransferLists() - .withTokenTransferList( - tokenTransferList() - .forToken(token) - .withAccountAmounts( - accountAmount(sender, -amountToBeSent), - accountAmountAlias( - recoverAddressFromPubKey( - ecdsaKey.getECDSASecp256K1() - .toByteArray()), - amountToBeSent)) - .build(), - tokenTransferList() - .forToken(nftToken) - .withNftTransfers(nftTransferToAlias( - sender, - recoverAddressFromPubKey( - ecdsaKey.getECDSASecp256K1() - .toByteArray()), - 1L)) - .build()) - .build(), - tokenTransferLists() - .withTokenTransferList(tokenTransferList() - .forToken(token) - .withAccountAmounts( - accountAmount(sender, -amountToBeSent), - accountAmountAlias( - recoverAddressFromPubKey( - ecdsaKey.getECDSASecp256K1() - .toByteArray()), - amountToBeSent)) - .build()) - .build()) - .payingWith(GENESIS) - .via(TRANSFER_TXN) - .signedBy(GENESIS, MULTI_KEY) - .alsoSigningWithFullPrefix(MULTI_KEY) - .gas(GAS_TO_OFFER), - contractCall( - NESTED_LAZY_PRECOMPILE_CONTRACT, - transferFn, - tokenTransferLists() - .withTokenTransferList(tokenTransferList() - .forToken(token) - .withAccountAmounts( - accountAmount(sender, -1L), - accountAmountAlias( - recoverAddressFromPubKey( - ecdsaKey.getECDSASecp256K1() - .toByteArray()), - 1L)) - .build()) - .build(), - tokenTransferLists() - .withTokenTransferList(tokenTransferList() - .forToken(token) - .withAccountAmounts( - accountAmount(sender, -1L), - accountAmountAlias( - recoverAddressFromPubKey( - ecdsaKey.getECDSASecp256K1() - .toByteArray()), - 1L)) - .build()) - .build()) - .payingWith(GENESIS) - .signedBy(GENESIS, MULTI_KEY) - .alsoSigningWithFullPrefix(MULTI_KEY) - .via(TRANSFER_TXN2) - .gas(GAS_TO_OFFER), - contractCall( - NESTED_LAZY_PRECOMPILE_CONTRACT, - transferFn, - tokenTransferLists() - .withTokenTransferList(tokenTransferList() - .forToken(token2) - .withAccountAmounts( - accountAmount(sender, -1L), - accountAmountAlias( - recoverAddressFromPubKey( - ecdsaKey.getECDSASecp256K1() - .toByteArray()), - 1L)) - .build()) - .build(), - tokenTransferLists() - .withTokenTransferList(tokenTransferList() - .forToken(token2) - .withAccountAmounts( - accountAmount(sender, -1L), - accountAmountAlias( - recoverAddressFromPubKey( - ecdsaKey.getECDSASecp256K1() - .toByteArray()), - 1L)) - .build()) - .build()) - .payingWith(GENESIS) - .signedBy(GENESIS, MULTI_KEY) - .alsoSigningWithFullPrefix(MULTI_KEY) - .via(TRANSFER_TXN3) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED) - .gas(GAS_TO_OFFER), - childRecordsCheck( - TRANSFER_TXN, - SUCCESS, - recordWith().status(SUCCESS), - recordWith().status(SUCCESS), - recordWith().status(SUCCESS)), - childRecordsCheck( - TRANSFER_TXN2, - SUCCESS, - recordWith().status(SUCCESS), - recordWith().status(SUCCESS)), - childRecordsCheck( - TRANSFER_TXN3, - CONTRACT_REVERT_EXECUTED, - recordWith().status(NO_REMAINING_AUTOMATIC_ASSOCIATIONS)), - getAliasedAccountInfo(ECDSA_KEY) - .has(AccountInfoAsserts.accountWith() - .key(EMPTY_KEY) - .autoRenew(THREE_MONTHS_IN_SECONDS) - .receiverSigReq(false) - .memo(LAZY_MEMO)), - getAliasedAccountBalance(evmAddressBytes) - .hasTokenBalance(FUNGIBLE_TOKEN, amountToBeSent * 2 + 2) - .hasTokenBalance(NFT_TOKEN, 1) - .logged()); - })) - .then(); - } - - private HapiSpec cryptoTransferV2LazyCreate() { - final var NESTED_LAZY_PRECOMPILE_CONTRACT = "LazyPrecompileTransfersAtomic"; - final var FUNGIBLE_TOKEN_2 = "ftnt"; - final var INIT_BALANCE = 10 * ONE_HUNDRED_HBARS; - return defaultHapiSpec("cryptoTransferV2LazyCreate") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(SENDER) - .balance(INIT_BALANCE) - .key(MULTI_KEY) - .maxAutomaticTokenAssociations(5), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(FUNGIBLE_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(TOTAL_SUPPLY) - .treasury(TOKEN_TREASURY), - tokenCreate(FUNGIBLE_TOKEN_2) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(TOTAL_SUPPLY) - .treasury(TOKEN_TREASURY), - tokenCreate(NFT_TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .treasury(SENDER) - .initialSupply(0L) - .supplyKey(MULTI_KEY), - mintToken(NFT_TOKEN, List.of(META1, META2)), - newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), - cryptoTransfer(moving(500, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, SENDER)), - cryptoTransfer(moving(500, FUNGIBLE_TOKEN_2).between(TOKEN_TREASURY, SENDER)), - uploadInitCode(NESTED_LAZY_PRECOMPILE_CONTRACT), - contractCreate(NESTED_LAZY_PRECOMPILE_CONTRACT).maxAutomaticTokenAssociations(1), - getContractInfo(NESTED_LAZY_PRECOMPILE_CONTRACT) - .has(ContractInfoAsserts.contractWith().maxAutoAssociations(1)) - .logged()) - .when(withOpContext((spec, opLog) -> { - final var ecdsaKey = spec.registry().getKey(ECDSA_KEY); - final var tmp = ecdsaKey.getECDSASecp256K1().toByteArray(); - final var addressBytes = recoverAddressFromPubKey(tmp); - final var evmAddressBytes = ByteString.copyFrom(addressBytes); - final var token = spec.registry().getTokenID(FUNGIBLE_TOKEN); - final var token2 = spec.registry().getTokenID(FUNGIBLE_TOKEN_2); - final var nftToken = spec.registry().getTokenID(NFT_TOKEN); - final var sender = spec.registry().getAccountID(SENDER); - final var amountToBeSent = 50L; - - final var cryptoTransferV2LazyCreateFn = "cryptoTransferV2LazyCreate"; - allRunFor( - spec, - contractCall( - NESTED_LAZY_PRECOMPILE_CONTRACT, - cryptoTransferV2LazyCreateFn, - transferList() - .withAccountAmounts( - accountAmount(sender, -amountToBeSent, false), - UtilVerbs.accountAmountAlias( - addressBytes, amountToBeSent, false)) - .build(), - tokenTransferLists() - .withTokenTransferList( - tokenTransferList() - .forToken(token) - .withAccountAmounts( - accountAmount( - sender, -amountToBeSent, false), - UtilVerbs.accountAmountAlias( - addressBytes, - amountToBeSent, - false)) - .build(), - tokenTransferList() - .forToken(nftToken) - .withNftTransfers(UtilVerbs.nftTransferToAlias( - sender, addressBytes, 1L, false)) - .build()) - .build(), - transferList() - .withAccountAmounts( - accountAmount(sender, -amountToBeSent, false), - UtilVerbs.accountAmountAlias( - addressBytes, amountToBeSent, false)) - .build(), - tokenTransferLists() - .withTokenTransferList(tokenTransferList() - .forToken(token) - .withAccountAmounts( - accountAmount(sender, -amountToBeSent, false), - UtilVerbs.accountAmountAlias( - addressBytes, amountToBeSent, false)) - .build()) - .build()) - .payingWith(GENESIS) - .via(TRANSFER_TXN) - .signedBy(GENESIS, MULTI_KEY) - .alsoSigningWithFullPrefix(MULTI_KEY) - .gas(GAS_TO_OFFER), - contractCall( - NESTED_LAZY_PRECOMPILE_CONTRACT, - cryptoTransferV2LazyCreateFn, - transferList() - .withAccountAmounts( - accountAmount(sender, -amountToBeSent, false), - UtilVerbs.accountAmountAlias( - addressBytes, amountToBeSent, false)) - .build(), - EMPTY_TUPLE_ARRAY, - transferList() - .withAccountAmounts( - accountAmount(sender, -amountToBeSent, false), - UtilVerbs.accountAmountAlias( - addressBytes, amountToBeSent, false)) - .build(), - EMPTY_TUPLE_ARRAY) - .payingWith(GENESIS) - .signedBy(GENESIS, MULTI_KEY) - .alsoSigningWithFullPrefix(MULTI_KEY) - .via(TRANSFER_TXN2) - .gas(GAS_TO_OFFER), - contractCall( - NESTED_LAZY_PRECOMPILE_CONTRACT, - cryptoTransferV2LazyCreateFn, - transferList() - .withAccountAmounts(EMPTY_TUPLE_ARRAY) - .build(), - tokenTransferLists() - .withTokenTransferList(tokenTransferList() - .forToken(token2) - .withAccountAmounts( - accountAmount(sender, -amountToBeSent, false), - UtilVerbs.accountAmountAlias( - addressBytes, amountToBeSent, false)) - .build()) - .build(), - transferList() - .withAccountAmounts(EMPTY_TUPLE_ARRAY) - .build(), - EMPTY_TUPLE_ARRAY) - .payingWith(GENESIS) - .signedBy(GENESIS, MULTI_KEY) - .alsoSigningWithFullPrefix(MULTI_KEY) - .via(TRANSFER_TXN3) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED) - .gas(GAS_TO_OFFER), - childRecordsCheck( - TRANSFER_TXN, - SUCCESS, - recordWith().status(SUCCESS), - recordWith().status(SUCCESS), - recordWith().status(SUCCESS)), - childRecordsCheck( - TRANSFER_TXN2, - SUCCESS, - recordWith().status(SUCCESS), - recordWith().status(SUCCESS)), - childRecordsCheck( - TRANSFER_TXN3, - CONTRACT_REVERT_EXECUTED, - recordWith().status(NO_REMAINING_AUTOMATIC_ASSOCIATIONS)), - getAliasedAccountInfo(ECDSA_KEY) - .has(AccountInfoAsserts.accountWith() - .key(EMPTY_KEY) - .autoRenew(THREE_MONTHS_IN_SECONDS) - .receiverSigReq(false) - .memo(LAZY_MEMO)), - getAliasedAccountBalance(evmAddressBytes) - .hasTinyBars(4 * amountToBeSent) - .hasTokenBalance(FUNGIBLE_TOKEN, amountToBeSent * 2) - .hasTokenBalance(NFT_TOKEN, 1) - .logged()); - })) - .then(); - } - - private HapiSpec transferTokenLazyCreate() { - final AtomicReference tokenAddr = new AtomicReference<>(); - - return defaultHapiSpec("transferTokenLazyCreate") - .given( - newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), - newKeyNamed(MULTI_KEY), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(OWNER).balance(100 * ONE_HUNDRED_HBARS), - tokenCreate(FUNGIBLE_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(5) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .exposingCreatedIdTo(id -> tokenAddr.set( - HapiPropertySource.asHexedSolidityAddress(HapiPropertySource.asToken(id)))), - uploadInitCode(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), - contractCreate(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), - tokenAssociate(OWNER, List.of(FUNGIBLE_TOKEN)), - cryptoTransfer(moving(5, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, OWNER))) - .when(withOpContext((spec, opLog) -> { - final var ecdsaKey = spec.registry().getKey(ECDSA_KEY); - final var tmp = ecdsaKey.getECDSASecp256K1().toByteArray(); - final var addressBytes = recoverAddressFromPubKey(tmp); - final var alias = ByteStringUtils.wrapUnsafely(addressBytes); - allRunFor( - spec, - contractCall( - TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT, - "transferTokenCallNestedThenAgain", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(FUNGIBLE_TOKEN))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(OWNER))), - HapiParserUtil.asHeadlongAddress(addressBytes), - 2L, - 2L) - .via(TRANSFER_TOKEN_TXN) - .alsoSigningWithFullPrefix(OWNER) - .gas(GAS_TO_OFFER) - .hasKnownStatus(SUCCESS), - getAliasedAccountInfo(ECDSA_KEY) - .has(AccountInfoAsserts.accountWith() - .key(EMPTY_KEY) - .autoRenew(THREE_MONTHS_IN_SECONDS) - .receiverSigReq(false) - .memo(LAZY_MEMO)), - getAliasedAccountBalance(alias) - .hasTokenBalance(FUNGIBLE_TOKEN, 4) - .logged(), - childRecordsCheck( - TRANSFER_TOKEN_TXN, - SUCCESS, - recordWith().status(SUCCESS), - recordWith().status(SUCCESS), - recordWith().status(SUCCESS))); - })) - .then(); - } - - private HapiSpec transferTokensLazyCreate() { - final AtomicReference tokenAddr = new AtomicReference<>(); - - return defaultHapiSpec("transferTokensToEVMAddressAliasRevertAndTransferAgainSuccessfully") - .given( - newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), - newKeyNamed(MULTI_KEY), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(OWNER).balance(100 * ONE_HUNDRED_HBARS), - tokenCreate(FUNGIBLE_TOKEN) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(5) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .exposingCreatedIdTo(id -> tokenAddr.set( - HapiPropertySource.asHexedSolidityAddress(HapiPropertySource.asToken(id)))), - uploadInitCode(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), - contractCreate(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), - tokenAssociate(OWNER, List.of(FUNGIBLE_TOKEN)), - cryptoTransfer(moving(5, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, OWNER))) - .when(withOpContext((spec, opLog) -> { - final var ecdsaKey = spec.registry().getKey(ECDSA_KEY); - final var tmp = ecdsaKey.getECDSASecp256K1().toByteArray(); - final var addressBytes = recoverAddressFromPubKey(tmp); - final var alias = ByteStringUtils.wrapUnsafely(addressBytes); - assert addressBytes != null; - allRunFor( - spec, - contractCall( - TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT, - "transferTokensCallNestedThenAgain", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(FUNGIBLE_TOKEN))), - new Address[] { - HapiParserUtil.asHeadlongAddress(asAddress( - spec.registry().getAccountID(OWNER))), - HapiParserUtil.asHeadlongAddress(addressBytes) - }, - new long[] {-2L, 2L}, - new long[] {-2L, 2L}) - .via(TRANSFER_TOKENS_TXN) - .gas(GAS_TO_OFFER) - .alsoSigningWithFullPrefix(OWNER) - .hasKnownStatus(SUCCESS), - getAliasedAccountInfo(ECDSA_KEY) - .has(AccountInfoAsserts.accountWith() - .key(EMPTY_KEY) - .autoRenew(THREE_MONTHS_IN_SECONDS) - .receiverSigReq(false) - .memo(LAZY_MEMO)), - getAliasedAccountBalance(alias) - .hasTokenBalance(FUNGIBLE_TOKEN, 4) - .logged(), - childRecordsCheck( - TRANSFER_TOKENS_TXN, - SUCCESS, - recordWith().status(SUCCESS), - recordWith().status(SUCCESS), - recordWith().status(SUCCESS))); - })) - .then(); - } - - private HapiSpec transferNftLazyCreate() { - return defaultHapiSpec("transferNftLazyCreate") - .given( - newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), - newKeyNamed(MULTI_KEY), - cryptoCreate(OWNER).balance(100 * ONE_HUNDRED_HBARS), - cryptoCreate(SPENDER), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(NON_FUNGIBLE_TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .initialSupply(0) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY), - uploadInitCode(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), - contractCreate(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), - tokenAssociate(OWNER, NON_FUNGIBLE_TOKEN), - tokenAssociate(SPENDER, NON_FUNGIBLE_TOKEN), - tokenAssociate(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT, NON_FUNGIBLE_TOKEN), - mintToken(NON_FUNGIBLE_TOKEN, List.of(FIRST_META, SECOND_META)), - cryptoTransfer(movingUnique(NON_FUNGIBLE_TOKEN, 1L, 2L).between(TOKEN_TREASURY, OWNER))) - .when(withOpContext((spec, opLog) -> { - final var ecdsaKey = spec.registry().getKey(ECDSA_KEY); - final var tmp = ecdsaKey.getECDSASecp256K1().toByteArray(); - final var addressBytes = recoverAddressFromPubKey(tmp); - final var alias = ByteStringUtils.wrapUnsafely(addressBytes); - allRunFor( - spec, - contractCall( - TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT, - "transferNFTCallNestedThenAgain", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(NON_FUNGIBLE_TOKEN))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(OWNER))), - HapiParserUtil.asHeadlongAddress(addressBytes), - 1L, - 2L) - .via(TRANSFER_NFT_TXN) - .alsoSigningWithFullPrefix(OWNER) - .gas(GAS_TO_OFFER) - .hasKnownStatus(SUCCESS), - getAliasedAccountInfo(ECDSA_KEY) - .has(AccountInfoAsserts.accountWith() - .key(EMPTY_KEY) - .autoRenew(THREE_MONTHS_IN_SECONDS) - .receiverSigReq(false) - .memo(LAZY_MEMO)), - getAliasedAccountBalance(alias) - .hasTokenBalance(NON_FUNGIBLE_TOKEN, 2) - .logged(), - childRecordsCheck( - TRANSFER_NFT_TXN, - SUCCESS, - recordWith().status(SUCCESS), - recordWith().status(SUCCESS), - recordWith().status(SUCCESS))); - })) - .then(); - } - - private HapiSpec transferNftsLazyCreate() { - return defaultHapiSpec("transferNftsLazyCreate") - .given( - newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), - newKeyNamed(MULTI_KEY), - cryptoCreate(OWNER).balance(100 * ONE_HUNDRED_HBARS), - cryptoCreate(SPENDER), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(NON_FUNGIBLE_TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .initialSupply(0) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY), - uploadInitCode(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), - contractCreate(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), - tokenAssociate(OWNER, NON_FUNGIBLE_TOKEN), - tokenAssociate(SPENDER, NON_FUNGIBLE_TOKEN), - tokenAssociate(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT, NON_FUNGIBLE_TOKEN), - mintToken(NON_FUNGIBLE_TOKEN, List.of(FIRST_META, SECOND_META)), - cryptoTransfer(movingUnique(NON_FUNGIBLE_TOKEN, 1L, 2L).between(TOKEN_TREASURY, OWNER))) - .when(withOpContext((spec, opLog) -> { - final var ecdsaKey = spec.registry().getKey(ECDSA_KEY); - final var tmp = ecdsaKey.getECDSASecp256K1().toByteArray(); - final var addressBytes = recoverAddressFromPubKey(tmp); - final var alias = ByteStringUtils.wrapUnsafely(addressBytes); - assert addressBytes != null; - allRunFor( - spec, - contractCall( - TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT, - "transferNFTsCallNestedThenAgain", - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(NON_FUNGIBLE_TOKEN))), - new Address[] { - HapiParserUtil.asHeadlongAddress(asAddress( - spec.registry().getAccountID(OWNER))) - }, - new Address[] {HapiParserUtil.asHeadlongAddress(addressBytes)}, - new long[] {1L}, - new long[] {2L}) - .via(TRANSFER_NFTS_TXN) - .alsoSigningWithFullPrefix(OWNER) - .gas(GAS_TO_OFFER), - getAliasedAccountInfo(ECDSA_KEY) - .has(AccountInfoAsserts.accountWith() - .key(EMPTY_KEY) - .autoRenew(THREE_MONTHS_IN_SECONDS) - .receiverSigReq(false) - .memo(LAZY_MEMO)), - getAliasedAccountBalance(alias) - .hasTokenBalance(NON_FUNGIBLE_TOKEN, 2) - .logged(), - childRecordsCheck( - TRANSFER_NFTS_TXN, - SUCCESS, - recordWith().status(SUCCESS), - recordWith().status(SUCCESS), - recordWith().status(SUCCESS))); - })) - .then(); - } - private HapiSpec erc20TransferLazyCreate() { final AtomicReference tokenAddr = new AtomicReference<>(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/LazyCreateThroughPrecompileV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/LazyCreateThroughPrecompileV1SecurityModelSuite.java new file mode 100644 index 000000000000..aae9dee2afcb --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/LazyCreateThroughPrecompileV1SecurityModelSuite.java @@ -0,0 +1,1069 @@ +/* + * Copyright (C) 2022-2023 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.node.app.service.evm.utils.EthSigsUtils.recoverAddressFromPubKey; +import static com.hedera.services.bdd.spec.HapiPropertySource.asHexedSolidityAddress; +import static com.hedera.services.bdd.spec.HapiPropertySource.asToken; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +import static com.hedera.services.bdd.spec.assertions.AccountDetailsAsserts.accountDetailsWith; +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.keys.KeyShape.ED25519; +import static com.hedera.services.bdd.spec.keys.SigControl.SECP256K1_ON; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountDetails; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAliasedAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAliasedAccountInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getLiteralAliasAccountInfo; +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.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoApproveAllowance; +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.mintToken; +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.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fractionalFee; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +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.accountAmount; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.accountAmountAlias; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.nftTransferToAlias; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.tokenTransferList; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.tokenTransferLists; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.transferList; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.headlongFromHexed; +import static com.hedera.services.bdd.suites.contract.Utils.mirrorAddrWith; +import static com.hedera.services.bdd.suites.contract.Utils.nCopiesOfSender; +import static com.hedera.services.bdd.suites.contract.Utils.nNonMirrorAddressFrom; +import static com.hedera.services.bdd.suites.contract.Utils.nonMirrorAddrWith; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.crypto.AutoAccountCreationSuite.LAZY_MEMO; +import static com.hedera.services.bdd.suites.file.FileUpdateSuite.CIVILIAN; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.NO_REMAINING_AUTOMATIC_ASSOCIATIONS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.REVERTED_SUCCESS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.swirlds.common.utility.CommonUtils.hex; + +import com.esaulpaugh.headlong.abi.Address; +import com.esaulpaugh.headlong.abi.Tuple; +import com.google.protobuf.ByteString; +import com.hedera.node.app.hapi.utils.ByteStringUtils; +import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; +import com.hedera.services.bdd.spec.HapiPropertySource; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.assertions.AccountInfoAsserts; +import com.hedera.services.bdd.spec.assertions.ContractInfoAsserts; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.spec.transactions.token.TokenMovement; +import com.hedera.services.bdd.spec.utilops.UtilVerbs; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.TokenSupplyType; +import com.hederahashgraph.api.proto.java.TokenType; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.OptionalLong; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Assertions; + +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse +public class LazyCreateThroughPrecompileV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(LazyCreateThroughPrecompileV1SecurityModelSuite.class); + private static final long GAS_TO_OFFER = 4_000_000L; + private static final String FUNGIBLE_TOKEN = "fungibleToken"; + private static final String NON_FUNGIBLE_TOKEN = "nonFungibleToken"; + private static final String MULTI_KEY = "purpose"; + private static final String OWNER = "owner"; + private static final String FIRST = "FIRST"; + public static final ByteString FIRST_META = ByteString.copyFrom(FIRST.getBytes(StandardCharsets.UTF_8)); + public static final ByteString SECOND_META = ByteString.copyFrom(FIRST.getBytes(StandardCharsets.UTF_8)); + private static final String TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT = "PrecompileAliasXfer"; + private static final String SPENDER = "spender"; + private static final String TRANSFER_TOKEN_TXN = "transferTokenTxn"; + private static final String TRANSFER_TOKENS_TXN = "transferTokensTxn"; + private static final String TRANSFER_NFT_TXN = "transferNFTTxn"; + private static final String TRANSFER_NFTS_TXN = "transferNFTsTxn"; + private static final String SENDER = "sender"; + private static final long TOTAL_SUPPLY = 1_000; + private static final String NFT_TOKEN = "Token_NFT"; + private static final String TRANSFER_TXN = "transferTxn"; + private static final String TRANSFER_TXN2 = "transferTxn2"; + private static final String NFT_KEY = "nftKey"; + private static final String AUTO_CREATION_MODES = "AutoCreationModes"; + private static final String CREATION_ATTEMPT = "creationAttempt"; + private static final String ONE_TIME = "ONE TIME"; + private static final String CREATE_DIRECTLY = "createDirectly"; + private static final String HTS_TRANSFER_FROM_CONTRACT = "HtsTransferFrom"; + private static final ByteString META1 = ByteStringUtils.wrapUnsafely("meta1".getBytes()); + private static final ByteString META2 = ByteStringUtils.wrapUnsafely("meta2".getBytes()); + private static final String TOKEN_TREASURY = "treasury"; + private static final String HTS_TRANSFER_FROM = "htsTransferFrom"; + private static final String BASE_APPROVE_TXN = "baseApproveTxn"; + private static final String TRANSFER_TXN3 = "transferTxn3"; + private static final Tuple[] EMPTY_TUPLE_ARRAY = new Tuple[] {}; + private static final String ECDSA_KEY = "abcdECDSAkey"; + + public static void main(String... args) { + new LazyCreateThroughPrecompileV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + protected Logger getResultsLogger() { + return log; + } + + @Override + public List getSpecsInSuite() { + return List.of( + cryptoTransferV1LazyCreate(), + cryptoTransferV2LazyCreate(), + transferTokenLazyCreate(), + transferTokensToEVMAddressAliasRevertAndTransferAgainSuccessfully(), + transferNftLazyCreate(), + transferNftsLazyCreate(), + htsTransferFromFungibleTokenLazyCreate(), + hollowAccountSigningReqsStillEnforced(), + revertedAutoCreationRollsBackEvenIfTopLevelSucceeds(), + canCreateMultipleHollows(), + canCreateViaFungibleWithFractionalFee()); + } + + HapiSpec hollowAccountSigningReqsStillEnforced() { + final var nft = "nft"; + final var nftKey = NFT_KEY; + final var creationAttempt = CREATION_ATTEMPT; + final var creationReversal = "creationReversal"; + final AtomicLong civilianId = new AtomicLong(); + final AtomicReference nftMirrorAddr = new AtomicReference<>(); + + return propertyPreservingHapiSpec("hollowAccountSigningReqsStillEnforced") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(nftKey), + uploadInitCode(AUTO_CREATION_MODES), + contractCreate(AUTO_CREATION_MODES), + cryptoCreate(CIVILIAN) + .keyShape(ED25519) + .exposingCreatedIdTo(id -> civilianId.set(id.getAccountNum())), + tokenCreate(nft) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyKey(nftKey) + .initialSupply(0) + .treasury(CIVILIAN) + .exposingCreatedIdTo( + idLit -> nftMirrorAddr.set(asHexedSolidityAddress(asToken(idLit)))), + mintToken(nft, List.of(ByteString.copyFromUtf8(ONE_TIME)))) + .when(sourcing(() -> contractCall( + AUTO_CREATION_MODES, + CREATE_DIRECTLY, + headlongFromHexed(nftMirrorAddr.get()), + mirrorAddrWith(civilianId.get()), + nonMirrorAddrWith(civilianId.get() + 4_000_000), + 1L, + false) + .via(creationAttempt) + .gas(GAS_TO_OFFER) + .alsoSigningWithFullPrefix(CIVILIAN))) + .then( + getTxnRecord(creationAttempt).andAllChildRecords().logged(), + sourcing(() -> getLiteralAliasAccountInfo( + hex(Bytes.fromHexString(nonMirrorAddrWith(civilianId.get() + 4_000_000) + .toString()) + .toArray())) + .logged()), + // Now try to reverse the transfer and take the hollow account's NFT + sourcing(() -> contractCall( + AUTO_CREATION_MODES, + CREATE_DIRECTLY, + headlongFromHexed(nftMirrorAddr.get()), + nonMirrorAddrWith(civilianId.get() + 4_000_000), + mirrorAddrWith(civilianId.get()), + 1L, + false) + .via(creationReversal) + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)), + sourcing(() -> childRecordsCheck( + creationReversal, + CONTRACT_REVERT_EXECUTED, + recordWith().status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)))); + } + + HapiSpec revertedAutoCreationRollsBackEvenIfTopLevelSucceeds() { + final var nft = "nft"; + final var nftKey = NFT_KEY; + final var creationAttempt = CREATION_ATTEMPT; + final AtomicLong civilianId = new AtomicLong(); + final AtomicReference nftMirrorAddr = new AtomicReference<>(); + + return propertyPreservingHapiSpec("revertedAutoCreationRollsBackEvenIfTopLevelSucceeds") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(nftKey), + uploadInitCode(AUTO_CREATION_MODES), + contractCreate(AUTO_CREATION_MODES), + cryptoCreate(CIVILIAN) + .keyShape(ED25519) + .exposingCreatedIdTo(id -> civilianId.set(id.getAccountNum())), + tokenCreate(nft) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyKey(nftKey) + .initialSupply(0) + .treasury(CIVILIAN) + .exposingCreatedIdTo( + idLit -> nftMirrorAddr.set(asHexedSolidityAddress(asToken(idLit)))), + mintToken(nft, List.of(ByteString.copyFromUtf8(ONE_TIME)))) + .when(sourcing(() -> contractCall( + AUTO_CREATION_MODES, + "createIndirectlyRevertingAndRecover", + headlongFromHexed(nftMirrorAddr.get()), + mirrorAddrWith(civilianId.get()), + nonMirrorAddrWith(civilianId.get() + 8_000_000), + 1L) + .via(creationAttempt) + .gas(GAS_TO_OFFER) + .alsoSigningWithFullPrefix(CIVILIAN) + .hasKnownStatus(SUCCESS))) + .then(childRecordsCheck( + creationAttempt, + SUCCESS, + recordWith() + .status(REVERTED_SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS))))); + } + + @SuppressWarnings("java:S5960") + HapiSpec canCreateViaFungibleWithFractionalFee() { + final var ft = "ft"; + final var ftKey = NFT_KEY; + final var creationAttempt = CREATION_ATTEMPT; + final AtomicLong civilianId = new AtomicLong(); + final AtomicReference ftMirrorAddr = new AtomicReference<>(); + final long supply = 100_000_000; + + return propertyPreservingHapiSpec("canCreateViaFungibleWithFractionalFee") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ftKey), + uploadInitCode(AUTO_CREATION_MODES), + contractCreate(AUTO_CREATION_MODES), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(CIVILIAN) + .maxAutomaticTokenAssociations(1) + .keyShape(ED25519) + .exposingCreatedIdTo(id -> civilianId.set(id.getAccountNum())), + // If running locally, ensures the entity 0.0. is an account w/ EVM address + cryptoCreate("somebody").keyShape(SECP256K1_ON).withMatchingEvmAddress(), + tokenCreate(ft) + .tokenType(TokenType.FUNGIBLE_COMMON) + .supplyKey(ftKey) + .initialSupply(supply) + .withCustom(fractionalFee(1L, 20L, 0L, OptionalLong.of(0L), TOKEN_TREASURY)) + .treasury(TOKEN_TREASURY) + .exposingCreatedIdTo(idLit -> ftMirrorAddr.set(asHexedSolidityAddress(asToken(idLit)))), + cryptoTransfer(TokenMovement.moving(supply, ft).between(TOKEN_TREASURY, CIVILIAN))) + .when(withOpContext((spec, opLog) -> { + final var op = contractCall( + AUTO_CREATION_MODES, + "createDirectlyViaFungible", + headlongFromHexed(ftMirrorAddr.get()), + mirrorAddrWith(civilianId.get()), + nonMirrorAddrWith(123, civilianId.get() + 1), + supply) + .via(creationAttempt) + .gas(GAS_TO_OFFER) + .alsoSigningWithFullPrefix(CIVILIAN) + .hasKnownStatusFrom(SUCCESS, CONTRACT_REVERT_EXECUTED); + allRunFor(spec, op); + // If this ContractCall was converted to an EthereumTransaction, then it will + // not be tracking the last receipt and we can't do this extra logging; this is + // fine for now, since the _Eth spec hasn't been flaky + if (op.hasActualStatus() && op.getActualStatus() == CONTRACT_REVERT_EXECUTED) { + final var lookup = getTxnRecord(creationAttempt).andAllChildRecords(); + allRunFor(spec, lookup); + Assertions.fail("canCreateViaFungibleWithFractionalFee() failed w/ record " + + lookup.getResponseRecord() + + " and child records " + + lookup.getChildRecords()); + } + })) + .then(); + } + + HapiSpec canCreateMultipleHollows() { + final var n = 3; + final var nft = "nft"; + final var nftKey = NFT_KEY; + final var creationAttempt = CREATION_ATTEMPT; + final AtomicLong civilianId = new AtomicLong(); + final AtomicReference nftMirrorAddr = new AtomicReference<>(); + + return propertyPreservingHapiSpec("canCreateMultipleHollows") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(nftKey), + uploadInitCode(AUTO_CREATION_MODES), + contractCreate(AUTO_CREATION_MODES), + cryptoCreate(CIVILIAN) + .keyShape(ED25519) + .exposingCreatedIdTo(id -> civilianId.set(id.getAccountNum())), + tokenCreate(nft) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyKey(nftKey) + .initialSupply(0) + .treasury(CIVILIAN) + .exposingCreatedIdTo( + idLit -> nftMirrorAddr.set(asHexedSolidityAddress(asToken(idLit)))), + mintToken( + nft, + IntStream.range(0, n) + .mapToObj(i -> ByteString.copyFromUtf8(ONE_TIME + i)) + .toList())) + .when(sourcing(() -> contractCall( + AUTO_CREATION_MODES, + "createSeveralDirectly", + headlongFromHexed(nftMirrorAddr.get()), + nCopiesOfSender(n, mirrorAddrWith(civilianId.get())), + nNonMirrorAddressFrom(n, civilianId.get() + 1_234_567_890L), + LongStream.iterate(1L, l -> l + 1).limit(n).toArray()) + .via(creationAttempt) + .gas(GAS_TO_OFFER) + .alsoSigningWithFullPrefix(CIVILIAN) + .hasKnownStatus(SUCCESS))) + .then(getTxnRecord(creationAttempt).andAllChildRecords().logged()); + } + + private HapiSpec cryptoTransferV1LazyCreate() { + final var NESTED_LAZY_PRECOMPILE_CONTRACT = "LazyPrecompileTransfers"; + final var FUNGIBLE_TOKEN_2 = "ftnt"; + return propertyPreservingHapiSpec("cryptoTransferV1LazyCreate") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(SENDER) + .balance(10 * ONE_HUNDRED_HBARS) + .key(MULTI_KEY) + .maxAutomaticTokenAssociations(5), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(FUNGIBLE_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(TOTAL_SUPPLY) + .treasury(TOKEN_TREASURY), + tokenCreate(FUNGIBLE_TOKEN_2) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(TOTAL_SUPPLY) + .treasury(TOKEN_TREASURY), + tokenCreate(NFT_TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .treasury(SENDER) + .initialSupply(0L) + .supplyKey(MULTI_KEY), + mintToken(NFT_TOKEN, List.of(META1, META2)), + newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), + cryptoTransfer(moving(500, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, SENDER)), + cryptoTransfer(moving(500, FUNGIBLE_TOKEN_2).between(TOKEN_TREASURY, SENDER)), + uploadInitCode(NESTED_LAZY_PRECOMPILE_CONTRACT), + contractCreate(NESTED_LAZY_PRECOMPILE_CONTRACT).maxAutomaticTokenAssociations(1), + getContractInfo(NESTED_LAZY_PRECOMPILE_CONTRACT) + .has(ContractInfoAsserts.contractWith().maxAutoAssociations(1)) + .logged()) + .when(withOpContext((spec, opLog) -> { + final var token = spec.registry().getTokenID(FUNGIBLE_TOKEN); + final var token2 = spec.registry().getTokenID(FUNGIBLE_TOKEN_2); + final var nftToken = spec.registry().getTokenID(NFT_TOKEN); + final var sender = spec.registry().getAccountID(SENDER); + final var ecdsaKey = spec.registry().getKey(ECDSA_KEY); + final var evmAddressBytes = ByteString.copyFrom(recoverAddressFromPubKey( + ecdsaKey.getECDSASecp256K1().toByteArray())); + final var amountToBeSent = 50L; + final var transferFn = "cryptoTransferV1LazyCreate"; + allRunFor( + spec, + contractCall( + NESTED_LAZY_PRECOMPILE_CONTRACT, + transferFn, + tokenTransferLists() + .withTokenTransferList( + tokenTransferList() + .forToken(token) + .withAccountAmounts( + accountAmount(sender, -amountToBeSent), + accountAmountAlias( + recoverAddressFromPubKey( + ecdsaKey.getECDSASecp256K1() + .toByteArray()), + amountToBeSent)) + .build(), + tokenTransferList() + .forToken(nftToken) + .withNftTransfers(nftTransferToAlias( + sender, + recoverAddressFromPubKey( + ecdsaKey.getECDSASecp256K1() + .toByteArray()), + 1L)) + .build()) + .build(), + tokenTransferLists() + .withTokenTransferList(tokenTransferList() + .forToken(token) + .withAccountAmounts( + accountAmount(sender, -amountToBeSent), + accountAmountAlias( + recoverAddressFromPubKey( + ecdsaKey.getECDSASecp256K1() + .toByteArray()), + amountToBeSent)) + .build()) + .build()) + .payingWith(GENESIS) + .via(TRANSFER_TXN) + .signedBy(GENESIS, MULTI_KEY) + .alsoSigningWithFullPrefix(MULTI_KEY) + .gas(GAS_TO_OFFER), + contractCall( + NESTED_LAZY_PRECOMPILE_CONTRACT, + transferFn, + tokenTransferLists() + .withTokenTransferList(tokenTransferList() + .forToken(token) + .withAccountAmounts( + accountAmount(sender, -1L), + accountAmountAlias( + recoverAddressFromPubKey( + ecdsaKey.getECDSASecp256K1() + .toByteArray()), + 1L)) + .build()) + .build(), + tokenTransferLists() + .withTokenTransferList(tokenTransferList() + .forToken(token) + .withAccountAmounts( + accountAmount(sender, -1L), + accountAmountAlias( + recoverAddressFromPubKey( + ecdsaKey.getECDSASecp256K1() + .toByteArray()), + 1L)) + .build()) + .build()) + .payingWith(GENESIS) + .signedBy(GENESIS, MULTI_KEY) + .alsoSigningWithFullPrefix(MULTI_KEY) + .via(TRANSFER_TXN2) + .gas(GAS_TO_OFFER), + contractCall( + NESTED_LAZY_PRECOMPILE_CONTRACT, + transferFn, + tokenTransferLists() + .withTokenTransferList(tokenTransferList() + .forToken(token2) + .withAccountAmounts( + accountAmount(sender, -1L), + accountAmountAlias( + recoverAddressFromPubKey( + ecdsaKey.getECDSASecp256K1() + .toByteArray()), + 1L)) + .build()) + .build(), + tokenTransferLists() + .withTokenTransferList(tokenTransferList() + .forToken(token2) + .withAccountAmounts( + accountAmount(sender, -1L), + accountAmountAlias( + recoverAddressFromPubKey( + ecdsaKey.getECDSASecp256K1() + .toByteArray()), + 1L)) + .build()) + .build()) + .payingWith(GENESIS) + .signedBy(GENESIS, MULTI_KEY) + .alsoSigningWithFullPrefix(MULTI_KEY) + .via(TRANSFER_TXN3) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED) + .gas(GAS_TO_OFFER), + childRecordsCheck( + TRANSFER_TXN, + SUCCESS, + recordWith().status(SUCCESS), + recordWith().status(SUCCESS), + recordWith().status(SUCCESS)), + childRecordsCheck( + TRANSFER_TXN2, + SUCCESS, + recordWith().status(SUCCESS), + recordWith().status(SUCCESS)), + childRecordsCheck( + TRANSFER_TXN3, + CONTRACT_REVERT_EXECUTED, + recordWith().status(NO_REMAINING_AUTOMATIC_ASSOCIATIONS)), + getAliasedAccountInfo(ECDSA_KEY) + .has(AccountInfoAsserts.accountWith() + .key(EMPTY_KEY) + .autoRenew(THREE_MONTHS_IN_SECONDS) + .receiverSigReq(false) + .memo(LAZY_MEMO)), + getAliasedAccountBalance(evmAddressBytes) + .hasTokenBalance(FUNGIBLE_TOKEN, amountToBeSent * 2 + 2) + .hasTokenBalance(NFT_TOKEN, 1) + .logged()); + })) + .then(); + } + + private HapiSpec cryptoTransferV2LazyCreate() { + final var NESTED_LAZY_PRECOMPILE_CONTRACT = "LazyPrecompileTransfersAtomic"; + final var FUNGIBLE_TOKEN_2 = "ftnt"; + final var INIT_BALANCE = 10 * ONE_HUNDRED_HBARS; + return propertyPreservingHapiSpec("cryptoTransferV2LazyCreate") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(SENDER) + .balance(INIT_BALANCE) + .key(MULTI_KEY) + .maxAutomaticTokenAssociations(5), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(FUNGIBLE_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(TOTAL_SUPPLY) + .treasury(TOKEN_TREASURY), + tokenCreate(FUNGIBLE_TOKEN_2) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(TOTAL_SUPPLY) + .treasury(TOKEN_TREASURY), + tokenCreate(NFT_TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .treasury(SENDER) + .initialSupply(0L) + .supplyKey(MULTI_KEY), + mintToken(NFT_TOKEN, List.of(META1, META2)), + newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), + cryptoTransfer(moving(500, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, SENDER)), + cryptoTransfer(moving(500, FUNGIBLE_TOKEN_2).between(TOKEN_TREASURY, SENDER)), + uploadInitCode(NESTED_LAZY_PRECOMPILE_CONTRACT), + contractCreate(NESTED_LAZY_PRECOMPILE_CONTRACT).maxAutomaticTokenAssociations(1), + getContractInfo(NESTED_LAZY_PRECOMPILE_CONTRACT) + .has(ContractInfoAsserts.contractWith().maxAutoAssociations(1)) + .logged()) + .when(withOpContext((spec, opLog) -> { + final var ecdsaKey = spec.registry().getKey(ECDSA_KEY); + final var tmp = ecdsaKey.getECDSASecp256K1().toByteArray(); + final var addressBytes = recoverAddressFromPubKey(tmp); + final var evmAddressBytes = ByteString.copyFrom(addressBytes); + final var token = spec.registry().getTokenID(FUNGIBLE_TOKEN); + final var token2 = spec.registry().getTokenID(FUNGIBLE_TOKEN_2); + final var nftToken = spec.registry().getTokenID(NFT_TOKEN); + final var sender = spec.registry().getAccountID(SENDER); + final var amountToBeSent = 50L; + + final var cryptoTransferV2LazyCreateFn = "cryptoTransferV2LazyCreate"; + allRunFor( + spec, + contractCall( + NESTED_LAZY_PRECOMPILE_CONTRACT, + cryptoTransferV2LazyCreateFn, + transferList() + .withAccountAmounts( + accountAmount(sender, -amountToBeSent, false), + UtilVerbs.accountAmountAlias( + addressBytes, amountToBeSent, false)) + .build(), + tokenTransferLists() + .withTokenTransferList( + tokenTransferList() + .forToken(token) + .withAccountAmounts( + accountAmount( + sender, -amountToBeSent, false), + UtilVerbs.accountAmountAlias( + addressBytes, + amountToBeSent, + false)) + .build(), + tokenTransferList() + .forToken(nftToken) + .withNftTransfers(UtilVerbs.nftTransferToAlias( + sender, addressBytes, 1L, false)) + .build()) + .build(), + transferList() + .withAccountAmounts( + accountAmount(sender, -amountToBeSent, false), + UtilVerbs.accountAmountAlias( + addressBytes, amountToBeSent, false)) + .build(), + tokenTransferLists() + .withTokenTransferList(tokenTransferList() + .forToken(token) + .withAccountAmounts( + accountAmount(sender, -amountToBeSent, false), + UtilVerbs.accountAmountAlias( + addressBytes, amountToBeSent, false)) + .build()) + .build()) + .payingWith(GENESIS) + .via(TRANSFER_TXN) + .signedBy(GENESIS, MULTI_KEY) + .alsoSigningWithFullPrefix(MULTI_KEY) + .gas(GAS_TO_OFFER), + contractCall( + NESTED_LAZY_PRECOMPILE_CONTRACT, + cryptoTransferV2LazyCreateFn, + transferList() + .withAccountAmounts( + accountAmount(sender, -amountToBeSent, false), + UtilVerbs.accountAmountAlias( + addressBytes, amountToBeSent, false)) + .build(), + EMPTY_TUPLE_ARRAY, + transferList() + .withAccountAmounts( + accountAmount(sender, -amountToBeSent, false), + UtilVerbs.accountAmountAlias( + addressBytes, amountToBeSent, false)) + .build(), + EMPTY_TUPLE_ARRAY) + .payingWith(GENESIS) + .signedBy(GENESIS, MULTI_KEY) + .alsoSigningWithFullPrefix(MULTI_KEY) + .via(TRANSFER_TXN2) + .gas(GAS_TO_OFFER), + contractCall( + NESTED_LAZY_PRECOMPILE_CONTRACT, + cryptoTransferV2LazyCreateFn, + transferList() + .withAccountAmounts(EMPTY_TUPLE_ARRAY) + .build(), + tokenTransferLists() + .withTokenTransferList(tokenTransferList() + .forToken(token2) + .withAccountAmounts( + accountAmount(sender, -amountToBeSent, false), + UtilVerbs.accountAmountAlias( + addressBytes, amountToBeSent, false)) + .build()) + .build(), + transferList() + .withAccountAmounts(EMPTY_TUPLE_ARRAY) + .build(), + EMPTY_TUPLE_ARRAY) + .payingWith(GENESIS) + .signedBy(GENESIS, MULTI_KEY) + .alsoSigningWithFullPrefix(MULTI_KEY) + .via(TRANSFER_TXN3) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED) + .gas(GAS_TO_OFFER), + childRecordsCheck( + TRANSFER_TXN, + SUCCESS, + recordWith().status(SUCCESS), + recordWith().status(SUCCESS), + recordWith().status(SUCCESS)), + childRecordsCheck( + TRANSFER_TXN2, + SUCCESS, + recordWith().status(SUCCESS), + recordWith().status(SUCCESS)), + childRecordsCheck( + TRANSFER_TXN3, + CONTRACT_REVERT_EXECUTED, + recordWith().status(NO_REMAINING_AUTOMATIC_ASSOCIATIONS)), + getAliasedAccountInfo(ECDSA_KEY) + .has(AccountInfoAsserts.accountWith() + .key(EMPTY_KEY) + .autoRenew(THREE_MONTHS_IN_SECONDS) + .receiverSigReq(false) + .memo(LAZY_MEMO)), + getAliasedAccountBalance(evmAddressBytes) + .hasTinyBars(4 * amountToBeSent) + .hasTokenBalance(FUNGIBLE_TOKEN, amountToBeSent * 2) + .hasTokenBalance(NFT_TOKEN, 1) + .logged()); + })) + .then(); + } + + private HapiSpec transferTokenLazyCreate() { + final AtomicReference tokenAddr = new AtomicReference<>(); + + return propertyPreservingHapiSpec("transferTokenLazyCreate") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), + newKeyNamed(MULTI_KEY), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(OWNER).balance(100 * ONE_HUNDRED_HBARS), + tokenCreate(FUNGIBLE_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(5) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .exposingCreatedIdTo(id -> tokenAddr.set( + HapiPropertySource.asHexedSolidityAddress(HapiPropertySource.asToken(id)))), + uploadInitCode(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), + contractCreate(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), + tokenAssociate(OWNER, List.of(FUNGIBLE_TOKEN)), + cryptoTransfer(moving(5, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, OWNER))) + .when(withOpContext((spec, opLog) -> { + final var ecdsaKey = spec.registry().getKey(ECDSA_KEY); + final var tmp = ecdsaKey.getECDSASecp256K1().toByteArray(); + final var addressBytes = recoverAddressFromPubKey(tmp); + final var alias = ByteStringUtils.wrapUnsafely(addressBytes); + allRunFor( + spec, + contractCall( + TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT, + "transferTokenCallNestedThenAgain", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(FUNGIBLE_TOKEN))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(OWNER))), + HapiParserUtil.asHeadlongAddress(addressBytes), + 2L, + 2L) + .via(TRANSFER_TOKEN_TXN) + .alsoSigningWithFullPrefix(OWNER) + .gas(GAS_TO_OFFER) + .hasKnownStatus(SUCCESS), + getAliasedAccountInfo(ECDSA_KEY) + .has(AccountInfoAsserts.accountWith() + .key(EMPTY_KEY) + .autoRenew(THREE_MONTHS_IN_SECONDS) + .receiverSigReq(false) + .memo(LAZY_MEMO)), + getAliasedAccountBalance(alias) + .hasTokenBalance(FUNGIBLE_TOKEN, 4) + .logged(), + childRecordsCheck( + TRANSFER_TOKEN_TXN, + SUCCESS, + recordWith().status(SUCCESS), + recordWith().status(SUCCESS), + recordWith().status(SUCCESS))); + })) + .then(); + } + + private HapiSpec transferTokensToEVMAddressAliasRevertAndTransferAgainSuccessfully() { + final AtomicReference tokenAddr = new AtomicReference<>(); + + return propertyPreservingHapiSpec("transferTokensToEVMAddressAliasRevertAndTransferAgainSuccessfully") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), + newKeyNamed(MULTI_KEY), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(OWNER).balance(100 * ONE_HUNDRED_HBARS), + tokenCreate(FUNGIBLE_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(5) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .exposingCreatedIdTo(id -> tokenAddr.set( + HapiPropertySource.asHexedSolidityAddress(HapiPropertySource.asToken(id)))), + uploadInitCode(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), + contractCreate(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), + tokenAssociate(OWNER, List.of(FUNGIBLE_TOKEN)), + cryptoTransfer(moving(5, FUNGIBLE_TOKEN).between(TOKEN_TREASURY, OWNER))) + .when(withOpContext((spec, opLog) -> { + final var ecdsaKey = spec.registry().getKey(ECDSA_KEY); + final var tmp = ecdsaKey.getECDSASecp256K1().toByteArray(); + final var addressBytes = recoverAddressFromPubKey(tmp); + final var alias = ByteStringUtils.wrapUnsafely(addressBytes); + assert addressBytes != null; + allRunFor( + spec, + contractCall( + TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT, + "transferTokensCallNestedThenAgain", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(FUNGIBLE_TOKEN))), + new Address[] { + HapiParserUtil.asHeadlongAddress(asAddress( + spec.registry().getAccountID(OWNER))), + HapiParserUtil.asHeadlongAddress(addressBytes) + }, + new long[] {-2L, 2L}, + new long[] {-2L, 2L}) + .via(TRANSFER_TOKENS_TXN) + .gas(GAS_TO_OFFER) + .alsoSigningWithFullPrefix(OWNER) + .hasKnownStatus(SUCCESS), + getAliasedAccountInfo(ECDSA_KEY) + .has(AccountInfoAsserts.accountWith() + .key(EMPTY_KEY) + .autoRenew(THREE_MONTHS_IN_SECONDS) + .receiverSigReq(false) + .memo(LAZY_MEMO)), + getAliasedAccountBalance(alias) + .hasTokenBalance(FUNGIBLE_TOKEN, 4) + .logged(), + childRecordsCheck( + TRANSFER_TOKENS_TXN, + SUCCESS, + recordWith().status(SUCCESS), + recordWith().status(SUCCESS), + recordWith().status(SUCCESS))); + })) + .then(); + } + + private HapiSpec transferNftLazyCreate() { + return propertyPreservingHapiSpec("transferNftLazyCreate") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), + newKeyNamed(MULTI_KEY), + cryptoCreate(OWNER).balance(100 * ONE_HUNDRED_HBARS), + cryptoCreate(SPENDER), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(NON_FUNGIBLE_TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .initialSupply(0) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY), + uploadInitCode(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), + contractCreate(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), + tokenAssociate(OWNER, NON_FUNGIBLE_TOKEN), + tokenAssociate(SPENDER, NON_FUNGIBLE_TOKEN), + tokenAssociate(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT, NON_FUNGIBLE_TOKEN), + mintToken(NON_FUNGIBLE_TOKEN, List.of(FIRST_META, SECOND_META)), + cryptoTransfer(movingUnique(NON_FUNGIBLE_TOKEN, 1L, 2L).between(TOKEN_TREASURY, OWNER))) + .when(withOpContext((spec, opLog) -> { + final var ecdsaKey = spec.registry().getKey(ECDSA_KEY); + final var tmp = ecdsaKey.getECDSASecp256K1().toByteArray(); + final var addressBytes = recoverAddressFromPubKey(tmp); + final var alias = ByteStringUtils.wrapUnsafely(addressBytes); + allRunFor( + spec, + contractCall( + TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT, + "transferNFTCallNestedThenAgain", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(NON_FUNGIBLE_TOKEN))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(OWNER))), + HapiParserUtil.asHeadlongAddress(addressBytes), + 1L, + 2L) + .via(TRANSFER_NFT_TXN) + .alsoSigningWithFullPrefix(OWNER) + .gas(GAS_TO_OFFER) + .hasKnownStatus(SUCCESS), + getAliasedAccountInfo(ECDSA_KEY) + .has(AccountInfoAsserts.accountWith() + .key(EMPTY_KEY) + .autoRenew(THREE_MONTHS_IN_SECONDS) + .receiverSigReq(false) + .memo(LAZY_MEMO)), + getAliasedAccountBalance(alias) + .hasTokenBalance(NON_FUNGIBLE_TOKEN, 2) + .logged(), + childRecordsCheck( + TRANSFER_NFT_TXN, + SUCCESS, + recordWith().status(SUCCESS), + recordWith().status(SUCCESS), + recordWith().status(SUCCESS))); + })) + .then(); + } + + private HapiSpec transferNftsLazyCreate() { + return propertyPreservingHapiSpec("transferNftsLazyCreate") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), + newKeyNamed(MULTI_KEY), + cryptoCreate(OWNER).balance(100 * ONE_HUNDRED_HBARS), + cryptoCreate(SPENDER), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(NON_FUNGIBLE_TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .initialSupply(0) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY), + uploadInitCode(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), + contractCreate(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT), + tokenAssociate(OWNER, NON_FUNGIBLE_TOKEN), + tokenAssociate(SPENDER, NON_FUNGIBLE_TOKEN), + tokenAssociate(TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT, NON_FUNGIBLE_TOKEN), + mintToken(NON_FUNGIBLE_TOKEN, List.of(FIRST_META, SECOND_META)), + cryptoTransfer(movingUnique(NON_FUNGIBLE_TOKEN, 1L, 2L).between(TOKEN_TREASURY, OWNER))) + .when(withOpContext((spec, opLog) -> { + final var ecdsaKey = spec.registry().getKey(ECDSA_KEY); + final var tmp = ecdsaKey.getECDSASecp256K1().toByteArray(); + final var addressBytes = recoverAddressFromPubKey(tmp); + final var alias = ByteStringUtils.wrapUnsafely(addressBytes); + assert addressBytes != null; + allRunFor( + spec, + contractCall( + TRANSFER_TO_ALIAS_PRECOMPILE_CONTRACT, + "transferNFTsCallNestedThenAgain", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(NON_FUNGIBLE_TOKEN))), + new Address[] { + HapiParserUtil.asHeadlongAddress(asAddress( + spec.registry().getAccountID(OWNER))) + }, + new Address[] {HapiParserUtil.asHeadlongAddress(addressBytes)}, + new long[] {1L}, + new long[] {2L}) + .via(TRANSFER_NFTS_TXN) + .alsoSigningWithFullPrefix(OWNER) + .gas(GAS_TO_OFFER), + getAliasedAccountInfo(ECDSA_KEY) + .has(AccountInfoAsserts.accountWith() + .key(EMPTY_KEY) + .autoRenew(THREE_MONTHS_IN_SECONDS) + .receiverSigReq(false) + .memo(LAZY_MEMO)), + getAliasedAccountBalance(alias) + .hasTokenBalance(NON_FUNGIBLE_TOKEN, 2) + .logged(), + childRecordsCheck( + TRANSFER_NFTS_TXN, + SUCCESS, + recordWith().status(SUCCESS), + recordWith().status(SUCCESS), + recordWith().status(SUCCESS))); + })) + .then(); + } + + private HapiSpec htsTransferFromFungibleTokenLazyCreate() { + final var allowance = 10L; + final var successfulTransferFromTxn = "txn"; + return propertyPreservingHapiSpec("htsTransferFromFungibleTokenLazyCreate") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ECDSA_KEY).shape(SECP_256K1_SHAPE), + newKeyNamed(MULTI_KEY), + cryptoCreate(OWNER).balance(100 * ONE_HUNDRED_HBARS).maxAutomaticTokenAssociations(5), + tokenCreate(FUNGIBLE_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .supplyType(TokenSupplyType.FINITE) + .initialSupply(10L) + .maxSupply(1000L) + .supplyKey(MULTI_KEY) + .treasury(OWNER), + uploadInitCode(HTS_TRANSFER_FROM_CONTRACT), + contractCreate(HTS_TRANSFER_FROM_CONTRACT), + cryptoApproveAllowance() + .payingWith(DEFAULT_PAYER) + .addTokenAllowance(OWNER, FUNGIBLE_TOKEN, HTS_TRANSFER_FROM_CONTRACT, allowance) + .via(BASE_APPROVE_TXN) + .signedBy(DEFAULT_PAYER, OWNER) + .fee(ONE_HBAR), + getAccountDetails(OWNER) + .payingWith(GENESIS) + .has(accountDetailsWith() + .tokenAllowancesContaining( + FUNGIBLE_TOKEN, HTS_TRANSFER_FROM_CONTRACT, allowance))) + .when(withOpContext((spec, opLog) -> { + final var ecdsaKey = spec.registry().getKey(ECDSA_KEY); + final var tmp = ecdsaKey.getECDSASecp256K1().toByteArray(); + final var addressBytes = recoverAddressFromPubKey(tmp); + final ByteString alias = ByteStringUtils.wrapUnsafely(addressBytes); + allRunFor( + spec, + // transfer allowance/2 amount + contractCall( + HTS_TRANSFER_FROM_CONTRACT, + HTS_TRANSFER_FROM, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(FUNGIBLE_TOKEN))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(OWNER))), + HapiParserUtil.asHeadlongAddress(addressBytes), + BigInteger.valueOf(allowance / 2)) + .gas(GAS_TO_OFFER) + .via(successfulTransferFromTxn) + .hasKnownStatus(SUCCESS), + childRecordsCheck( + successfulTransferFromTxn, + SUCCESS, + recordWith().status(SUCCESS).memo(LAZY_MEMO), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_TRANSFER_FROM) + .withStatus(SUCCESS)))), + getAliasedAccountInfo(ECDSA_KEY) + .logged() + .has(AccountInfoAsserts.accountWith() + .key(EMPTY_KEY) + .autoRenew(THREE_MONTHS_IN_SECONDS) + .receiverSigReq(false) + .memo(LAZY_MEMO)), + getAliasedAccountBalance(alias) + .hasTokenBalance(FUNGIBLE_TOKEN, allowance / 2) + .logged()); + })) + .then(); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/MixedHTSPrecompileTestsSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/MixedHTSPrecompileTestsV1SecurityModelSuite.java similarity index 91% rename from hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/MixedHTSPrecompileTestsSuite.java rename to hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/MixedHTSPrecompileTestsV1SecurityModelSuite.java index d1a93ff21d25..28aeacd47139 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/MixedHTSPrecompileTestsSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/MixedHTSPrecompileTestsV1SecurityModelSuite.java @@ -17,7 +17,7 @@ package com.hedera.services.bdd.suites.contract.precompile; import static com.hedera.services.bdd.spec.HapiPropertySource.asTokenString; -import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; 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.keys.KeyShape.ED25519; @@ -36,8 +36,11 @@ import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT; @@ -56,8 +59,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -public class MixedHTSPrecompileTestsSuite extends HapiSuite { - private static final Logger log = LogManager.getLogger(MixedHTSPrecompileTestsSuite.class); +public class MixedHTSPrecompileTestsV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(MixedHTSPrecompileTestsV1SecurityModelSuite.class); private static final long GAS_TO_OFFER = 4_000_000L; private static final long TOTAL_SUPPLY = 1_000; @@ -70,12 +73,12 @@ public class MixedHTSPrecompileTestsSuite extends HapiSuite { private static final String EXPLICIT_CREATE_RESULT = "Explicit create result is {}"; public static void main(String... args) { - new MixedHTSPrecompileTestsSuite().runSuiteAsync(); + new MixedHTSPrecompileTestsV1SecurityModelSuite().runSuiteSync(); } @Override public boolean canRunConcurrent() { - return true; + return false; } @Override @@ -91,8 +94,10 @@ private HapiSpec hscsPrec021TryCatchConstructOnlyRollsBackTheFailedPrecompile() final var outerContract = "AssociateTryCatch"; final var nestedContract = "CalledContract"; - return defaultHapiSpec("HSCS_PREC_021_try_catch_construct_only_rolls_back_the_failed_precompile") + return propertyPreservingHapiSpec("hscsPrec021TryCatchConstructOnlyRollsBackTheFailedPrecompile") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), cryptoCreate(theAccount).balance(10 * ONE_HUNDRED_HBARS), cryptoCreate(TOKEN_TREASURY), tokenCreate(token) @@ -144,8 +149,10 @@ private HapiSpec createTokenWithFixedFeeThenTransferAndAssessFee() { final var TREASURY_KEY = "treasuryKey"; final var RECIPIENT_KEY = "recipientKey"; final var SECOND_RECIPIENT_KEY = "secondRecipientKey"; - return defaultHapiSpec("createTokenWithFixedFeeThenTransferAndAssessFee") + return propertyPreservingHapiSpec("createTokenWithFixedFeeThenTransferAndAssessFee") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), newKeyNamed(ED25519KEY).shape(ED25519), newKeyNamed(FEE_COLLECTOR_KEY), newKeyNamed(TREASURY_KEY), diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/PauseUnpauseTokenAccountPrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/PauseUnpauseTokenAccountPrecompileSuite.java index 68ee9a44cbf2..a4c089bb4e1a 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/PauseUnpauseTokenAccountPrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/PauseUnpauseTokenAccountPrecompileSuite.java @@ -19,15 +19,11 @@ import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; 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.getTokenInfo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; -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.tokenCreate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenDelete; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenUnpause; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; @@ -40,14 +36,9 @@ import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_PAUSE_KEY; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_WAS_DELETED; -import static com.hederahashgraph.api.proto.java.TokenPauseStatus.Paused; -import static com.hederahashgraph.api.proto.java.TokenPauseStatus.Unpaused; import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; -import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.suites.HapiSuite; @@ -62,20 +53,12 @@ public class PauseUnpauseTokenAccountPrecompileSuite extends HapiSuite { private static final Logger log = LogManager.getLogger(PauseUnpauseTokenAccountPrecompileSuite.class); public static final String PAUSE_UNPAUSE_CONTRACT = "PauseUnpauseTokenAccount"; - private static final String UNPAUSE_KEY = "UNPAUSE_KEY"; - - private static final String PAUSE_KEY = "PAUSE_KEY"; - private static final String ACCOUNT = "account"; public static final long INITIAL_BALANCE = 1_000_000_000L; private static final long GAS_TO_OFFER = 4_000_000L; public static final String PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME = "pauseTokenAccount"; public static final String UNPAUSE_TOKEN_ACCOUNT_FUNCTION_NAME = "unpauseTokenAccount"; - private static final String PAUSE_FUNGIBLE_TXN = "pauseFungibleTxn"; - private static final String UNPAUSE_FUNGIBLE_TXN = "unpauseFungibleTxn"; - private static final String PAUSE_NONFUNGIBLE_TXN = "pauseNonFungibleTxn"; - private static final String UNPAUSE_NONFUNGIBLE_TXN = "unpauseNonFungibleTxn"; private static final String INVALID_ADDRESS = "0x0000000000000000000000000000000000123456"; public static final String UNPAUSE_TX = "UnpauseTx"; public static final String PAUSE_TX = "PauseTx"; @@ -96,13 +79,7 @@ protected Logger getResultsLogger() { @Override public List getSpecsInSuite() { - return List.of( - pauseFungibleTokenHappyPath(), - unpauseFungibleTokenHappyPath(), - pauseNonFungibleTokenHappyPath(), - unpauseNonFungibleTokenHappyPath(), - noTokenIdReverts(), - noAccountKeyReverts()); + return List.of(noTokenIdReverts(), noAccountKeyReverts()); } private HapiSpec noTokenIdReverts() { @@ -201,236 +178,4 @@ private HapiSpec noAccountKeyReverts() { .contractCallResult( htsPrecompileResult().withStatus(TOKEN_HAS_NO_PAUSE_KEY))))); } - - HapiSpec pauseFungibleTokenHappyPath() { - final AtomicReference vanillaTokenID = new AtomicReference<>(); - return defaultHapiSpec("PauseFungibleTokenHappyPath") - .given( - newKeyNamed(MULTI_KEY), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(ACCOUNT).balance(INITIAL_BALANCE), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .pauseKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .initialSupply(1_000) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - uploadInitCode(PAUSE_UNPAUSE_CONTRACT), - contractCreate(PAUSE_UNPAUSE_CONTRACT)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - PAUSE_UNPAUSE_CONTRACT, - PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("pauseFungibleAccountDoesNotOwnPauseKeyFailingTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - cryptoUpdate(ACCOUNT).key(MULTI_KEY), - contractCall( - PAUSE_UNPAUSE_CONTRACT, - PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via(PAUSE_FUNGIBLE_TXN) - .gas(GAS_TO_OFFER), - getTokenInfo(VANILLA_TOKEN).hasPauseStatus(Paused), - tokenUnpause(VANILLA_TOKEN), - tokenDelete(VANILLA_TOKEN), - contractCall( - PAUSE_UNPAUSE_CONTRACT, - PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("pauseFungibleAccountIsDeletedFailingTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) - .then( - childRecordsCheck( - "pauseFungibleAccountDoesNotOwnPauseKeyFailingTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_SIGNATURE) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), - childRecordsCheck( - "pauseFungibleAccountIsDeletedFailingTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(TOKEN_WAS_DELETED) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(TOKEN_WAS_DELETED))))); - } - - HapiSpec unpauseFungibleTokenHappyPath() { - final AtomicReference vanillaTokenID = new AtomicReference<>(); - return defaultHapiSpec("UnpauseFungibleTokenHappyPath") - .given( - newKeyNamed(UNPAUSE_KEY), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(ACCOUNT).balance(INITIAL_BALANCE), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .pauseKey(UNPAUSE_KEY) - .initialSupply(1_000) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - uploadInitCode(PAUSE_UNPAUSE_CONTRACT), - contractCreate(PAUSE_UNPAUSE_CONTRACT)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - PAUSE_UNPAUSE_CONTRACT, - UNPAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("unpauseFungibleAccountDoesNotOwnPauseKeyFailingTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - cryptoUpdate(ACCOUNT).key(UNPAUSE_KEY), - contractCall( - PAUSE_UNPAUSE_CONTRACT, - UNPAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via(UNPAUSE_FUNGIBLE_TXN) - .gas(GAS_TO_OFFER)))) - .then( - childRecordsCheck( - "unpauseFungibleAccountDoesNotOwnPauseKeyFailingTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_SIGNATURE) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), - getTokenInfo(VANILLA_TOKEN).hasPauseStatus(Unpaused)); - } - - HapiSpec pauseNonFungibleTokenHappyPath() { - final AtomicReference vanillaTokenID = new AtomicReference<>(); - return defaultHapiSpec("PauseNonFungibleTokenHappyPath") - .given( - newKeyNamed(MULTI_KEY), - newKeyNamed(PAUSE_KEY), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(ACCOUNT).balance(INITIAL_BALANCE), - tokenCreate(VANILLA_TOKEN) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .pauseKey(PAUSE_KEY) - .supplyKey(MULTI_KEY) - .initialSupply(0) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - uploadInitCode(PAUSE_UNPAUSE_CONTRACT), - contractCreate(PAUSE_UNPAUSE_CONTRACT)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - PAUSE_UNPAUSE_CONTRACT, - PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("pauseNonFungibleAccountDoesNotOwnPauseKeyFailingTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - cryptoUpdate(ACCOUNT).key(MULTI_KEY).key(PAUSE_KEY), - contractCall( - PAUSE_UNPAUSE_CONTRACT, - PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via(PAUSE_NONFUNGIBLE_TXN) - .gas(GAS_TO_OFFER), - getTokenInfo(VANILLA_TOKEN).hasPauseStatus(Paused), - tokenUnpause(VANILLA_TOKEN), - tokenDelete(VANILLA_TOKEN), - contractCall( - PAUSE_UNPAUSE_CONTRACT, - PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("pauseNonFungibleAccountIsDeletedFailingTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) - .then( - childRecordsCheck( - "pauseNonFungibleAccountDoesNotOwnPauseKeyFailingTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_SIGNATURE) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), - childRecordsCheck( - "pauseNonFungibleAccountIsDeletedFailingTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(TOKEN_WAS_DELETED) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(TOKEN_WAS_DELETED))))); - } - - HapiSpec unpauseNonFungibleTokenHappyPath() { - final AtomicReference vanillaTokenID = new AtomicReference<>(); - return defaultHapiSpec("UnpauseNonFungibleTokenHappyPath") - .given( - newKeyNamed(MULTI_KEY), - newKeyNamed(UNPAUSE_KEY), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(ACCOUNT).balance(INITIAL_BALANCE), - tokenCreate(VANILLA_TOKEN) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(TOKEN_TREASURY) - .pauseKey(UNPAUSE_KEY) - .supplyKey(MULTI_KEY) - .initialSupply(0) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - uploadInitCode(PAUSE_UNPAUSE_CONTRACT), - contractCreate(PAUSE_UNPAUSE_CONTRACT)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - PAUSE_UNPAUSE_CONTRACT, - UNPAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via("unpauseNonFungibleAccountDoesNotOwnPauseKeyFailingTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - cryptoUpdate(ACCOUNT).key(UNPAUSE_KEY), - contractCall( - PAUSE_UNPAUSE_CONTRACT, - UNPAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, - asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .via(UNPAUSE_NONFUNGIBLE_TXN) - .gas(GAS_TO_OFFER)))) - .then( - childRecordsCheck( - "unpauseNonFungibleAccountDoesNotOwnPauseKeyFailingTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_SIGNATURE) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), - getTokenInfo(VANILLA_TOKEN).hasPauseStatus(Unpaused)); - } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/PauseUnpauseTokenAccountPrecompileV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/PauseUnpauseTokenAccountPrecompileV1SecurityModelSuite.java new file mode 100644 index 000000000000..0411d15f7387 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/PauseUnpauseTokenAccountPrecompileV1SecurityModelSuite.java @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2021-2023 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.spec.HapiSpec.propertyPreservingHapiSpec; +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.getTokenInfo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoUpdate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenDelete; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenUnpause; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asHexedAddress; +import static com.hedera.services.bdd.suites.contract.Utils.asToken; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.MULTI_KEY; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_WAS_DELETED; +import static com.hederahashgraph.api.proto.java.TokenPauseStatus.Paused; +import static com.hederahashgraph.api.proto.java.TokenPauseStatus.Unpaused; +import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; +import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; + +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.TokenID; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse +public class PauseUnpauseTokenAccountPrecompileV1SecurityModelSuite extends HapiSuite { + private static final Logger log = + LogManager.getLogger(PauseUnpauseTokenAccountPrecompileV1SecurityModelSuite.class); + public static final String PAUSE_UNPAUSE_CONTRACT = "PauseUnpauseTokenAccount"; + + private static final String UNPAUSE_KEY = "UNPAUSE_KEY"; + + private static final String PAUSE_KEY = "PAUSE_KEY"; + + private static final String ACCOUNT = "account"; + + public static final long INITIAL_BALANCE = 1_000_000_000L; + private static final long GAS_TO_OFFER = 4_000_000L; + public static final String PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME = "pauseTokenAccount"; + public static final String UNPAUSE_TOKEN_ACCOUNT_FUNCTION_NAME = "unpauseTokenAccount"; + private static final String PAUSE_FUNGIBLE_TXN = "pauseFungibleTxn"; + private static final String UNPAUSE_FUNGIBLE_TXN = "unpauseFungibleTxn"; + private static final String PAUSE_NONFUNGIBLE_TXN = "pauseNonFungibleTxn"; + private static final String UNPAUSE_NONFUNGIBLE_TXN = "unpauseNonFungibleTxn"; + public static final String UNPAUSE_TX = "UnpauseTx"; + public static final String PAUSE_TX = "PauseTx"; + + public static void main(String... args) { + new PauseUnpauseTokenAccountPrecompileV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + protected Logger getResultsLogger() { + return log; + } + + @Override + public List getSpecsInSuite() { + return List.of( + pauseFungibleTokenHappyPath(), + unpauseFungibleTokenHappyPath(), + pauseNonFungibleTokenHappyPath(), + unpauseNonFungibleTokenHappyPath()); + } + + HapiSpec pauseFungibleTokenHappyPath() { + final AtomicReference vanillaTokenID = new AtomicReference<>(); + return propertyPreservingHapiSpec("PauseFungibleTokenHappyPath") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,TokenCreate,TokenDelete,TokenPause,TokenUnpause", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(ACCOUNT).balance(INITIAL_BALANCE), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .pauseKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .initialSupply(1_000) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + uploadInitCode(PAUSE_UNPAUSE_CONTRACT), + contractCreate(PAUSE_UNPAUSE_CONTRACT)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + PAUSE_UNPAUSE_CONTRACT, + PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("pauseFungibleAccountDoesNotOwnPauseKeyFailingTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + cryptoUpdate(ACCOUNT).key(MULTI_KEY), + contractCall( + PAUSE_UNPAUSE_CONTRACT, + PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via(PAUSE_FUNGIBLE_TXN) + .gas(GAS_TO_OFFER), + getTokenInfo(VANILLA_TOKEN).hasPauseStatus(Paused), + tokenUnpause(VANILLA_TOKEN), + tokenDelete(VANILLA_TOKEN), + contractCall( + PAUSE_UNPAUSE_CONTRACT, + PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("pauseFungibleAccountIsDeletedFailingTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) + .then( + childRecordsCheck( + "pauseFungibleAccountDoesNotOwnPauseKeyFailingTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_SIGNATURE) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), + childRecordsCheck( + "pauseFungibleAccountIsDeletedFailingTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(TOKEN_WAS_DELETED) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(TOKEN_WAS_DELETED))))); + } + + HapiSpec unpauseFungibleTokenHappyPath() { + final AtomicReference vanillaTokenID = new AtomicReference<>(); + return propertyPreservingHapiSpec("UnpauseFungibleTokenHappyPath") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,TokenCreate,TokenDelete,TokenPause,TokenUnpause", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(UNPAUSE_KEY), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(ACCOUNT).balance(INITIAL_BALANCE), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .pauseKey(UNPAUSE_KEY) + .initialSupply(1_000) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + uploadInitCode(PAUSE_UNPAUSE_CONTRACT), + contractCreate(PAUSE_UNPAUSE_CONTRACT)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + PAUSE_UNPAUSE_CONTRACT, + UNPAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("unpauseFungibleAccountDoesNotOwnPauseKeyFailingTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + cryptoUpdate(ACCOUNT).key(UNPAUSE_KEY), + contractCall( + PAUSE_UNPAUSE_CONTRACT, + UNPAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via(UNPAUSE_FUNGIBLE_TXN) + .gas(GAS_TO_OFFER)))) + .then( + childRecordsCheck( + "unpauseFungibleAccountDoesNotOwnPauseKeyFailingTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_SIGNATURE) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), + getTokenInfo(VANILLA_TOKEN).hasPauseStatus(Unpaused)); + } + + HapiSpec pauseNonFungibleTokenHappyPath() { + final AtomicReference vanillaTokenID = new AtomicReference<>(); + return propertyPreservingHapiSpec("PauseNonFungibleTokenHappyPath") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,TokenCreate,TokenDelete,TokenPause,TokenUnpause", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + newKeyNamed(PAUSE_KEY), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(ACCOUNT).balance(INITIAL_BALANCE), + tokenCreate(VANILLA_TOKEN) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .pauseKey(PAUSE_KEY) + .supplyKey(MULTI_KEY) + .initialSupply(0) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + uploadInitCode(PAUSE_UNPAUSE_CONTRACT), + contractCreate(PAUSE_UNPAUSE_CONTRACT)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + PAUSE_UNPAUSE_CONTRACT, + PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("pauseNonFungibleAccountDoesNotOwnPauseKeyFailingTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + cryptoUpdate(ACCOUNT).key(MULTI_KEY).key(PAUSE_KEY), + contractCall( + PAUSE_UNPAUSE_CONTRACT, + PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via(PAUSE_NONFUNGIBLE_TXN) + .gas(GAS_TO_OFFER), + getTokenInfo(VANILLA_TOKEN).hasPauseStatus(Paused), + tokenUnpause(VANILLA_TOKEN), + tokenDelete(VANILLA_TOKEN), + contractCall( + PAUSE_UNPAUSE_CONTRACT, + PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("pauseNonFungibleAccountIsDeletedFailingTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) + .then( + childRecordsCheck( + "pauseNonFungibleAccountDoesNotOwnPauseKeyFailingTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_SIGNATURE) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), + childRecordsCheck( + "pauseNonFungibleAccountIsDeletedFailingTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(TOKEN_WAS_DELETED) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(TOKEN_WAS_DELETED))))); + } + + HapiSpec unpauseNonFungibleTokenHappyPath() { + final AtomicReference vanillaTokenID = new AtomicReference<>(); + return propertyPreservingHapiSpec("UnpauseNonFungibleTokenHappyPath") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,TokenCreate,TokenDelete,TokenPause,TokenUnpause", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + newKeyNamed(UNPAUSE_KEY), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(ACCOUNT).balance(INITIAL_BALANCE), + tokenCreate(VANILLA_TOKEN) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(TOKEN_TREASURY) + .pauseKey(UNPAUSE_KEY) + .supplyKey(MULTI_KEY) + .initialSupply(0) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + uploadInitCode(PAUSE_UNPAUSE_CONTRACT), + contractCreate(PAUSE_UNPAUSE_CONTRACT)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + PAUSE_UNPAUSE_CONTRACT, + UNPAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via("unpauseNonFungibleAccountDoesNotOwnPauseKeyFailingTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + cryptoUpdate(ACCOUNT).key(UNPAUSE_KEY), + contractCall( + PAUSE_UNPAUSE_CONTRACT, + UNPAUSE_TOKEN_ACCOUNT_FUNCTION_NAME, + asHeadlongAddress(asHexedAddress(vanillaTokenID.get()))) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .via(UNPAUSE_NONFUNGIBLE_TXN) + .gas(GAS_TO_OFFER)))) + .then( + childRecordsCheck( + "unpauseNonFungibleAccountDoesNotOwnPauseKeyFailingTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_SIGNATURE) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), + getTokenInfo(VANILLA_TOKEN).hasPauseStatus(Unpaused)); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/SigningReqsSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/SigningReqsSuite.java index 6143d5ac29c4..694794824dec 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/SigningReqsSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/SigningReqsSuite.java @@ -16,27 +16,21 @@ package com.hedera.services.bdd.suites.contract.precompile; -import static com.hedera.services.bdd.spec.HapiPropertySource.asToken; import static com.hedera.services.bdd.spec.HapiPropertySource.asTokenString; import static com.hedera.services.bdd.spec.HapiPropertySource.idAsHeadlongAddress; -import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; import static com.hedera.services.bdd.spec.keys.KeyShape.*; import static com.hedera.services.bdd.spec.queries.QueryVerbs.*; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.*; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.*; -import static com.hedera.services.bdd.suites.contract.precompile.WipeTokenAccountPrecompileSuite.*; import static com.hedera.services.bdd.suites.file.FileUpdateSuite.*; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import com.esaulpaugh.headlong.abi.Address; import com.hedera.services.bdd.spec.*; import com.hedera.services.bdd.spec.assertions.*; import com.hedera.services.bdd.spec.keys.KeyShape; -import com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoCreate; import com.hedera.services.bdd.spec.utilops.CustomSpecAssert; import com.hedera.services.bdd.suites.*; import com.hederahashgraph.api.proto.java.TokenID; @@ -58,7 +52,7 @@ public class SigningReqsSuite extends HapiSuite { private static final String LEGACY_ACTIVATIONS_PROP = "contracts.keys.legacyActivations"; public static final String AUTO_RENEW = "autoRenew"; - public static final String AR_KEY = "arKey"; + public static final int GAS_TO_OFFER = 1_000_000; public static void main(String... args) { new SigningReqsSuite().runSuiteAsync(); @@ -71,144 +65,7 @@ public boolean canRunConcurrent() { @Override public List getSpecsInSuite() { - return List.of( - newAutoRenewAccountMustSignUpdate(), - newTreasuryAccountMustSignUpdate(), - autoRenewAccountMustSignCreation(), - fractionalFeeCollectorMustSign(), - selfDenominatedFixedCollectorMustSign(), - autoRenewAccountCanUseLegacySigActivationIfConfigured()); - } - - @SuppressWarnings("java:S5960") - private HapiSpec selfDenominatedFixedCollectorMustSign() { - final var fcKey = "fcKey"; - final var arKey = AR_KEY; - final var feeCollector = "feeCollector"; - final var autoRenew = AUTO_RENEW; - final AtomicLong contractId = new AtomicLong(); - final AtomicReference
autoRenewAlias = new AtomicReference<>(); - final AtomicReference
feeCollectorAlias = new AtomicReference<>(); - final AtomicReference createdToken = new AtomicReference<>(); - - return defaultHapiSpec("SelfDenominatedFixedCollectorMustSign") - .given( - newKeyNamed(arKey).shape(SECP256K1), - newKeyNamed(fcKey).shape(SECP256K1), - cryptoCreate(CIVILIAN).balance(10L * ONE_HUNDRED_HBARS), - cryptoCreateWithExposingId(autoRenew, arKey, autoRenewAlias), - cryptoCreateWithExposingId(feeCollector, fcKey, feeCollectorAlias), - uploadInitCode(MINIMAL_CREATIONS_CONTRACT), - contractCreate(MINIMAL_CREATIONS_CONTRACT) - .gas(GAS_TO_OFFER) - .exposingNumTo(contractId::set)) - .when( - sourcing(() -> contractCall( - MINIMAL_CREATIONS_CONTRACT, - "makeRenewableTokenWithSelfDenominatedFixedFee", - autoRenewAlias.get(), - THREE_MONTHS_IN_SECONDS, - feeCollectorAlias.get()) - .via(FIRST_CREATE_TXN) - .gas(10L * GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .payingWith(CIVILIAN) - .alsoSigningWithFullPrefix(autoRenew) - .refusingEthConversion() - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)), - sourcing(() -> contractCall( - MINIMAL_CREATIONS_CONTRACT, - "makeRenewableTokenWithSelfDenominatedFixedFee", - autoRenewAlias.get(), - THREE_MONTHS_IN_SECONDS, - feeCollectorAlias.get()) - .via(FIRST_CREATE_TXN) - .gas(10L * GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .payingWith(CIVILIAN) - .alsoSigningWithFullPrefix(autoRenew, feeCollector) - .refusingEthConversion())) - .then( - getTxnRecord(FIRST_CREATE_TXN) - .andAllChildRecords() - .exposingTokenCreationsTo(creations -> createdToken.set(creations.get(0))), - sourcing(() -> getTokenInfo(asTokenString(createdToken.get())) - .hasAutoRenewAccount(autoRenew) - .logged() - .hasCustom((spec, fees) -> { - assertEquals(1, fees.size()); - final var fee = fees.get(0); - assertTrue(fee.hasFixedFee()); - assertEquals( - createdToken.get(), - fee.getFixedFee().getDenominatingTokenId()); - assertEquals( - spec.registry().getAccountID(feeCollector), fee.getFeeCollectorAccountId()); - }))); - } - - @SuppressWarnings("java:S5960") - private HapiSpec fractionalFeeCollectorMustSign() { - final var fcKey = "fcKey"; - final var arKey = AR_KEY; - final var feeCollector = "feeCollector"; - final var autoRenew = AUTO_RENEW; - final AtomicLong contractId = new AtomicLong(); - final AtomicReference
autoRenewAlias = new AtomicReference<>(); - final AtomicReference
feeCollectorAlias = new AtomicReference<>(); - final AtomicReference createdToken = new AtomicReference<>(); - - return defaultHapiSpec("FractionalFeeCollectorMustSign") - .given( - newKeyNamed(arKey).shape(SECP256K1), - newKeyNamed(fcKey).shape(SECP256K1), - cryptoCreate(CIVILIAN).balance(10L * ONE_HUNDRED_HBARS), - cryptoCreateWithExposingId(autoRenew, arKey, autoRenewAlias), - cryptoCreateWithExposingId(feeCollector, fcKey, feeCollectorAlias), - uploadInitCode(MINIMAL_CREATIONS_CONTRACT), - contractCreate(MINIMAL_CREATIONS_CONTRACT) - .gas(GAS_TO_OFFER) - .exposingNumTo(contractId::set)) - .when( - sourcing(() -> contractCall( - MINIMAL_CREATIONS_CONTRACT, - "makeRenewableTokenWithFractionalFee", - autoRenewAlias.get(), - THREE_MONTHS_IN_SECONDS, - feeCollectorAlias.get()) - .via(FIRST_CREATE_TXN) - .gas(10L * GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .payingWith(CIVILIAN) - .alsoSigningWithFullPrefix(autoRenew) - .refusingEthConversion() - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)), - sourcing(() -> contractCall( - MINIMAL_CREATIONS_CONTRACT, - "makeRenewableTokenWithFractionalFee", - autoRenewAlias.get(), - THREE_MONTHS_IN_SECONDS, - feeCollectorAlias.get()) - .via(FIRST_CREATE_TXN) - .gas(10L * GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .payingWith(CIVILIAN) - .alsoSigningWithFullPrefix(autoRenew, feeCollector) - .refusingEthConversion())) - .then( - getTxnRecord(FIRST_CREATE_TXN) - .andAllChildRecords() - .exposingTokenCreationsTo(creations -> createdToken.set(creations.get(0))), - sourcing(() -> getTokenInfo(asTokenString(createdToken.get())) - .hasAutoRenewAccount(autoRenew) - .logged() - .hasCustom((spec, fees) -> { - assertEquals(1, fees.size()); - final var fee = fees.get(0); - assertTrue(fee.hasFractionalFee()); - assertEquals( - spec.registry().getAccountID(feeCollector), fee.getFeeCollectorAccountId()); - }))); + return List.of(autoRenewAccountCanUseLegacySigActivationIfConfigured()); } private HapiSpec autoRenewAccountCanUseLegacySigActivationIfConfigured() { @@ -277,164 +134,6 @@ private HapiSpec autoRenewAccountCanUseLegacySigActivationIfConfigured() { getTokenInfo(asTokenString(createdToken.get())).hasAutoRenewAccount(autoRenew))); } - private HapiSpec autoRenewAccountMustSignCreation() { - final var arKey = AR_KEY; - final var autoRenew = AUTO_RENEW; - final AtomicReference
autoRenewAlias = new AtomicReference<>(); - final AtomicLong contractId = new AtomicLong(); - final AtomicReference createdToken = new AtomicReference<>(); - - return defaultHapiSpec("AutoRenewAccountMustSignCreation") - .given( - newKeyNamed(arKey).shape(SECP256K1), - cryptoCreate(CIVILIAN).balance(10L * ONE_HUNDRED_HBARS), - cryptoCreateWithExposingId(autoRenew, arKey, autoRenewAlias), - uploadInitCode(MINIMAL_CREATIONS_CONTRACT), - contractCreate(MINIMAL_CREATIONS_CONTRACT) - .exposingNumTo(contractId::set) - .gas(GAS_TO_OFFER)) - .when( - // Fails without the auto-renew account's full-prefix signature - sourcing(() -> contractCall( - MINIMAL_CREATIONS_CONTRACT, - "makeRenewableToken", - autoRenewAlias.get(), - THREE_MONTHS_IN_SECONDS) - .via(FIRST_CREATE_TXN) - .gas(10L * GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .payingWith(CIVILIAN) - .refusingEthConversion() - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)), - // Succeeds with the full-prefix signature - sourcing(() -> contractCall( - MINIMAL_CREATIONS_CONTRACT, - "makeRenewableToken", - autoRenewAlias.get(), - THREE_MONTHS_IN_SECONDS) - .via(SECOND_CREATE_TXN) - .gas(10L * GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .payingWith(CIVILIAN) - .alsoSigningWithFullPrefix(arKey) - .refusingEthConversion())) - .then( - getTxnRecord(SECOND_CREATE_TXN) - .andAllChildRecords() - .exposingTokenCreationsTo(creations -> createdToken.set(creations.get(0))), - childRecordsCheck( - FIRST_CREATE_TXN, - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith() - .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)), - sourcing(() -> - getTokenInfo(asTokenString(createdToken.get())).hasAutoRenewAccount(autoRenew))); - } - - private HapiSpec newTreasuryAccountMustSignUpdate() { - final var ft = "fungibleToken"; - final var ntKey = "ntKey"; - final var updateTxn = "updateTxn"; - final var newTreasury = "newTreasury"; - final AtomicReference
tokenMirrorAddr = new AtomicReference<>(); - final AtomicReference
newTreasuryAliasAddr = new AtomicReference<>(); - - return defaultHapiSpec("NewTreasuryAccountMustSignUpdate") - .given( - newKeyNamed(ntKey).shape(SECP256K1), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(newTreasury) - // The new treasury must either already be associated or - // have open auto-association slots; it's therefore a bit - // odd that we require it to also sign, but this is the - // HAPI behavior, so we should be consistent for now - .maxAutomaticTokenAssociations(1) - .key(ntKey) - .exposingCreatedIdTo(id -> newTreasuryAliasAddr.set(idAsHeadlongAddress(id))), - cryptoCreate(CIVILIAN).balance(10L * ONE_HUNDRED_HBARS), - uploadInitCode(MINIMAL_CREATIONS_CONTRACT), - contractCreate(MINIMAL_CREATIONS_CONTRACT).gas(GAS_TO_OFFER), - tokenCreate(ft) - .adminKey(CIVILIAN) - .treasury(TOKEN_TREASURY) - .exposingCreatedIdTo(idLit -> tokenMirrorAddr.set(idAsHeadlongAddress(asToken(idLit))))) - .when(sourcing(() -> contractCall( - MINIMAL_CREATIONS_CONTRACT, - "updateTokenWithNewTreasury", - tokenMirrorAddr.get(), - newTreasuryAliasAddr.get()) - .via(updateTxn) - .gas(10L * GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .payingWith(CIVILIAN) - .refusingEthConversion() - .hasKnownStatus(CONTRACT_REVERT_EXECUTED))) - .then( - childRecordsCheck( - updateTxn, - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith() - .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)), - // Treasury account is unchanged - getTokenInfo(ft).hasTreasury(TOKEN_TREASURY)); - } - - private HapiSpec newAutoRenewAccountMustSignUpdate() { - final var ft = "fungibleToken"; - final var narKey = "narKey"; - final var adminKey = "adminKey"; - final var updateTxn = "updateTxn"; - final var newAutoRenewAccount = "newAutoRenewAccount"; - final AtomicReference
tokenMirrorAddr = new AtomicReference<>(); - final AtomicReference
newAutoRenewAliasAddr = new AtomicReference<>(); - - return defaultHapiSpec("NewAutoRenewAccountMustSign") - .given( - newKeyNamed(adminKey), - newKeyNamed(narKey).shape(SECP256K1), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(newAutoRenewAccount) - .maxAutomaticTokenAssociations(2) - .key(narKey) - .exposingCreatedIdTo(id -> newAutoRenewAliasAddr.set(idAsHeadlongAddress(id))), - cryptoCreate(CIVILIAN).balance(10L * ONE_HUNDRED_HBARS), - uploadInitCode(MINIMAL_CREATIONS_CONTRACT), - contractCreate(MINIMAL_CREATIONS_CONTRACT).gas(GAS_TO_OFFER), - tokenCreate(ft) - .autoRenewAccount(TOKEN_TREASURY) - .autoRenewPeriod(THREE_MONTHS_IN_SECONDS - 3600L) - .adminKey(CIVILIAN) - .treasury(TOKEN_TREASURY) - .exposingCreatedIdTo(idLit -> tokenMirrorAddr.set(idAsHeadlongAddress(asToken(idLit))))) - .when(sourcing(() -> contractCall( - MINIMAL_CREATIONS_CONTRACT, - "updateTokenWithNewAutoRenewInfo", - tokenMirrorAddr.get(), - newAutoRenewAliasAddr.get(), - THREE_MONTHS_IN_SECONDS + 3600) - .via(updateTxn) - .gas(10L * GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .payingWith(CIVILIAN) - .refusingEthConversion() - .hasKnownStatus(CONTRACT_REVERT_EXECUTED))) - .then( - childRecordsCheck( - updateTxn, - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith() - .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)), - // Auto-renew account is unchanged - getTokenInfo(ft).hasAutoRenewAccount(TOKEN_TREASURY)); - } - - private static HapiCryptoCreate cryptoCreateWithExposingId( - String accountName, String keyName, AtomicReference
addressReference) { - return cryptoCreate(accountName) - .key(keyName) - .exposingCreatedIdTo(id -> addressReference.set(idAsHeadlongAddress(id))); - } - @Override protected Logger getResultsLogger() { return log; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/SigningReqsV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/SigningReqsV1SecurityModelSuite.java new file mode 100644 index 000000000000..3624995f718d --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/SigningReqsV1SecurityModelSuite.java @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2022-2023 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.spec.HapiPropertySource.asToken; +import static com.hedera.services.bdd.spec.HapiPropertySource.asTokenString; +import static com.hedera.services.bdd.spec.HapiPropertySource.idAsHeadlongAddress; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +import static com.hedera.services.bdd.spec.keys.KeyShape.*; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.*; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.*; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.*; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.file.FileUpdateSuite.*; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.esaulpaugh.headlong.abi.Address; +import com.hedera.services.bdd.spec.*; +import com.hedera.services.bdd.spec.assertions.*; +import com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoCreate; +import com.hedera.services.bdd.suites.*; +import com.hederahashgraph.api.proto.java.TokenID; +import java.util.*; +import java.util.concurrent.atomic.*; +import org.apache.logging.log4j.*; + +// Some of the test cases cannot be converted to use eth calls, +// since they use admin keys, which are held by the txn payer. +// In the case of an eth txn, we revoke the payers keys and the txn would fail. +// The only way an eth account to create a token is the admin key to be of a contractId type. +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse +public class SigningReqsV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(SigningReqsV1SecurityModelSuite.class); + + private static final String FIRST_CREATE_TXN = "firstCreateTxn"; + private static final String SECOND_CREATE_TXN = "secondCreateTxn"; + private static final long DEFAULT_AMOUNT_TO_SEND = 20 * ONE_HBAR; + private static final String MINIMAL_CREATIONS_CONTRACT = "MinimalTokenCreations"; + + public static final String AUTO_RENEW = "autoRenew"; + public static final String AR_KEY = "arKey"; + public static final int GAS_TO_OFFER = 1_000_000; + + public static void main(String... args) { + new SigningReqsV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + public List getSpecsInSuite() { + return List.of( + newAutoRenewAccountMustSignUpdate(), + newTreasuryAccountMustSignUpdate(), + autoRenewAccountMustSignCreation(), + fractionalFeeCollectorMustSign(), + selfDenominatedFixedCollectorMustSign()); + } + + @SuppressWarnings("java:S5960") // "assertions should not be used in production code" - not production + private HapiSpec selfDenominatedFixedCollectorMustSign() { + final var fcKey = "fcKey"; + final var arKey = AR_KEY; + final var feeCollector = "feeCollector"; + final var autoRenew = AUTO_RENEW; + final AtomicLong contractId = new AtomicLong(); + final AtomicReference
autoRenewAlias = new AtomicReference<>(); + final AtomicReference
feeCollectorAlias = new AtomicReference<>(); + final AtomicReference createdToken = new AtomicReference<>(); + + return propertyPreservingHapiSpec("SelfDenominatedFixedCollectorMustSign") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenCreate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(arKey).shape(SECP256K1), + newKeyNamed(fcKey).shape(SECP256K1), + cryptoCreate(CIVILIAN).balance(10L * ONE_HUNDRED_HBARS), + cryptoCreateWithExposingId(autoRenew, arKey, autoRenewAlias), + cryptoCreateWithExposingId(feeCollector, fcKey, feeCollectorAlias), + uploadInitCode(MINIMAL_CREATIONS_CONTRACT), + contractCreate(MINIMAL_CREATIONS_CONTRACT) + .gas(GAS_TO_OFFER) + .exposingNumTo(contractId::set)) + .when( + sourcing(() -> contractCall( + MINIMAL_CREATIONS_CONTRACT, + "makeRenewableTokenWithSelfDenominatedFixedFee", + autoRenewAlias.get(), + THREE_MONTHS_IN_SECONDS, + feeCollectorAlias.get()) + .via(FIRST_CREATE_TXN) + .gas(10L * GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(CIVILIAN) + .alsoSigningWithFullPrefix(autoRenew) + .refusingEthConversion() + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)), + sourcing(() -> contractCall( + MINIMAL_CREATIONS_CONTRACT, + "makeRenewableTokenWithSelfDenominatedFixedFee", + autoRenewAlias.get(), + THREE_MONTHS_IN_SECONDS, + feeCollectorAlias.get()) + .via(FIRST_CREATE_TXN) + .gas(10L * GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(CIVILIAN) + .alsoSigningWithFullPrefix(autoRenew, feeCollector) + .refusingEthConversion())) + .then( + getTxnRecord(FIRST_CREATE_TXN) + .andAllChildRecords() + .exposingTokenCreationsTo(creations -> createdToken.set(creations.get(0))), + sourcing(() -> getTokenInfo(asTokenString(createdToken.get())) + .hasAutoRenewAccount(autoRenew) + .logged() + .hasCustom((spec, fees) -> { + assertEquals(1, fees.size()); + final var fee = fees.get(0); + assertTrue(fee.hasFixedFee()); + assertEquals( + createdToken.get(), + fee.getFixedFee().getDenominatingTokenId()); + assertEquals( + spec.registry().getAccountID(feeCollector), fee.getFeeCollectorAccountId()); + }))); + } + + @SuppressWarnings("java:S5960") // "assertions should not be used in production code" - not production + private HapiSpec fractionalFeeCollectorMustSign() { + final var fcKey = "fcKey"; + final var arKey = AR_KEY; + final var feeCollector = "feeCollector"; + final var autoRenew = AUTO_RENEW; + final AtomicLong contractId = new AtomicLong(); + final AtomicReference
autoRenewAlias = new AtomicReference<>(); + final AtomicReference
feeCollectorAlias = new AtomicReference<>(); + final AtomicReference createdToken = new AtomicReference<>(); + + return propertyPreservingHapiSpec("FractionalFeeCollectorMustSign") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenCreate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(arKey).shape(SECP256K1), + newKeyNamed(fcKey).shape(SECP256K1), + cryptoCreate(CIVILIAN).balance(10L * ONE_HUNDRED_HBARS), + cryptoCreateWithExposingId(autoRenew, arKey, autoRenewAlias), + cryptoCreateWithExposingId(feeCollector, fcKey, feeCollectorAlias), + uploadInitCode(MINIMAL_CREATIONS_CONTRACT), + contractCreate(MINIMAL_CREATIONS_CONTRACT) + .gas(GAS_TO_OFFER) + .exposingNumTo(contractId::set)) + .when( + sourcing(() -> contractCall( + MINIMAL_CREATIONS_CONTRACT, + "makeRenewableTokenWithFractionalFee", + autoRenewAlias.get(), + THREE_MONTHS_IN_SECONDS, + feeCollectorAlias.get()) + .via(FIRST_CREATE_TXN) + .gas(10L * GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(CIVILIAN) + .alsoSigningWithFullPrefix(autoRenew) + .refusingEthConversion() + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)), + sourcing(() -> contractCall( + MINIMAL_CREATIONS_CONTRACT, + "makeRenewableTokenWithFractionalFee", + autoRenewAlias.get(), + THREE_MONTHS_IN_SECONDS, + feeCollectorAlias.get()) + .via(FIRST_CREATE_TXN) + .gas(10L * GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(CIVILIAN) + .alsoSigningWithFullPrefix(autoRenew, feeCollector) + .refusingEthConversion())) + .then( + getTxnRecord(FIRST_CREATE_TXN) + .andAllChildRecords() + .exposingTokenCreationsTo(creations -> createdToken.set(creations.get(0))), + sourcing(() -> getTokenInfo(asTokenString(createdToken.get())) + .hasAutoRenewAccount(autoRenew) + .logged() + .hasCustom((spec, fees) -> { + assertEquals(1, fees.size()); + final var fee = fees.get(0); + assertTrue(fee.hasFractionalFee()); + assertEquals( + spec.registry().getAccountID(feeCollector), fee.getFeeCollectorAccountId()); + }))); + } + + private HapiSpec autoRenewAccountMustSignCreation() { + final var arKey = AR_KEY; + final var autoRenew = AUTO_RENEW; + final AtomicReference
autoRenewAlias = new AtomicReference<>(); + final AtomicLong contractId = new AtomicLong(); + final AtomicReference createdToken = new AtomicReference<>(); + + return propertyPreservingHapiSpec("AutoRenewAccountMustSignCreation") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenCreate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(arKey).shape(SECP256K1), + cryptoCreate(CIVILIAN).balance(10L * ONE_HUNDRED_HBARS), + cryptoCreateWithExposingId(autoRenew, arKey, autoRenewAlias), + uploadInitCode(MINIMAL_CREATIONS_CONTRACT), + contractCreate(MINIMAL_CREATIONS_CONTRACT) + .exposingNumTo(contractId::set) + .gas(GAS_TO_OFFER)) + .when( + // Fails without the auto-renew account's full-prefix signature + sourcing(() -> contractCall( + MINIMAL_CREATIONS_CONTRACT, + "makeRenewableToken", + autoRenewAlias.get(), + THREE_MONTHS_IN_SECONDS) + .via(FIRST_CREATE_TXN) + .gas(10L * GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(CIVILIAN) + .refusingEthConversion() + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)), + // Succeeds with the full-prefix signature + sourcing(() -> contractCall( + MINIMAL_CREATIONS_CONTRACT, + "makeRenewableToken", + autoRenewAlias.get(), + THREE_MONTHS_IN_SECONDS) + .via(SECOND_CREATE_TXN) + .gas(10L * GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(CIVILIAN) + .alsoSigningWithFullPrefix(arKey) + .refusingEthConversion())) + .then( + getTxnRecord(SECOND_CREATE_TXN) + .andAllChildRecords() + .exposingTokenCreationsTo(creations -> createdToken.set(creations.get(0))), + childRecordsCheck( + FIRST_CREATE_TXN, + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith() + .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)), + sourcing(() -> + getTokenInfo(asTokenString(createdToken.get())).hasAutoRenewAccount(autoRenew))); + } + + private HapiSpec newTreasuryAccountMustSignUpdate() { + final var ft = "fungibleToken"; + final var ntKey = "ntKey"; + final var updateTxn = "updateTxn"; + final var newTreasury = "newTreasury"; + final AtomicReference
tokenMirrorAddr = new AtomicReference<>(); + final AtomicReference
newTreasuryAliasAddr = new AtomicReference<>(); + + return propertyPreservingHapiSpec("NewTreasuryAccountMustSignUpdate") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenCreate,TokenUpdate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ntKey).shape(SECP256K1), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(newTreasury) + // The new treasury must either already be associated or + // have open auto-association slots; it's therefore a bit + // odd that we require it to also sign, but this is the + // HAPI behavior, so we should be consistent for now + .maxAutomaticTokenAssociations(1) + .key(ntKey) + .exposingCreatedIdTo(id -> newTreasuryAliasAddr.set(idAsHeadlongAddress(id))), + cryptoCreate(CIVILIAN).balance(10L * ONE_HUNDRED_HBARS), + uploadInitCode(MINIMAL_CREATIONS_CONTRACT), + contractCreate(MINIMAL_CREATIONS_CONTRACT).gas(GAS_TO_OFFER), + tokenCreate(ft) + .adminKey(CIVILIAN) + .treasury(TOKEN_TREASURY) + .exposingCreatedIdTo(idLit -> tokenMirrorAddr.set(idAsHeadlongAddress(asToken(idLit))))) + .when(sourcing(() -> contractCall( + MINIMAL_CREATIONS_CONTRACT, + "updateTokenWithNewTreasury", + tokenMirrorAddr.get(), + newTreasuryAliasAddr.get()) + .via(updateTxn) + .gas(10L * GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(CIVILIAN) + .refusingEthConversion() + .hasKnownStatus(CONTRACT_REVERT_EXECUTED))) + .then( + childRecordsCheck( + updateTxn, + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith() + .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)), + // Treasury account is unchanged + getTokenInfo(ft).hasTreasury(TOKEN_TREASURY)); + } + + private HapiSpec newAutoRenewAccountMustSignUpdate() { + final var ft = "fungibleToken"; + final var narKey = "narKey"; + final var adminKey = "adminKey"; + final var updateTxn = "updateTxn"; + final var newAutoRenewAccount = "newAutoRenewAccount"; + final AtomicReference
tokenMirrorAddr = new AtomicReference<>(); + final AtomicReference
newAutoRenewAliasAddr = new AtomicReference<>(); + + return propertyPreservingHapiSpec("newAutoRenewAccountMustSignUpdate") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenCreate,TokenUpdate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(adminKey), + newKeyNamed(narKey).shape(SECP256K1), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(newAutoRenewAccount) + .maxAutomaticTokenAssociations(2) + .key(narKey) + .exposingCreatedIdTo(id -> newAutoRenewAliasAddr.set(idAsHeadlongAddress(id))), + cryptoCreate(CIVILIAN).balance(10L * ONE_HUNDRED_HBARS), + uploadInitCode(MINIMAL_CREATIONS_CONTRACT), + contractCreate(MINIMAL_CREATIONS_CONTRACT).gas(GAS_TO_OFFER), + tokenCreate(ft) + .autoRenewAccount(TOKEN_TREASURY) + .autoRenewPeriod(THREE_MONTHS_IN_SECONDS - 3600L) + .adminKey(CIVILIAN) + .treasury(TOKEN_TREASURY) + .exposingCreatedIdTo(idLit -> tokenMirrorAddr.set(idAsHeadlongAddress(asToken(idLit))))) + .when(sourcing(() -> contractCall( + MINIMAL_CREATIONS_CONTRACT, + "updateTokenWithNewAutoRenewInfo", + tokenMirrorAddr.get(), + newAutoRenewAliasAddr.get(), + THREE_MONTHS_IN_SECONDS + 3600) + .via(updateTxn) + .gas(10L * GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(CIVILIAN) + .refusingEthConversion() + .hasKnownStatus(CONTRACT_REVERT_EXECUTED))) + .then( + childRecordsCheck( + updateTxn, + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith() + .status(INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE)), + // Auto-renew account is unchanged + getTokenInfo(ft).hasAutoRenewAccount(TOKEN_TREASURY)); + } + + private static HapiCryptoCreate cryptoCreateWithExposingId( + String accountName, String keyName, AtomicReference
addressReference) { + return cryptoCreate(accountName) + .key(keyName) + .exposingCreatedIdTo(id -> addressReference.set(idAsHeadlongAddress(id))); + } + + @Override + protected Logger getResultsLogger() { + return log; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenExpiryInfoSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenExpiryInfoSuite.java index 9dba6cfa9f41..946b25b69acf 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenExpiryInfoSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenExpiryInfoSuite.java @@ -26,33 +26,23 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; 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.contract.HapiParserUtil.asHeadlongAddress; import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; 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.asAddress; import static com.hedera.services.bdd.suites.contract.Utils.asToken; -import static com.hedera.services.bdd.suites.contract.precompile.WipeTokenAccountPrecompileSuite.GAS_TO_OFFER; import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_EXPIRATION_TIME; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_RENEWAL_PERIOD; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; -import static org.junit.jupiter.api.Assertions.assertEquals; import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; import com.hedera.services.bdd.spec.HapiSpec; -import com.hedera.services.bdd.spec.HapiSpecSetup; -import com.hedera.services.bdd.spec.keys.SigControl; import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; import com.hedera.services.bdd.suites.HapiSuite; import com.hedera.services.bdd.suites.utils.contracts.precompile.TokenKeyType; -import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TokenSupplyType; import java.util.List; @@ -65,16 +55,9 @@ public class TokenExpiryInfoSuite extends HapiSuite { private static final Logger log = LogManager.getLogger(TokenExpiryInfoSuite.class); private static final String TOKEN_EXPIRY_CONTRACT = "TokenExpiryContract"; private static final String AUTO_RENEW_ACCOUNT = "autoRenewAccount"; - private static final String UPDATED_AUTO_RENEW_ACCOUNT = "updatedAutoRenewAccount"; - private static final String INVALID_ADDRESS = "0x0000000000000000000000000000000000123456"; - private static final long DEFAULT_MAX_LIFETIME = - Long.parseLong(HapiSpecSetup.getDefaultNodeProps().get("entities.maxLifetime")); - public static final long MONTH_IN_SECONDS = 7_000_000L; private static final String ADMIN_KEY = TokenKeyType.ADMIN_KEY.name(); - public static final String UPDATE_EXPIRY_INFO_FOR_TOKEN = "updateExpiryInfoForToken"; public static final String GET_EXPIRY_INFO_FOR_TOKEN = "getExpiryInfoForToken"; - public static final String UPDATE_EXPIRY_INFO_FOR_TOKEN_AND_READ_LATEST_INFO = - "updateExpiryInfoForTokenAndReadLatestInfo"; + public static final int GAS_TO_OFFER = 1_000_000; public static void main(String... args) { new TokenExpiryInfoSuite().runSuiteAsync(); @@ -92,8 +75,7 @@ protected Logger getResultsLogger() { @Override public List getSpecsInSuite() { - return List.of( - getExpiryInfoForToken(), updateExpiryInfoForToken(), updateExpiryInfoForTokenAndReadLatestInfo()); + return List.of(getExpiryInfoForToken()); } private HapiSpec getExpiryInfoForToken() { @@ -172,207 +154,4 @@ private HapiSpec getExpiryInfoForToken() { THREE_MONTHS_IN_SECONDS))))); })); } - - @SuppressWarnings("java:S5960") - private HapiSpec updateExpiryInfoForToken() { - - final AtomicReference vanillaTokenID = new AtomicReference<>(); - final AtomicReference updatedAutoRenewAccountID = new AtomicReference<>(); - - return defaultHapiSpec("UpdateExpiryInfoForToken") - .given( - cryptoCreate(TOKEN_TREASURY).balance(0L), - cryptoCreate(AUTO_RENEW_ACCOUNT).balance(0L), - cryptoCreate(UPDATED_AUTO_RENEW_ACCOUNT) - .balance(0L) - .keyShape(SigControl.ED25519_ON) - .exposingCreatedIdTo(updatedAutoRenewAccountID::set), - newKeyNamed(ADMIN_KEY), - uploadInitCode(TOKEN_EXPIRY_CONTRACT), - contractCreate(TOKEN_EXPIRY_CONTRACT).gas(1_000_000L), - tokenCreate(VANILLA_TOKEN) - .supplyType(TokenSupplyType.FINITE) - .treasury(TOKEN_TREASURY) - .expiry(100) - .autoRenewAccount(AUTO_RENEW_ACCOUNT) - .autoRenewPeriod(THREE_MONTHS_IN_SECONDS) - .maxSupply(1000) - .initialSupply(500L) - .adminKey(ADMIN_KEY) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id)))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_EXPIRY_CONTRACT, - UPDATE_EXPIRY_INFO_FOR_TOKEN, - asHeadlongAddress(INVALID_ADDRESS), - DEFAULT_MAX_LIFETIME - 12_345L, - HapiParserUtil.asHeadlongAddress(asAddress(updatedAutoRenewAccountID.get())), - MONTH_IN_SECONDS) - .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_AUTO_RENEW_ACCOUNT) - .via("invalidTokenTxn") - .gas(GAS_TO_OFFER) - .payingWith(GENESIS) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_EXPIRY_CONTRACT, - UPDATE_EXPIRY_INFO_FOR_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - DEFAULT_MAX_LIFETIME - 12_345L, - HapiParserUtil.asHeadlongAddress(asAddress(updatedAutoRenewAccountID.get())), - MONTH_IN_SECONDS) - .via("invalidSignatureTxn") - .gas(GAS_TO_OFFER) - .payingWith(GENESIS) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_EXPIRY_CONTRACT, - UPDATE_EXPIRY_INFO_FOR_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - 100L, - HapiParserUtil.asHeadlongAddress(asAddress(updatedAutoRenewAccountID.get())), - MONTH_IN_SECONDS) - .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_AUTO_RENEW_ACCOUNT) - .via("invalidExpiryTxn") - .gas(GAS_TO_OFFER) - .payingWith(GENESIS) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_EXPIRY_CONTRACT, - UPDATE_EXPIRY_INFO_FOR_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - DEFAULT_MAX_LIFETIME - 12_345L, - asHeadlongAddress(INVALID_ADDRESS), - MONTH_IN_SECONDS) - .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_AUTO_RENEW_ACCOUNT) - .via("invalidAutoRenewAccountTxn") - .gas(GAS_TO_OFFER) - .payingWith(GENESIS) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_EXPIRY_CONTRACT, - UPDATE_EXPIRY_INFO_FOR_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - DEFAULT_MAX_LIFETIME - 12_345L, - HapiParserUtil.asHeadlongAddress(asAddress(updatedAutoRenewAccountID.get())), - 1L) - .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_AUTO_RENEW_ACCOUNT) - .via("invalidAutoRenewPeriodTxn") - .gas(GAS_TO_OFFER) - .payingWith(GENESIS) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_EXPIRY_CONTRACT, - UPDATE_EXPIRY_INFO_FOR_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - DEFAULT_MAX_LIFETIME - 12_345L, - HapiParserUtil.asHeadlongAddress(asAddress(updatedAutoRenewAccountID.get())), - MONTH_IN_SECONDS) - .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_AUTO_RENEW_ACCOUNT) - .via("updateExpiryTxn") - .gas(GAS_TO_OFFER) - .payingWith(GENESIS)))) - .then( - childRecordsCheck( - "invalidTokenTxn", - CONTRACT_REVERT_EXECUTED, - recordWith().status(INVALID_TOKEN_ID)), - childRecordsCheck( - "invalidSignatureTxn", - CONTRACT_REVERT_EXECUTED, - recordWith().status(INVALID_SIGNATURE)), - childRecordsCheck( - "invalidExpiryTxn", - CONTRACT_REVERT_EXECUTED, - recordWith().status(INVALID_EXPIRATION_TIME)), - childRecordsCheck( - "invalidAutoRenewAccountTxn", - CONTRACT_REVERT_EXECUTED, - recordWith().status(INVALID_AUTORENEW_ACCOUNT)), - childRecordsCheck( - "invalidAutoRenewPeriodTxn", - CONTRACT_REVERT_EXECUTED, - recordWith().status(INVALID_RENEWAL_PERIOD)), - withOpContext((spec, opLog) -> { - final var getTokenInfoQuery = getTokenInfo(VANILLA_TOKEN); - allRunFor(spec, getTokenInfoQuery); - final var expirySecond = getTokenInfoQuery - .getResponse() - .getTokenGetInfo() - .getTokenInfo() - .getExpiry() - .getSeconds(); - final var autoRenewAccount = getTokenInfoQuery - .getResponse() - .getTokenGetInfo() - .getTokenInfo() - .getAutoRenewAccount(); - final var autoRenewPeriod = getTokenInfoQuery - .getResponse() - .getTokenGetInfo() - .getTokenInfo() - .getAutoRenewPeriod() - .getSeconds(); - assertEquals(expirySecond, DEFAULT_MAX_LIFETIME - 12_345L); - assertEquals(autoRenewAccount, spec.registry().getAccountID(UPDATED_AUTO_RENEW_ACCOUNT)); - assertEquals(autoRenewPeriod, MONTH_IN_SECONDS); - })); - } - - private HapiSpec updateExpiryInfoForTokenAndReadLatestInfo() { - - final AtomicReference vanillaTokenID = new AtomicReference<>(); - final AtomicReference updatedAutoRenewAccountID = new AtomicReference<>(); - - return defaultHapiSpec("UpdateExpiryInfoForTokenAndReadLatestInfo") - .given( - cryptoCreate(TOKEN_TREASURY).balance(0L), - cryptoCreate(AUTO_RENEW_ACCOUNT).balance(0L), - cryptoCreate(UPDATED_AUTO_RENEW_ACCOUNT) - .keyShape(SigControl.ED25519_ON) - .balance(0L) - .exposingCreatedIdTo(updatedAutoRenewAccountID::set), - newKeyNamed(ADMIN_KEY), - uploadInitCode(TOKEN_EXPIRY_CONTRACT), - contractCreate(TOKEN_EXPIRY_CONTRACT).gas(1_000_000L), - tokenCreate(VANILLA_TOKEN) - .supplyType(TokenSupplyType.FINITE) - .treasury(TOKEN_TREASURY) - .expiry(100) - .autoRenewAccount(AUTO_RENEW_ACCOUNT) - .autoRenewPeriod(THREE_MONTHS_IN_SECONDS) - .maxSupply(1000) - .initialSupply(500L) - .adminKey(ADMIN_KEY) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id)))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_EXPIRY_CONTRACT, - UPDATE_EXPIRY_INFO_FOR_TOKEN_AND_READ_LATEST_INFO, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - DEFAULT_MAX_LIFETIME - 12_345L, - HapiParserUtil.asHeadlongAddress(asAddress(updatedAutoRenewAccountID.get())), - MONTH_IN_SECONDS) - .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_AUTO_RENEW_ACCOUNT) - .via("updateExpiryAndReadLatestInfoTxn") - .gas(GAS_TO_OFFER) - .payingWith(GENESIS)))) - .then(withOpContext((spec, opLog) -> allRunFor( - spec, - childRecordsCheck( - "updateExpiryAndReadLatestInfoTxn", - SUCCESS, - recordWith().status(SUCCESS), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_GET_TOKEN_EXPIRY_INFO) - .withStatus(SUCCESS) - .withExpiry( - DEFAULT_MAX_LIFETIME - 12_345L, - updatedAutoRenewAccountID.get(), - MONTH_IN_SECONDS))))))); - } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenExpiryInfoV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenExpiryInfoV1SecurityModelSuite.java new file mode 100644 index 000000000000..8e8b9a6a8134 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenExpiryInfoV1SecurityModelSuite.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2021-2023 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.spec.HapiSpec.propertyPreservingHapiSpec; +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.getTokenInfo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +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.contract.HapiParserUtil.asHeadlongAddress; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.asToken; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_EXPIRATION_TIME; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_RENEWAL_PERIOD; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_TOKEN_ID; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.HapiSpecSetup; +import com.hedera.services.bdd.spec.keys.SigControl; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hedera.services.bdd.suites.utils.contracts.precompile.TokenKeyType; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.TokenID; +import com.hederahashgraph.api.proto.java.TokenSupplyType; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class TokenExpiryInfoV1SecurityModelSuite extends HapiSuite { + + private static final Logger log = LogManager.getLogger(TokenExpiryInfoV1SecurityModelSuite.class); + private static final String TOKEN_EXPIRY_CONTRACT = "TokenExpiryContract"; + private static final String AUTO_RENEW_ACCOUNT = "autoRenewAccount"; + private static final String UPDATED_AUTO_RENEW_ACCOUNT = "updatedAutoRenewAccount"; + private static final String INVALID_ADDRESS = "0x0000000000000000000000000000000000123456"; + private static final long DEFAULT_MAX_LIFETIME = + Long.parseLong(HapiSpecSetup.getDefaultNodeProps().get("entities.maxLifetime")); + public static final long MONTH_IN_SECONDS = 7_000_000L; + private static final String ADMIN_KEY = TokenKeyType.ADMIN_KEY.name(); + public static final String UPDATE_EXPIRY_INFO_FOR_TOKEN = "updateExpiryInfoForToken"; + public static final String UPDATE_EXPIRY_INFO_FOR_TOKEN_AND_READ_LATEST_INFO = + "updateExpiryInfoForTokenAndReadLatestInfo"; + public static final int GAS_TO_OFFER = 1_000_000; + + public static void main(String... args) { + new TokenExpiryInfoV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return true; + } + + @Override + protected Logger getResultsLogger() { + return log; + } + + @Override + public List getSpecsInSuite() { + return List.of(updateExpiryInfoForToken(), updateExpiryInfoForTokenAndReadLatestInfo()); + } + + @SuppressWarnings({"java:S5960", "java:S1192" + }) // using `assertThat` in production code - except this isn't production code + private HapiSpec updateExpiryInfoForToken() { + + final AtomicReference vanillaTokenID = new AtomicReference<>(); + final AtomicReference updatedAutoRenewAccountID = new AtomicReference<>(); + + return propertyPreservingHapiSpec("updateExpiryInfoForToken") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,TokenCreate,TokenUpdate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(TOKEN_TREASURY).balance(0L), + cryptoCreate(AUTO_RENEW_ACCOUNT).balance(0L), + cryptoCreate(UPDATED_AUTO_RENEW_ACCOUNT) + .balance(0L) + .keyShape(SigControl.ED25519_ON) + .exposingCreatedIdTo(updatedAutoRenewAccountID::set), + newKeyNamed(ADMIN_KEY), + uploadInitCode(TOKEN_EXPIRY_CONTRACT), + contractCreate(TOKEN_EXPIRY_CONTRACT).gas(1_000_000L), + tokenCreate(VANILLA_TOKEN) + .supplyType(TokenSupplyType.FINITE) + .treasury(TOKEN_TREASURY) + .expiry(100) + .autoRenewAccount(AUTO_RENEW_ACCOUNT) + .autoRenewPeriod(THREE_MONTHS_IN_SECONDS) + .maxSupply(1000) + .initialSupply(500L) + .adminKey(ADMIN_KEY) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id)))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_EXPIRY_CONTRACT, + UPDATE_EXPIRY_INFO_FOR_TOKEN, + asHeadlongAddress(INVALID_ADDRESS), + DEFAULT_MAX_LIFETIME - 12_345L, + HapiParserUtil.asHeadlongAddress(asAddress(updatedAutoRenewAccountID.get())), + MONTH_IN_SECONDS) + .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_AUTO_RENEW_ACCOUNT) + .via("invalidTokenTxn") + .gas(GAS_TO_OFFER) + .payingWith(GENESIS) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_EXPIRY_CONTRACT, + UPDATE_EXPIRY_INFO_FOR_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + DEFAULT_MAX_LIFETIME - 12_345L, + HapiParserUtil.asHeadlongAddress(asAddress(updatedAutoRenewAccountID.get())), + MONTH_IN_SECONDS) + .via("invalidSignatureTxn") + .gas(GAS_TO_OFFER) + .payingWith(GENESIS) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_EXPIRY_CONTRACT, + UPDATE_EXPIRY_INFO_FOR_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + 100L, + HapiParserUtil.asHeadlongAddress(asAddress(updatedAutoRenewAccountID.get())), + MONTH_IN_SECONDS) + .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_AUTO_RENEW_ACCOUNT) + .via("invalidExpiryTxn") + .gas(GAS_TO_OFFER) + .payingWith(GENESIS) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_EXPIRY_CONTRACT, + UPDATE_EXPIRY_INFO_FOR_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + DEFAULT_MAX_LIFETIME - 12_345L, + asHeadlongAddress(INVALID_ADDRESS), + MONTH_IN_SECONDS) + .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_AUTO_RENEW_ACCOUNT) + .via("invalidAutoRenewAccountTxn") + .gas(GAS_TO_OFFER) + .payingWith(GENESIS) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_EXPIRY_CONTRACT, + UPDATE_EXPIRY_INFO_FOR_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + DEFAULT_MAX_LIFETIME - 12_345L, + HapiParserUtil.asHeadlongAddress(asAddress(updatedAutoRenewAccountID.get())), + 1L) + .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_AUTO_RENEW_ACCOUNT) + .via("invalidAutoRenewPeriodTxn") + .gas(GAS_TO_OFFER) + .payingWith(GENESIS) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_EXPIRY_CONTRACT, + UPDATE_EXPIRY_INFO_FOR_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + DEFAULT_MAX_LIFETIME - 12_345L, + HapiParserUtil.asHeadlongAddress(asAddress(updatedAutoRenewAccountID.get())), + MONTH_IN_SECONDS) + .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_AUTO_RENEW_ACCOUNT) + .via("updateExpiryTxn") + .gas(GAS_TO_OFFER) + .payingWith(GENESIS)))) + .then( + childRecordsCheck( + "invalidTokenTxn", + CONTRACT_REVERT_EXECUTED, + recordWith().status(INVALID_TOKEN_ID)), + childRecordsCheck( + "invalidSignatureTxn", + CONTRACT_REVERT_EXECUTED, + recordWith().status(INVALID_SIGNATURE)), + childRecordsCheck( + "invalidExpiryTxn", + CONTRACT_REVERT_EXECUTED, + recordWith().status(INVALID_EXPIRATION_TIME)), + childRecordsCheck( + "invalidAutoRenewAccountTxn", + CONTRACT_REVERT_EXECUTED, + recordWith().status(INVALID_AUTORENEW_ACCOUNT)), + childRecordsCheck( + "invalidAutoRenewPeriodTxn", + CONTRACT_REVERT_EXECUTED, + recordWith().status(INVALID_RENEWAL_PERIOD)), + withOpContext((spec, opLog) -> { + final var getTokenInfoQuery = getTokenInfo(VANILLA_TOKEN); + allRunFor(spec, getTokenInfoQuery); + final var expirySecond = getTokenInfoQuery + .getResponse() + .getTokenGetInfo() + .getTokenInfo() + .getExpiry() + .getSeconds(); + final var autoRenewAccount = getTokenInfoQuery + .getResponse() + .getTokenGetInfo() + .getTokenInfo() + .getAutoRenewAccount(); + final var autoRenewPeriod = getTokenInfoQuery + .getResponse() + .getTokenGetInfo() + .getTokenInfo() + .getAutoRenewPeriod() + .getSeconds(); + assertEquals(expirySecond, DEFAULT_MAX_LIFETIME - 12_345L); + assertEquals(autoRenewAccount, spec.registry().getAccountID(UPDATED_AUTO_RENEW_ACCOUNT)); + assertEquals(autoRenewPeriod, MONTH_IN_SECONDS); + })); + } + + @SuppressWarnings("java:S1192") // "use already defined const instead of copying its value here" - not this time + private HapiSpec updateExpiryInfoForTokenAndReadLatestInfo() { + + final AtomicReference vanillaTokenID = new AtomicReference<>(); + final AtomicReference updatedAutoRenewAccountID = new AtomicReference<>(); + + return propertyPreservingHapiSpec("updateExpiryInfoForTokenAndReadLatestInfo") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,TokenCreate,TokenUpdate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(TOKEN_TREASURY).balance(0L), + cryptoCreate(AUTO_RENEW_ACCOUNT).balance(0L), + cryptoCreate(UPDATED_AUTO_RENEW_ACCOUNT) + .keyShape(SigControl.ED25519_ON) + .balance(0L) + .exposingCreatedIdTo(updatedAutoRenewAccountID::set), + newKeyNamed(ADMIN_KEY), + uploadInitCode(TOKEN_EXPIRY_CONTRACT), + contractCreate(TOKEN_EXPIRY_CONTRACT).gas(1_000_000L), + tokenCreate(VANILLA_TOKEN) + .supplyType(TokenSupplyType.FINITE) + .treasury(TOKEN_TREASURY) + .expiry(100) + .autoRenewAccount(AUTO_RENEW_ACCOUNT) + .autoRenewPeriod(THREE_MONTHS_IN_SECONDS) + .maxSupply(1000) + .initialSupply(500L) + .adminKey(ADMIN_KEY) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id)))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_EXPIRY_CONTRACT, + UPDATE_EXPIRY_INFO_FOR_TOKEN_AND_READ_LATEST_INFO, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + DEFAULT_MAX_LIFETIME - 12_345L, + HapiParserUtil.asHeadlongAddress(asAddress(updatedAutoRenewAccountID.get())), + MONTH_IN_SECONDS) + .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_AUTO_RENEW_ACCOUNT) + .via("updateExpiryAndReadLatestInfoTxn") + .gas(GAS_TO_OFFER) + .payingWith(GENESIS)))) + .then(withOpContext((spec, opLog) -> allRunFor( + spec, + childRecordsCheck( + "updateExpiryAndReadLatestInfoTxn", + SUCCESS, + recordWith().status(SUCCESS), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_GET_TOKEN_EXPIRY_INFO) + .withStatus(SUCCESS) + .withExpiry( + DEFAULT_MAX_LIFETIME - 12_345L, + updatedAutoRenewAccountID.get(), + MONTH_IN_SECONDS))))))); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSSuite.java index 15bf99dc0dc7..4f2a4759987c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSSuite.java @@ -19,8 +19,6 @@ import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; 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.keys.KeyShape.CONTRACT; -import static com.hedera.services.bdd.spec.keys.SigControl.ED25519_ON; import static com.hedera.services.bdd.spec.queries.QueryVerbs.contractCallLocal; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenNftInfo; @@ -47,7 +45,6 @@ import static com.hedera.services.bdd.suites.contract.Utils.asAddress; import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; -import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; import com.google.protobuf.ByteString; import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; @@ -96,41 +93,26 @@ public class TokenInfoHTSSuite extends HapiSuite { private static final String AUTO_RENEW_ACCOUNT = "autoRenewAccount"; private static final String FEE_DENOM = "denom"; public static final String HTS_COLLECTOR = "denomFee"; - private static final String ACCOUNT = "Account"; private static final String CREATE_TXN = "CreateTxn"; private static final String TOKEN_INFO_TXN = "TokenInfoTxn"; private static final String FUNGIBLE_TOKEN_INFO_TXN = "FungibleTokenInfoTxn"; - private static final String UPDATE_ANG_GET_TOKEN_INFO_TXN = "UpdateAndGetTokenInfoTxn"; - private static final String UPDATE_ANG_GET_FUNGIBLE_TOKEN_INFO_TXN = "UpdateAndGetFungibleTokenInfoTxn"; - private static final String UPDATE_ANG_GET_NON_FUNGIBLE_TOKEN_INFO_TXN = "UpdateAndGetNonFungibleTokenInfoTxn"; private static final String NON_FUNGIBLE_TOKEN_INFO_TXN = "NonFungibleTokenInfoTxn"; private static final String GET_TOKEN_INFO_TXN = "GetTokenInfo"; private static final String APPROVE_TXN = "approveTxn"; - private static final String UPDATE_AND_GET_TOKEN_KEYS_INFO_TXN = "updateTokenKeysAndReadLatestInformation"; private static final String SYMBOL = "T"; private static final String FUNGIBLE_SYMBOL = "FT"; private static final String FUNGIBLE_TOKEN_NAME = "FungibleToken"; private static final String NON_FUNGIBLE_SYMBOL = "NFT"; private static final String META = "First"; private static final String MEMO = "JUMP"; - private static final String UPDATE_NAME = "NewName"; - private static final String UPDATE_SYMBOL = "NewSymbol"; - private static final String UPDATE_MEMO = "NewMemo"; private static final String PRIMARY_TOKEN_NAME = "primary"; private static final String NFT_OWNER = "NFT Owner"; private static final String NFT_SPENDER = "NFT Spender"; private static final String NON_FUNGIBLE_TOKEN_NAME = "NonFungibleToken"; - private static final String MULTI_KEY = "multiKey"; private static final String GET_INFORMATION_FOR_TOKEN = "getInformationForToken"; private static final String GET_INFORMATION_FOR_FUNGIBLE_TOKEN = "getInformationForFungibleToken"; private static final String GET_INFORMATION_FOR_NON_FUNGIBLE_TOKEN = "getInformationForNonFungibleToken"; - private static final String UPDATE_INFORMATION_FOR_TOKEN_AND_GET_LATEST_INFORMATION = - "updateInformationForTokenAndGetLatestInformation"; - private static final String UPDATE_INFORMATION_FOR_FUNGIBLE_TOKEN_AND_GET_LATEST_INFORMATION = - "updateInformationForFungibleTokenAndGetLatestInformation"; - private static final String UPDATE_INFORMATION_FOR_NON_FUNGIBLE_TOKEN_AND_GET_LATEST_INFORMATION = - "updateInformationForNonFungibleTokenAndGetLatestInformation"; private static final int NUMERATOR = 1; private static final int DENOMINATOR = 2; private static final int MINIMUM_TO_COLLECT = 5; @@ -163,14 +145,10 @@ List negativeSpecs() { List positiveSpecs() { return List.of( happyPathGetTokenInfo(), - happyPathUpdateTokenInfoAndGetLatestInfo(), happyPathGetFungibleTokenInfo(), - happyPathUpdateFungibleTokenInfoAndGetLatestInfo(), happyPathGetNonFungibleTokenInfo(), - happyPathUpdateNonFungibleTokenInfoAndGetLatestInfo(), happyPathGetTokenCustomFees(), - happyPathGetNonFungibleTokenCustomFees(), - happyPathUpdateTokenKeysAndReadLatestInformation()); + happyPathGetNonFungibleTokenCustomFees()); } private HapiSpec happyPathGetTokenInfo() { @@ -264,109 +242,6 @@ private HapiSpec happyPathGetTokenInfo() { })); } - private HapiSpec happyPathUpdateTokenInfoAndGetLatestInfo() { - final int decimals = 1; - return defaultHapiSpec("HappyPathUpdateTokenInfoAndGetLatestInfo") - .given( - cryptoCreate(TOKEN_TREASURY).balance(0L), - cryptoCreate(UPDATED_TREASURY) - .keyShape(ED25519_ON) - .balance(0L) - .maxAutomaticTokenAssociations(3), - cryptoCreate(AUTO_RENEW_ACCOUNT).balance(0L), - cryptoCreate(HTS_COLLECTOR), - cryptoCreate(ACCOUNT), - newKeyNamed(ADMIN_KEY), - newKeyNamed(FREEZE_KEY), - newKeyNamed(KYC_KEY), - newKeyNamed(SUPPLY_KEY), - newKeyNamed(WIPE_KEY), - newKeyNamed(FEE_SCHEDULE_KEY), - newKeyNamed(PAUSE_KEY), - uploadInitCode(TOKEN_INFO_CONTRACT), - contractCreate(TOKEN_INFO_CONTRACT).gas(1_000_000L), - tokenCreate(FUNGIBLE_TOKEN_NAME) - .supplyType(TokenSupplyType.FINITE) - .entityMemo(MEMO) - .name(FUNGIBLE_TOKEN_NAME) - .symbol(FUNGIBLE_SYMBOL) - .treasury(TOKEN_TREASURY) - .autoRenewAccount(AUTO_RENEW_ACCOUNT) - .autoRenewPeriod(THREE_MONTHS_IN_SECONDS) - .maxSupply(MAX_SUPPLY) - .initialSupply(500) - .decimals(decimals) - .adminKey(ADMIN_KEY) - .freezeKey(FREEZE_KEY) - .kycKey(KYC_KEY) - .supplyKey(SUPPLY_KEY) - .wipeKey(WIPE_KEY) - .feeScheduleKey(FEE_SCHEDULE_KEY) - .pauseKey(PAUSE_KEY) - .withCustom(fixedHbarFee(500L, HTS_COLLECTOR)) - // Include a fractional fee with no minimum to collect - .withCustom(fractionalFee( - NUMERATOR, DENOMINATOR * 2L, 0, OptionalLong.empty(), TOKEN_TREASURY)) - .withCustom(fractionalFee( - NUMERATOR, - DENOMINATOR, - MINIMUM_TO_COLLECT, - OptionalLong.of(MAXIMUM_TO_COLLECT), - TOKEN_TREASURY)) - .via(CREATE_TXN), - tokenAssociate(ACCOUNT, FUNGIBLE_TOKEN_NAME)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_INFO_CONTRACT, - UPDATE_INFORMATION_FOR_TOKEN_AND_GET_LATEST_INFORMATION, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(FUNGIBLE_TOKEN_NAME))), - UPDATE_NAME, - UPDATE_SYMBOL, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(UPDATED_TREASURY))), - UPDATE_MEMO) - .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_TREASURY) - .payingWith(ACCOUNT) - .via(UPDATE_ANG_GET_TOKEN_INFO_TXN) - .gas(1_000_000L)))) - .then(withOpContext((spec, opLog) -> { - final var getTokenInfoQuery = getTokenInfo(FUNGIBLE_TOKEN_NAME); - allRunFor(spec, getTokenInfoQuery); - final var expirySecond = getTokenInfoQuery - .getResponse() - .getTokenGetInfo() - .getTokenInfo() - .getExpiry() - .getSeconds(); - allRunFor( - spec, - getTxnRecord(UPDATE_ANG_GET_TOKEN_INFO_TXN) - .andAllChildRecords() - .logged(), - childRecordsCheck( - UPDATE_ANG_GET_TOKEN_INFO_TXN, - SUCCESS, - recordWith().status(SUCCESS), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_GET_TOKEN_INFO) - .withStatus(SUCCESS) - .withDecimals(decimals) - .withTokenInfo(getTokenInfoStructForFungibleToken( - spec, - UPDATE_NAME, - UPDATE_SYMBOL, - UPDATE_MEMO, - spec.registry() - .getAccountID(UPDATED_TREASURY), - expirySecond)))))); - })); - } - private HapiSpec happyPathGetFungibleTokenInfo() { final int decimals = 1; return defaultHapiSpec("HappyPathGetFungibleTokenInfo") @@ -459,106 +334,6 @@ private HapiSpec happyPathGetFungibleTokenInfo() { })); } - private HapiSpec happyPathUpdateFungibleTokenInfoAndGetLatestInfo() { - final int decimals = 1; - return defaultHapiSpec("HappyPathUpdateFungibleTokenInfoAndGetLatestInfo") - .given( - cryptoCreate(TOKEN_TREASURY).balance(0L), - cryptoCreate(UPDATED_TREASURY).balance(0L).maxAutomaticTokenAssociations(3), - cryptoCreate(AUTO_RENEW_ACCOUNT).balance(0L), - cryptoCreate(HTS_COLLECTOR), - cryptoCreate(ACCOUNT), - newKeyNamed(ADMIN_KEY), - newKeyNamed(FREEZE_KEY), - newKeyNamed(KYC_KEY), - newKeyNamed(SUPPLY_KEY), - newKeyNamed(WIPE_KEY), - newKeyNamed(FEE_SCHEDULE_KEY), - newKeyNamed(PAUSE_KEY), - uploadInitCode(TOKEN_INFO_CONTRACT), - contractCreate(TOKEN_INFO_CONTRACT).gas(1_000_000L), - tokenCreate(FUNGIBLE_TOKEN_NAME) - .supplyType(TokenSupplyType.FINITE) - .entityMemo(MEMO) - .name(FUNGIBLE_TOKEN_NAME) - .symbol(FUNGIBLE_SYMBOL) - .treasury(TOKEN_TREASURY) - .autoRenewAccount(AUTO_RENEW_ACCOUNT) - .autoRenewPeriod(THREE_MONTHS_IN_SECONDS) - .maxSupply(MAX_SUPPLY) - .initialSupply(500) - .decimals(decimals) - .adminKey(ADMIN_KEY) - .freezeKey(FREEZE_KEY) - .kycKey(KYC_KEY) - .supplyKey(SUPPLY_KEY) - .wipeKey(WIPE_KEY) - .feeScheduleKey(FEE_SCHEDULE_KEY) - .pauseKey(PAUSE_KEY) - .withCustom(fixedHbarFee(500L, HTS_COLLECTOR)) - // Include a fractional fee with no minimum to collect - .withCustom(fractionalFee( - NUMERATOR, DENOMINATOR * 2L, 0, OptionalLong.empty(), TOKEN_TREASURY)) - .withCustom(fractionalFee( - NUMERATOR, - DENOMINATOR, - MINIMUM_TO_COLLECT, - OptionalLong.of(MAXIMUM_TO_COLLECT), - TOKEN_TREASURY)) - .via(CREATE_TXN), - tokenAssociate(ACCOUNT, FUNGIBLE_TOKEN_NAME)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_INFO_CONTRACT, - UPDATE_INFORMATION_FOR_FUNGIBLE_TOKEN_AND_GET_LATEST_INFORMATION, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(FUNGIBLE_TOKEN_NAME))), - UPDATE_NAME, - UPDATE_SYMBOL, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(UPDATED_TREASURY))), - UPDATE_MEMO) - .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_TREASURY) - .payingWith(ACCOUNT) - .via(UPDATE_ANG_GET_FUNGIBLE_TOKEN_INFO_TXN) - .gas(1_000_000L)))) - .then(withOpContext((spec, opLog) -> { - final var getTokenInfoQuery = getTokenInfo(FUNGIBLE_TOKEN_NAME); - allRunFor(spec, getTokenInfoQuery); - final var expirySecond = getTokenInfoQuery - .getResponse() - .getTokenGetInfo() - .getTokenInfo() - .getExpiry() - .getSeconds(); - allRunFor( - spec, - getTxnRecord(UPDATE_ANG_GET_FUNGIBLE_TOKEN_INFO_TXN) - .andAllChildRecords() - .logged(), - childRecordsCheck( - UPDATE_ANG_GET_FUNGIBLE_TOKEN_INFO_TXN, - SUCCESS, - recordWith().status(SUCCESS), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_GET_FUNGIBLE_TOKEN_INFO) - .withStatus(SUCCESS) - .withDecimals(decimals) - .withTokenInfo(getTokenInfoStructForFungibleToken( - spec, - UPDATE_NAME, - UPDATE_SYMBOL, - UPDATE_MEMO, - spec.registry() - .getAccountID(UPDATED_TREASURY), - expirySecond)))))); - })); - } - private HapiSpec happyPathGetNonFungibleTokenInfo() { final int maxSupply = 10; final ByteString meta = ByteString.copyFrom(META.getBytes(StandardCharsets.UTF_8)); @@ -667,119 +442,6 @@ private HapiSpec happyPathGetNonFungibleTokenInfo() { })); } - private HapiSpec happyPathUpdateNonFungibleTokenInfoAndGetLatestInfo() { - final int maxSupply = 10; - final ByteString meta = ByteString.copyFrom(META.getBytes(StandardCharsets.UTF_8)); - return defaultHapiSpec("HappyPathUpdateNonFungibleTokenInfoAndGetLatestInfo") - .given( - cryptoCreate(TOKEN_TREASURY).balance(0L), - cryptoCreate(UPDATED_TREASURY) - .balance(0L) - .keyShape(ED25519_ON) - .maxAutomaticTokenAssociations(2), - cryptoCreate(AUTO_RENEW_ACCOUNT).balance(0L), - cryptoCreate(NFT_OWNER), - cryptoCreate(NFT_SPENDER), - cryptoCreate(HTS_COLLECTOR), - newKeyNamed(ADMIN_KEY), - newKeyNamed(FREEZE_KEY), - newKeyNamed(KYC_KEY), - newKeyNamed(SUPPLY_KEY), - newKeyNamed(WIPE_KEY), - newKeyNamed(FEE_SCHEDULE_KEY), - newKeyNamed(PAUSE_KEY), - uploadInitCode(TOKEN_INFO_CONTRACT), - contractCreate(TOKEN_INFO_CONTRACT).gas(1_000_000L), - tokenCreate(FEE_DENOM).treasury(HTS_COLLECTOR), - tokenCreate(NON_FUNGIBLE_TOKEN_NAME) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .supplyType(TokenSupplyType.FINITE) - .entityMemo(MEMO) - .name(NON_FUNGIBLE_TOKEN_NAME) - .symbol(NON_FUNGIBLE_SYMBOL) - .treasury(TOKEN_TREASURY) - .autoRenewAccount(AUTO_RENEW_ACCOUNT) - .autoRenewPeriod(THREE_MONTHS_IN_SECONDS) - .maxSupply(maxSupply) - .initialSupply(0) - .adminKey(ADMIN_KEY) - .freezeKey(FREEZE_KEY) - .kycKey(KYC_KEY) - .supplyKey(SUPPLY_KEY) - .wipeKey(WIPE_KEY) - .feeScheduleKey(FEE_SCHEDULE_KEY) - .pauseKey(PAUSE_KEY) - .withCustom(royaltyFeeWithFallback( - 1, 2, fixedHtsFeeInheritingRoyaltyCollector(100, FEE_DENOM), HTS_COLLECTOR)) - .via(CREATE_TXN), - mintToken(NON_FUNGIBLE_TOKEN_NAME, List.of(meta)), - tokenAssociate(NFT_OWNER, List.of(NON_FUNGIBLE_TOKEN_NAME)), - tokenAssociate(NFT_SPENDER, List.of(NON_FUNGIBLE_TOKEN_NAME)), - grantTokenKyc(NON_FUNGIBLE_TOKEN_NAME, NFT_OWNER), - cryptoTransfer(TokenMovement.movingUnique(NON_FUNGIBLE_TOKEN_NAME, 1L) - .between(TOKEN_TREASURY, NFT_OWNER)), - cryptoApproveAllowance() - .payingWith(DEFAULT_PAYER) - .addNftAllowance(NFT_OWNER, NON_FUNGIBLE_TOKEN_NAME, NFT_SPENDER, false, List.of(1L)) - .via(APPROVE_TXN) - .logged() - .signedBy(DEFAULT_PAYER, NFT_OWNER) - .fee(ONE_HBAR)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_INFO_CONTRACT, - UPDATE_INFORMATION_FOR_NON_FUNGIBLE_TOKEN_AND_GET_LATEST_INFORMATION, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(NON_FUNGIBLE_TOKEN_NAME))), - 1L, - UPDATE_NAME, - UPDATE_SYMBOL, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(UPDATED_TREASURY))), - UPDATE_MEMO) - .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_TREASURY) - .via(UPDATE_ANG_GET_NON_FUNGIBLE_TOKEN_INFO_TXN) - .gas(1_000_000L)))) - .then(withOpContext((spec, opLog) -> { - final var getTokenInfoQuery = getTokenInfo(NON_FUNGIBLE_TOKEN_NAME); - allRunFor(spec, getTokenInfoQuery); - final var expirySecond = getTokenInfoQuery - .getResponse() - .getTokenGetInfo() - .getTokenInfo() - .getExpiry() - .getSeconds(); - - final var nftTokenInfo = getTokenNftInfoForCheck(spec, getTokenInfoQuery, meta); - - allRunFor( - spec, - getTxnRecord(UPDATE_ANG_GET_NON_FUNGIBLE_TOKEN_INFO_TXN) - .andAllChildRecords() - .logged(), - childRecordsCheck( - UPDATE_ANG_GET_NON_FUNGIBLE_TOKEN_INFO_TXN, - SUCCESS, - recordWith().status(SUCCESS), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_GET_NON_FUNGIBLE_TOKEN_INFO) - .withStatus(SUCCESS) - .withTokenInfo(getTokenInfoStructForNonFungibleToken( - spec, - UPDATE_NAME, - UPDATE_SYMBOL, - UPDATE_MEMO, - spec.registry() - .getAccountID(UPDATED_TREASURY), - expirySecond)) - .withNftTokenInfo(nftTokenInfo))))); - })); - } - private HapiSpec getInfoOnDeletedFungibleTokenWorks() { return defaultHapiSpec("GetInfoOnDeletedFungibleTokenFails") .given( @@ -1086,117 +748,6 @@ private HapiSpec happyPathGetNonFungibleTokenCustomFees() { .withCustomFees(getCustomFeeForNFT(spec)))))))); } - private HapiSpec happyPathUpdateTokenKeysAndReadLatestInformation() { - final String TOKEN_INFO_AS_KEY = "TOKEN_INFO_CONTRACT_KEY"; - return defaultHapiSpec("UpdateTokenKeysAndReadLatestInformation") - .given( - cryptoCreate(TOKEN_TREASURY).balance(0L), - cryptoCreate(AUTO_RENEW_ACCOUNT).balance(0L), - cryptoCreate(HTS_COLLECTOR), - cryptoCreate(ACCOUNT), - uploadInitCode(TOKEN_INFO_CONTRACT), - contractCreate(TOKEN_INFO_CONTRACT).gas(1_000_000L), - newKeyNamed(MULTI_KEY), - newKeyNamed(TOKEN_INFO_AS_KEY).shape(CONTRACT.signedWith(TOKEN_INFO_CONTRACT)), - tokenCreate(FUNGIBLE_TOKEN_NAME) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .feeScheduleKey(MULTI_KEY) - .pauseKey(MULTI_KEY) - .wipeKey(MULTI_KEY) - .freezeKey(MULTI_KEY) - .kycKey(MULTI_KEY) - .initialSupply(1_000), - tokenAssociate(ACCOUNT, FUNGIBLE_TOKEN_NAME)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_INFO_CONTRACT, - UPDATE_AND_GET_TOKEN_KEYS_INFO_TXN, - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getTokenID(FUNGIBLE_TOKEN_NAME))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_INFO_CONTRACT)))) - .via(UPDATE_AND_GET_TOKEN_KEYS_INFO_TXN) - .alsoSigningWithFullPrefix(MULTI_KEY)))) - .then(withOpContext((spec, opLog) -> allRunFor( - spec, - getTxnRecord(UPDATE_AND_GET_TOKEN_KEYS_INFO_TXN) - .andAllChildRecords() - .logged(), - childRecordsCheck( - UPDATE_AND_GET_TOKEN_KEYS_INFO_TXN, - SUCCESS, - recordWith().status(SUCCESS), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue( - // - // spec.registry().getKey(TOKEN_INFO_AS_KEY) - Key.newBuilder() - .setContractID( - spec.registry() - .getContractId( - TOKEN_INFO_CONTRACT)) - .build()))), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue(Key.newBuilder() - .setContractID(spec.registry() - .getContractId(TOKEN_INFO_CONTRACT)) - .build()))), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue( - spec.registry().getKey(TOKEN_INFO_AS_KEY)))), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue( - spec.registry().getKey(TOKEN_INFO_AS_KEY)))), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue( - spec.registry().getKey(TOKEN_INFO_AS_KEY)))), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue( - spec.registry().getKey(TOKEN_INFO_AS_KEY)))), - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue( - spec.registry().getKey(TOKEN_INFO_AS_KEY)))))))); - } - private TokenNftInfo getTokenNftInfoForCheck( final HapiSpec spec, final HapiGetTokenInfo getTokenInfoQuery, final ByteString meta) { final var tokenId = diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSV1SecurityModelSuite.java new file mode 100644 index 000000000000..9dc8c00fd150 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSV1SecurityModelSuite.java @@ -0,0 +1,796 @@ +/* + * Copyright (C) 2022-2023 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.spec.HapiSpec.propertyPreservingHapiSpec; +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.keys.KeyShape.CONTRACT; +import static com.hedera.services.bdd.spec.keys.SigControl.ED25519_ON; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenNftInfo; +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.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoApproveAllowance; +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.grantTokenKyc; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; +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.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFeeInheritingRoyaltyCollector; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fractionalFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.royaltyFeeWithFallback; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.*; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.*; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; + +import com.google.protobuf.ByteString; +import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.queries.token.HapiGetTokenInfo; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.spec.transactions.token.TokenMovement; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hedera.services.bdd.suites.utils.contracts.precompile.TokenKeyType; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.CustomFee; +import com.hederahashgraph.api.proto.java.Duration; +import com.hederahashgraph.api.proto.java.FixedFee; +import com.hederahashgraph.api.proto.java.Fraction; +import com.hederahashgraph.api.proto.java.FractionalFee; +import com.hederahashgraph.api.proto.java.Key; +import com.hederahashgraph.api.proto.java.NftID; +import com.hederahashgraph.api.proto.java.RoyaltyFee; +import com.hederahashgraph.api.proto.java.Timestamp; +import com.hederahashgraph.api.proto.java.TokenInfo; +import com.hederahashgraph.api.proto.java.TokenNftInfo; +import com.hederahashgraph.api.proto.java.TokenSupplyType; +import com.hederahashgraph.api.proto.java.TokenType; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalLong; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse +public class TokenInfoHTSV1SecurityModelSuite extends HapiSuite { + + private static final Logger LOG = LogManager.getLogger(TokenInfoHTSV1SecurityModelSuite.class); + + private static final String TOKEN_INFO_CONTRACT = "TokenInfoContract"; + private static final String ADMIN_KEY = TokenKeyType.ADMIN_KEY.name(); + private static final String KYC_KEY = TokenKeyType.KYC_KEY.name(); + private static final String SUPPLY_KEY = TokenKeyType.SUPPLY_KEY.name(); + private static final String FREEZE_KEY = TokenKeyType.FREEZE_KEY.name(); + private static final String WIPE_KEY = TokenKeyType.WIPE_KEY.name(); + private static final String FEE_SCHEDULE_KEY = TokenKeyType.FEE_SCHEDULE_KEY.name(); + private static final String PAUSE_KEY = TokenKeyType.PAUSE_KEY.name(); + private static final String AUTO_RENEW_ACCOUNT = "autoRenewAccount"; + private static final String FEE_DENOM = "denom"; + public static final String HTS_COLLECTOR = "denomFee"; + private static final String ACCOUNT = "Account"; + private static final String CREATE_TXN = "CreateTxn"; + private static final String UPDATE_ANG_GET_TOKEN_INFO_TXN = "UpdateAndGetTokenInfoTxn"; + private static final String UPDATE_ANG_GET_FUNGIBLE_TOKEN_INFO_TXN = "UpdateAndGetFungibleTokenInfoTxn"; + private static final String UPDATE_ANG_GET_NON_FUNGIBLE_TOKEN_INFO_TXN = "UpdateAndGetNonFungibleTokenInfoTxn"; + private static final String APPROVE_TXN = "approveTxn"; + private static final String UPDATE_AND_GET_TOKEN_KEYS_INFO_TXN = "updateTokenKeysAndReadLatestInformation"; + private static final String FUNGIBLE_SYMBOL = "FT"; + private static final String FUNGIBLE_TOKEN_NAME = "FungibleToken"; + private static final String NON_FUNGIBLE_SYMBOL = "NFT"; + private static final String META = "First"; + private static final String MEMO = "JUMP"; + private static final String UPDATE_NAME = "NewName"; + private static final String UPDATE_SYMBOL = "NewSymbol"; + private static final String UPDATE_MEMO = "NewMemo"; + private static final String NFT_OWNER = "NFT Owner"; + private static final String NFT_SPENDER = "NFT Spender"; + private static final String NON_FUNGIBLE_TOKEN_NAME = "NonFungibleToken"; + private static final String MULTI_KEY = "multiKey"; + + private static final String UPDATE_INFORMATION_FOR_TOKEN_AND_GET_LATEST_INFORMATION = + "updateInformationForTokenAndGetLatestInformation"; + private static final String UPDATE_INFORMATION_FOR_FUNGIBLE_TOKEN_AND_GET_LATEST_INFORMATION = + "updateInformationForFungibleTokenAndGetLatestInformation"; + private static final String UPDATE_INFORMATION_FOR_NON_FUNGIBLE_TOKEN_AND_GET_LATEST_INFORMATION = + "updateInformationForNonFungibleTokenAndGetLatestInformation"; + private static final int NUMERATOR = 1; + private static final int DENOMINATOR = 2; + private static final int MINIMUM_TO_COLLECT = 5; + private static final int MAXIMUM_TO_COLLECT = 400; + private static final int MAX_SUPPLY = 1000; + + public static void main(final String... args) { + new TokenInfoHTSV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + public List getSpecsInSuite() { + return allOf(positiveSpecs(), negativeSpecs()); + } + + List negativeSpecs() { + return List.of(); + } + + List positiveSpecs() { + return List.of( + happyPathUpdateTokenInfoAndGetLatestInfo(), + happyPathUpdateFungibleTokenInfoAndGetLatestInfo(), + happyPathUpdateNonFungibleTokenInfoAndGetLatestInfo(), + happyPathUpdateTokenKeysAndReadLatestInformation()); + } + + private HapiSpec happyPathUpdateTokenInfoAndGetLatestInfo() { + final int decimals = 1; + return propertyPreservingHapiSpec("happyPathUpdateTokenInfoAndGetLatestInfo") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,TokenAssociateToAccount,TokenCreate,TokenUpdate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(TOKEN_TREASURY).balance(0L), + cryptoCreate(UPDATED_TREASURY) + .keyShape(ED25519_ON) + .balance(0L) + .maxAutomaticTokenAssociations(3), + cryptoCreate(AUTO_RENEW_ACCOUNT).balance(0L), + cryptoCreate(HTS_COLLECTOR), + cryptoCreate(ACCOUNT), + newKeyNamed(ADMIN_KEY), + newKeyNamed(FREEZE_KEY), + newKeyNamed(KYC_KEY), + newKeyNamed(SUPPLY_KEY), + newKeyNamed(WIPE_KEY), + newKeyNamed(FEE_SCHEDULE_KEY), + newKeyNamed(PAUSE_KEY), + uploadInitCode(TOKEN_INFO_CONTRACT), + contractCreate(TOKEN_INFO_CONTRACT).gas(1_000_000L), + tokenCreate(FUNGIBLE_TOKEN_NAME) + .supplyType(TokenSupplyType.FINITE) + .entityMemo(MEMO) + .name(FUNGIBLE_TOKEN_NAME) + .symbol(FUNGIBLE_SYMBOL) + .treasury(TOKEN_TREASURY) + .autoRenewAccount(AUTO_RENEW_ACCOUNT) + .autoRenewPeriod(THREE_MONTHS_IN_SECONDS) + .maxSupply(MAX_SUPPLY) + .initialSupply(500) + .decimals(decimals) + .adminKey(ADMIN_KEY) + .freezeKey(FREEZE_KEY) + .kycKey(KYC_KEY) + .supplyKey(SUPPLY_KEY) + .wipeKey(WIPE_KEY) + .feeScheduleKey(FEE_SCHEDULE_KEY) + .pauseKey(PAUSE_KEY) + .withCustom(fixedHbarFee(500L, HTS_COLLECTOR)) + // Include a fractional fee with no minimum to collect + .withCustom(fractionalFee( + NUMERATOR, DENOMINATOR * 2L, 0, OptionalLong.empty(), TOKEN_TREASURY)) + .withCustom(fractionalFee( + NUMERATOR, + DENOMINATOR, + MINIMUM_TO_COLLECT, + OptionalLong.of(MAXIMUM_TO_COLLECT), + TOKEN_TREASURY)) + .via(CREATE_TXN), + tokenAssociate(ACCOUNT, FUNGIBLE_TOKEN_NAME)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_INFO_CONTRACT, + UPDATE_INFORMATION_FOR_TOKEN_AND_GET_LATEST_INFORMATION, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(FUNGIBLE_TOKEN_NAME))), + UPDATE_NAME, + UPDATE_SYMBOL, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(UPDATED_TREASURY))), + UPDATE_MEMO) + .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_TREASURY) + .payingWith(ACCOUNT) + .via(UPDATE_ANG_GET_TOKEN_INFO_TXN) + .gas(1_000_000L)))) + .then(withOpContext((spec, opLog) -> { + final var getTokenInfoQuery = getTokenInfo(FUNGIBLE_TOKEN_NAME); + allRunFor(spec, getTokenInfoQuery); + final var expirySecond = getTokenInfoQuery + .getResponse() + .getTokenGetInfo() + .getTokenInfo() + .getExpiry() + .getSeconds(); + allRunFor( + spec, + getTxnRecord(UPDATE_ANG_GET_TOKEN_INFO_TXN) + .andAllChildRecords() + .logged(), + childRecordsCheck( + UPDATE_ANG_GET_TOKEN_INFO_TXN, + SUCCESS, + recordWith().status(SUCCESS), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_GET_TOKEN_INFO) + .withStatus(SUCCESS) + .withDecimals(decimals) + .withTokenInfo(getTokenInfoStructForFungibleToken( + spec, + UPDATE_NAME, + UPDATE_SYMBOL, + UPDATE_MEMO, + spec.registry() + .getAccountID(UPDATED_TREASURY), + expirySecond)))))); + })); + } + + private HapiSpec happyPathUpdateFungibleTokenInfoAndGetLatestInfo() { + final int decimals = 1; + return propertyPreservingHapiSpec("happyPathUpdateFungibleTokenInfoAndGetLatestInfo") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,TokenAssociateToAccount,TokenCreate,TokenUpdate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(TOKEN_TREASURY).balance(0L), + cryptoCreate(UPDATED_TREASURY).balance(0L).maxAutomaticTokenAssociations(3), + cryptoCreate(AUTO_RENEW_ACCOUNT).balance(0L), + cryptoCreate(HTS_COLLECTOR), + cryptoCreate(ACCOUNT), + newKeyNamed(ADMIN_KEY), + newKeyNamed(FREEZE_KEY), + newKeyNamed(KYC_KEY), + newKeyNamed(SUPPLY_KEY), + newKeyNamed(WIPE_KEY), + newKeyNamed(FEE_SCHEDULE_KEY), + newKeyNamed(PAUSE_KEY), + uploadInitCode(TOKEN_INFO_CONTRACT), + contractCreate(TOKEN_INFO_CONTRACT).gas(1_000_000L), + tokenCreate(FUNGIBLE_TOKEN_NAME) + .supplyType(TokenSupplyType.FINITE) + .entityMemo(MEMO) + .name(FUNGIBLE_TOKEN_NAME) + .symbol(FUNGIBLE_SYMBOL) + .treasury(TOKEN_TREASURY) + .autoRenewAccount(AUTO_RENEW_ACCOUNT) + .autoRenewPeriod(THREE_MONTHS_IN_SECONDS) + .maxSupply(MAX_SUPPLY) + .initialSupply(500) + .decimals(decimals) + .adminKey(ADMIN_KEY) + .freezeKey(FREEZE_KEY) + .kycKey(KYC_KEY) + .supplyKey(SUPPLY_KEY) + .wipeKey(WIPE_KEY) + .feeScheduleKey(FEE_SCHEDULE_KEY) + .pauseKey(PAUSE_KEY) + .withCustom(fixedHbarFee(500L, HTS_COLLECTOR)) + // Include a fractional fee with no minimum to collect + .withCustom(fractionalFee( + NUMERATOR, DENOMINATOR * 2L, 0, OptionalLong.empty(), TOKEN_TREASURY)) + .withCustom(fractionalFee( + NUMERATOR, + DENOMINATOR, + MINIMUM_TO_COLLECT, + OptionalLong.of(MAXIMUM_TO_COLLECT), + TOKEN_TREASURY)) + .via(CREATE_TXN), + tokenAssociate(ACCOUNT, FUNGIBLE_TOKEN_NAME)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_INFO_CONTRACT, + UPDATE_INFORMATION_FOR_FUNGIBLE_TOKEN_AND_GET_LATEST_INFORMATION, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(FUNGIBLE_TOKEN_NAME))), + UPDATE_NAME, + UPDATE_SYMBOL, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(UPDATED_TREASURY))), + UPDATE_MEMO) + .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_TREASURY) + .payingWith(ACCOUNT) + .via(UPDATE_ANG_GET_FUNGIBLE_TOKEN_INFO_TXN) + .gas(1_000_000L)))) + .then(withOpContext((spec, opLog) -> { + final var getTokenInfoQuery = getTokenInfo(FUNGIBLE_TOKEN_NAME); + allRunFor(spec, getTokenInfoQuery); + final var expirySecond = getTokenInfoQuery + .getResponse() + .getTokenGetInfo() + .getTokenInfo() + .getExpiry() + .getSeconds(); + allRunFor( + spec, + getTxnRecord(UPDATE_ANG_GET_FUNGIBLE_TOKEN_INFO_TXN) + .andAllChildRecords() + .logged(), + childRecordsCheck( + UPDATE_ANG_GET_FUNGIBLE_TOKEN_INFO_TXN, + SUCCESS, + recordWith().status(SUCCESS), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_GET_FUNGIBLE_TOKEN_INFO) + .withStatus(SUCCESS) + .withDecimals(decimals) + .withTokenInfo(getTokenInfoStructForFungibleToken( + spec, + UPDATE_NAME, + UPDATE_SYMBOL, + UPDATE_MEMO, + spec.registry() + .getAccountID(UPDATED_TREASURY), + expirySecond)))))); + })); + } + + private HapiSpec happyPathUpdateNonFungibleTokenInfoAndGetLatestInfo() { + final int maxSupply = 10; + final ByteString meta = ByteString.copyFrom(META.getBytes(StandardCharsets.UTF_8)); + return propertyPreservingHapiSpec("happyPathUpdateNonFungibleTokenInfoAndGetLatestInfo") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,TokenAssociateToAccount,TokenCreate,TokenUpdate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(TOKEN_TREASURY).balance(0L), + cryptoCreate(UPDATED_TREASURY) + .balance(0L) + .keyShape(ED25519_ON) + .maxAutomaticTokenAssociations(2), + cryptoCreate(AUTO_RENEW_ACCOUNT).balance(0L), + cryptoCreate(NFT_OWNER), + cryptoCreate(NFT_SPENDER), + cryptoCreate(HTS_COLLECTOR), + newKeyNamed(ADMIN_KEY), + newKeyNamed(FREEZE_KEY), + newKeyNamed(KYC_KEY), + newKeyNamed(SUPPLY_KEY), + newKeyNamed(WIPE_KEY), + newKeyNamed(FEE_SCHEDULE_KEY), + newKeyNamed(PAUSE_KEY), + uploadInitCode(TOKEN_INFO_CONTRACT), + contractCreate(TOKEN_INFO_CONTRACT).gas(1_000_000L), + tokenCreate(FEE_DENOM).treasury(HTS_COLLECTOR), + tokenCreate(NON_FUNGIBLE_TOKEN_NAME) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .supplyType(TokenSupplyType.FINITE) + .entityMemo(MEMO) + .name(NON_FUNGIBLE_TOKEN_NAME) + .symbol(NON_FUNGIBLE_SYMBOL) + .treasury(TOKEN_TREASURY) + .autoRenewAccount(AUTO_RENEW_ACCOUNT) + .autoRenewPeriod(THREE_MONTHS_IN_SECONDS) + .maxSupply(maxSupply) + .initialSupply(0) + .adminKey(ADMIN_KEY) + .freezeKey(FREEZE_KEY) + .kycKey(KYC_KEY) + .supplyKey(SUPPLY_KEY) + .wipeKey(WIPE_KEY) + .feeScheduleKey(FEE_SCHEDULE_KEY) + .pauseKey(PAUSE_KEY) + .withCustom(royaltyFeeWithFallback( + 1, 2, fixedHtsFeeInheritingRoyaltyCollector(100, FEE_DENOM), HTS_COLLECTOR)) + .via(CREATE_TXN), + mintToken(NON_FUNGIBLE_TOKEN_NAME, List.of(meta)), + tokenAssociate(NFT_OWNER, List.of(NON_FUNGIBLE_TOKEN_NAME)), + tokenAssociate(NFT_SPENDER, List.of(NON_FUNGIBLE_TOKEN_NAME)), + grantTokenKyc(NON_FUNGIBLE_TOKEN_NAME, NFT_OWNER), + cryptoTransfer(TokenMovement.movingUnique(NON_FUNGIBLE_TOKEN_NAME, 1L) + .between(TOKEN_TREASURY, NFT_OWNER)), + cryptoApproveAllowance() + .payingWith(DEFAULT_PAYER) + .addNftAllowance(NFT_OWNER, NON_FUNGIBLE_TOKEN_NAME, NFT_SPENDER, false, List.of(1L)) + .via(APPROVE_TXN) + .logged() + .signedBy(DEFAULT_PAYER, NFT_OWNER) + .fee(ONE_HBAR)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_INFO_CONTRACT, + UPDATE_INFORMATION_FOR_NON_FUNGIBLE_TOKEN_AND_GET_LATEST_INFORMATION, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(NON_FUNGIBLE_TOKEN_NAME))), + 1L, + UPDATE_NAME, + UPDATE_SYMBOL, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(UPDATED_TREASURY))), + UPDATE_MEMO) + .alsoSigningWithFullPrefix(ADMIN_KEY, UPDATED_TREASURY) + .via(UPDATE_ANG_GET_NON_FUNGIBLE_TOKEN_INFO_TXN) + .gas(1_000_000L)))) + .then(withOpContext((spec, opLog) -> { + final var getTokenInfoQuery = getTokenInfo(NON_FUNGIBLE_TOKEN_NAME); + allRunFor(spec, getTokenInfoQuery); + final var expirySecond = getTokenInfoQuery + .getResponse() + .getTokenGetInfo() + .getTokenInfo() + .getExpiry() + .getSeconds(); + + final var nftTokenInfo = getTokenNftInfoForCheck(spec, getTokenInfoQuery, meta); + + allRunFor( + spec, + getTxnRecord(UPDATE_ANG_GET_NON_FUNGIBLE_TOKEN_INFO_TXN) + .andAllChildRecords() + .logged(), + childRecordsCheck( + UPDATE_ANG_GET_NON_FUNGIBLE_TOKEN_INFO_TXN, + SUCCESS, + recordWith().status(SUCCESS), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_GET_NON_FUNGIBLE_TOKEN_INFO) + .withStatus(SUCCESS) + .withTokenInfo(getTokenInfoStructForNonFungibleToken( + spec, + UPDATE_NAME, + UPDATE_SYMBOL, + UPDATE_MEMO, + spec.registry() + .getAccountID(UPDATED_TREASURY), + expirySecond)) + .withNftTokenInfo(nftTokenInfo))))); + })); + } + + private HapiSpec happyPathUpdateTokenKeysAndReadLatestInformation() { + final String TOKEN_INFO_AS_KEY = "TOKEN_INFO_CONTRACT_KEY"; + return propertyPreservingHapiSpec("happyPathUpdateTokenKeysAndReadLatestInformation") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,TokenAssociateToAccount,TokenCreate,TokenUpdate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(TOKEN_TREASURY).balance(0L), + cryptoCreate(AUTO_RENEW_ACCOUNT).balance(0L), + cryptoCreate(HTS_COLLECTOR), + cryptoCreate(ACCOUNT), + uploadInitCode(TOKEN_INFO_CONTRACT), + contractCreate(TOKEN_INFO_CONTRACT).gas(1_000_000L), + newKeyNamed(MULTI_KEY), + newKeyNamed(TOKEN_INFO_AS_KEY).shape(CONTRACT.signedWith(TOKEN_INFO_CONTRACT)), + tokenCreate(FUNGIBLE_TOKEN_NAME) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .feeScheduleKey(MULTI_KEY) + .pauseKey(MULTI_KEY) + .wipeKey(MULTI_KEY) + .freezeKey(MULTI_KEY) + .kycKey(MULTI_KEY) + .initialSupply(1_000), + tokenAssociate(ACCOUNT, FUNGIBLE_TOKEN_NAME)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_INFO_CONTRACT, + UPDATE_AND_GET_TOKEN_KEYS_INFO_TXN, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getTokenID(FUNGIBLE_TOKEN_NAME))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_INFO_CONTRACT)))) + .via(UPDATE_AND_GET_TOKEN_KEYS_INFO_TXN) + .alsoSigningWithFullPrefix(MULTI_KEY)))) + .then(withOpContext((spec, opLog) -> allRunFor( + spec, + getTxnRecord(UPDATE_AND_GET_TOKEN_KEYS_INFO_TXN) + .andAllChildRecords() + .logged(), + childRecordsCheck( + UPDATE_AND_GET_TOKEN_KEYS_INFO_TXN, + SUCCESS, + recordWith().status(SUCCESS), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue( + // + // spec.registry().getKey(TOKEN_INFO_AS_KEY) + Key.newBuilder() + .setContractID( + spec.registry() + .getContractId( + TOKEN_INFO_CONTRACT)) + .build()))), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue(Key.newBuilder() + .setContractID(spec.registry() + .getContractId(TOKEN_INFO_CONTRACT)) + .build()))), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue( + spec.registry().getKey(TOKEN_INFO_AS_KEY)))), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue( + spec.registry().getKey(TOKEN_INFO_AS_KEY)))), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue( + spec.registry().getKey(TOKEN_INFO_AS_KEY)))), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue( + spec.registry().getKey(TOKEN_INFO_AS_KEY)))), + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue( + spec.registry().getKey(TOKEN_INFO_AS_KEY)))))))); + } + + private TokenNftInfo getTokenNftInfoForCheck( + final HapiSpec spec, final HapiGetTokenInfo getTokenInfoQuery, final ByteString meta) { + final var tokenId = + getTokenInfoQuery.getResponse().getTokenGetInfo().getTokenInfo().getTokenId(); + + final var getNftTokenInfoQuery = getTokenNftInfo(NON_FUNGIBLE_TOKEN_NAME, 1L); + allRunFor(spec, getNftTokenInfoQuery); + final var creationTime = + getNftTokenInfoQuery.getResponse().getTokenGetNftInfo().getNft().getCreationTime(); + + final var ownerId = spec.registry().getAccountID(NFT_OWNER); + final var spenderId = spec.registry().getAccountID(NFT_SPENDER); + + return TokenNftInfo.newBuilder() + .setLedgerId(fromString("0x03")) + .setNftID(NftID.newBuilder() + .setTokenID(tokenId) + .setSerialNumber(1L) + .build()) + .setAccountID(ownerId) + .setCreationTime(creationTime) + .setMetadata(meta) + .setSpenderId(spenderId) + .build(); + } + + private TokenInfo getTokenInfoStructForFungibleToken( + final HapiSpec spec, + final String tokenName, + final String symbol, + final String memo, + final AccountID treasury, + final long expirySecond) { + final var autoRenewAccount = spec.registry().getAccountID(AUTO_RENEW_ACCOUNT); + + final ArrayList customFees = getExpectedCustomFees(spec); + + return TokenInfo.newBuilder() + .setLedgerId(fromString("0x03")) + .setSupplyTypeValue(TokenSupplyType.FINITE_VALUE) + .setExpiry(Timestamp.newBuilder().setSeconds(expirySecond)) + .setAutoRenewAccount(autoRenewAccount) + .setAutoRenewPeriod(Duration.newBuilder() + .setSeconds(THREE_MONTHS_IN_SECONDS) + .build()) + .setSymbol(symbol) + .setName(tokenName) + .setMemo(memo) + .setTreasury(treasury) + .setTotalSupply(500L) + .setMaxSupply(MAX_SUPPLY) + .addAllCustomFees(customFees) + .setAdminKey(getTokenKeyFromSpec(spec, TokenKeyType.ADMIN_KEY)) + .setKycKey(getTokenKeyFromSpec(spec, TokenKeyType.KYC_KEY)) + .setFreezeKey(getTokenKeyFromSpec(spec, TokenKeyType.FREEZE_KEY)) + .setWipeKey(getTokenKeyFromSpec(spec, TokenKeyType.WIPE_KEY)) + .setSupplyKey(getTokenKeyFromSpec(spec, TokenKeyType.SUPPLY_KEY)) + .setFeeScheduleKey(getTokenKeyFromSpec(spec, TokenKeyType.FEE_SCHEDULE_KEY)) + .setPauseKey(getTokenKeyFromSpec(spec, TokenKeyType.PAUSE_KEY)) + .build(); + } + + @NotNull + private ArrayList getExpectedCustomFees(final HapiSpec spec) { + final var fixedFee = FixedFee.newBuilder().setAmount(500L).build(); + final var customFixedFee = CustomFee.newBuilder() + .setFixedFee(fixedFee) + .setFeeCollectorAccountId(spec.registry().getAccountID(HTS_COLLECTOR)) + .build(); + + final var firstFraction = Fraction.newBuilder() + .setNumerator(NUMERATOR) + .setDenominator(DENOMINATOR * 2L) + .build(); + final var firstFractionalFee = + FractionalFee.newBuilder().setFractionalAmount(firstFraction).build(); + final var firstCustomFractionalFee = CustomFee.newBuilder() + .setFractionalFee(firstFractionalFee) + .setFeeCollectorAccountId(spec.registry().getAccountID(TOKEN_TREASURY)) + .build(); + + final var fraction = Fraction.newBuilder() + .setNumerator(NUMERATOR) + .setDenominator(DENOMINATOR) + .build(); + final var fractionalFee = FractionalFee.newBuilder() + .setFractionalAmount(fraction) + .setMinimumAmount(MINIMUM_TO_COLLECT) + .setMaximumAmount(MAXIMUM_TO_COLLECT) + .build(); + final var customFractionalFee = CustomFee.newBuilder() + .setFractionalFee(fractionalFee) + .setFeeCollectorAccountId(spec.registry().getAccountID(TOKEN_TREASURY)) + .build(); + + final var customFees = new ArrayList(); + customFees.add(customFixedFee); + customFees.add(firstCustomFractionalFee); + customFees.add(customFractionalFee); + return customFees; + } + + private TokenInfo getTokenInfoStructForNonFungibleToken( + final HapiSpec spec, + final String tokenName, + final String symbol, + final String memo, + final AccountID treasury, + final long expirySecond) { + final var autoRenewAccount = spec.registry().getAccountID(AUTO_RENEW_ACCOUNT); + + return TokenInfo.newBuilder() + .setLedgerId(fromString("0x03")) + .setSupplyTypeValue(TokenSupplyType.FINITE_VALUE) + .setExpiry(Timestamp.newBuilder().setSeconds(expirySecond)) + .setAutoRenewAccount(autoRenewAccount) + .setAutoRenewPeriod(Duration.newBuilder() + .setSeconds(THREE_MONTHS_IN_SECONDS) + .build()) + .setSymbol(symbol) + .setName(tokenName) + .setMemo(memo) + .setTreasury(treasury) + .setTotalSupply(1L) + .setMaxSupply(10L) + .addAllCustomFees(getCustomFeeForNFT(spec)) + .setAdminKey(getTokenKeyFromSpec(spec, TokenKeyType.ADMIN_KEY)) + .setKycKey(getTokenKeyFromSpec(spec, TokenKeyType.KYC_KEY)) + .setFreezeKey(getTokenKeyFromSpec(spec, TokenKeyType.FREEZE_KEY)) + .setWipeKey(getTokenKeyFromSpec(spec, TokenKeyType.WIPE_KEY)) + .setSupplyKey(getTokenKeyFromSpec(spec, TokenKeyType.SUPPLY_KEY)) + .setFeeScheduleKey(getTokenKeyFromSpec(spec, TokenKeyType.FEE_SCHEDULE_KEY)) + .setPauseKey(getTokenKeyFromSpec(spec, TokenKeyType.PAUSE_KEY)) + .build(); + } + + @NotNull + private ArrayList getCustomFeeForNFT(final HapiSpec spec) { + final var fraction = Fraction.newBuilder() + .setNumerator(NUMERATOR) + .setDenominator(DENOMINATOR) + .build(); + final var fallbackFee = FixedFee.newBuilder() + .setAmount(100L) + .setDenominatingTokenId(spec.registry().getTokenID(FEE_DENOM)) + .build(); + final var royaltyFee = RoyaltyFee.newBuilder() + .setExchangeValueFraction(fraction) + .setFallbackFee(fallbackFee) + .build(); + + final var customRoyaltyFee = CustomFee.newBuilder() + .setRoyaltyFee(royaltyFee) + .setFeeCollectorAccountId(spec.registry().getAccountID(HTS_COLLECTOR)) + .build(); + + final var customFees = new ArrayList(); + customFees.add(customRoyaltyFee); + + return customFees; + } + + private Key getTokenKeyFromSpec(final HapiSpec spec, final TokenKeyType type) { + final var key = spec.registry().getKey(type.name()); + + final var keyBuilder = Key.newBuilder(); + + if (key.getContractID().getContractNum() > 0) { + keyBuilder.setContractID(key.getContractID()); + } + if (key.getEd25519().toByteArray().length > 0) { + keyBuilder.setEd25519(key.getEd25519()); + } + if (key.getECDSASecp256K1().toByteArray().length > 0) { + keyBuilder.setECDSASecp256K1(key.getECDSASecp256K1()); + } + if (key.getDelegatableContractId().getContractNum() > 0) { + keyBuilder.setDelegatableContractId(key.getDelegatableContractId()); + } + + return keyBuilder.build(); + } + + private ByteString fromString(final String value) { + return ByteString.copyFrom(Bytes.fromHexString(value).toArray()); + } + + @Override + protected Logger getResultsLogger() { + return LOG; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenUpdatePrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenUpdatePrecompileSuite.java index 9101c2b0e741..1346fdf9b1f9 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenUpdatePrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenUpdatePrecompileSuite.java @@ -17,17 +17,12 @@ package com.hedera.services.bdd.suites.contract.precompile; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; -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.keys.KeyShape.CONTRACT; import static com.hedera.services.bdd.spec.keys.KeyShape.DELEGATE_CONTRACT; import static com.hedera.services.bdd.spec.keys.KeyShape.ED25519; import static com.hedera.services.bdd.spec.keys.KeyShape.SECP256K1; import static com.hedera.services.bdd.spec.keys.SigControl.ED25519_ON; import static com.hedera.services.bdd.spec.queries.QueryVerbs.contractCallLocal; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenNftInfo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; @@ -46,35 +41,20 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.contract.Utils.asAddress; import static com.hedera.services.bdd.suites.contract.Utils.asToken; -import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.KEY_NOT_PROVIDED; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_FEE_SCHEDULE_KEY; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_FREEZE_KEY; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_KYC_KEY; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_PAUSE_KEY; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_SUPPLY_KEY; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_WIPE_KEY; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_IS_IMMUTABLE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_NAME_TOO_LONG; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_SYMBOL_TOO_LONG; import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; import com.google.protobuf.ByteString; -import com.hedera.node.app.hapi.utils.contracts.ParsingConstants; import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts; -import com.hedera.services.bdd.spec.transactions.TxnUtils; import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; import com.hedera.services.bdd.suites.HapiSuite; import com.hederahashgraph.api.proto.java.TokenID; -import com.hederahashgraph.api.proto.java.TokenPauseStatus; -import com.hederahashgraph.api.proto.java.TokenSupplyType; import com.hederahashgraph.api.proto.java.TokenType; import java.math.BigInteger; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; @@ -94,13 +74,6 @@ public class TokenUpdatePrecompileSuite extends HapiSuite { private static final String GET_KEY_FUNC = "getKeyFromToken"; public static final String TOKEN_UPDATE_CONTRACT = "UpdateTokenInfoContract"; private static final String UPDATE_TXN = "updateTxn"; - private static final String GET_KYC_KEY_TXN = "getKycTokenKeyTxn"; - private static final String GET_ADMIN_KEY_TXN = "getAdminTokenKeyTxn"; - private static final String GET_PAUSE_KEY_TXN = "getPauseTokenKeyTxn"; - private static final String GET_FREEZE_KEY_TXN = "getFreezeTokenKeyTxn"; - private static final String GET_WIPE_KEY_TXN = "getWipeTokenKeyTxn"; - private static final String GET_FEE_KEY_TXN = "getFeeTokenKeyTxn"; - private static final String GET_SUPPLY_KEY_TXN = "getSupplyTokenKeyTxn"; private static final String NO_ADMIN_KEY = "noAdminKeyTxn"; private static final long DEFAULT_AMOUNT_TO_SEND = 20 * ONE_HBAR; private static final String ED25519KEY = "ed25519key"; @@ -113,12 +86,7 @@ public class TokenUpdatePrecompileSuite extends HapiSuite { public static final String CUSTOM_SYMBOL = "Ω"; public static final String CUSTOM_MEMO = "Omega"; private static final long ADMIN_KEY_TYPE = 1L; - private static final long KYC_KEY_TYPE = 2L; - private static final long FREEZE_KEY_TYPE = 4L; - private static final long WIPE_KEY_TYPE = 8L; private static final long SUPPLY_KEY_TYPE = 16L; - private static final long FEE_SCHEDULE_KEY_TYPE = 32L; - private static final long PAUSE_KEY_TYPE = 64L; public static void main(String... args) { new TokenUpdatePrecompileSuite().runSuiteAsync(); @@ -136,509 +104,14 @@ protected Logger getResultsLogger() { @Override public List getSpecsInSuite() { - return allOf(positiveCases(), negativeCases()); + return allOf(negativeCases()); } - List positiveCases() { - return List.of( - updateTokenWithKeysHappyPath(), - updateNftTreasuryWithAndWithoutAdminKey(), - updateOnlyTokenKeysAndGetTheUpdatedValues(), - updateOnlyKeysForNonFungibleToken(), - updateTokenWithoutNameSymbolMemo()); - } - - List negativeCases() { - return List.of( - updateWithTooLongNameAndSymbol(), - updateTokenWithKeysNegative(), - updateTokenWithInvalidKeyValues(), - updateNftTokenKeysWithWrongTokenIdAndMissingAdmin(), - getTokenKeyForNonFungibleNegative()); - } - - private HapiSpec updateTokenWithKeysHappyPath() { - final AtomicReference vanillaTokenID = new AtomicReference<>(); - return defaultHapiSpec("updateTokenWithKeysHappyPath") - .given( - newKeyNamed(ED25519KEY).shape(ED25519), - newKeyNamed(ECDSA_KEY).shape(SECP256K1), - newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), - newKeyNamed(MULTI_KEY).shape(ED25519_ON), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(MULTI_KEY), - cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), - uploadInitCode(TOKEN_UPDATE_CONTRACT), - contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .feeScheduleKey(MULTI_KEY) - .pauseKey(MULTI_KEY) - .wipeKey(MULTI_KEY) - .freezeKey(MULTI_KEY) - .kycKey(MULTI_KEY) - .initialSupply(1_000) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - tokenAssociate(ACCOUNT, VANILLA_TOKEN), - grantTokenKyc(VANILLA_TOKEN, ACCOUNT), - cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_UPDATE_CONTRACT, - "updateTokenWithAllFields", - HapiParserUtil.asHeadlongAddress(new byte[20]), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - AUTO_RENEW_PERIOD, - CUSTOM_NAME, - CUSTOM_SYMBOL, - CUSTOM_MEMO) - .via(UPDATE_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_UPDATE_CONTRACT, - "updateTokenWithAllFields", - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - AUTO_RENEW_PERIOD, - CUSTOM_NAME, - CUSTOM_SYMBOL, - CUSTOM_MEMO) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT), - newKeyNamed(DELEGATE_KEY).shape(DELEGATE_CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT)), - newKeyNamed(TOKEN_UPDATE_AS_KEY).shape(CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT))))) - .then( - childRecordsCheck( - UPDATE_TXN, - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(INVALID_TOKEN_ID)), - sourcing(() -> getTokenInfo(VANILLA_TOKEN) - .logged() - .hasTokenType(TokenType.FUNGIBLE_COMMON) - .hasSymbol(CUSTOM_SYMBOL) - .hasName(CUSTOM_NAME) - .hasEntityMemo(CUSTOM_MEMO) - .hasTreasury(ACCOUNT) - .hasAutoRenewAccount(ACCOUNT) - .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) - .hasSupplyType(TokenSupplyType.INFINITE) - .searchKeysGlobally() - .hasAdminKey(ED25519KEY) - .hasPauseKey(MULTI_KEY) - .hasKycKey(ED25519KEY) - .hasFreezeKey(ECDSA_KEY) - .hasWipeKey(ECDSA_KEY) - .hasFeeScheduleKey(DELEGATE_KEY) - .hasSupplyKey(TOKEN_UPDATE_AS_KEY) - .hasPauseKey(TOKEN_UPDATE_AS_KEY))); - } - - public HapiSpec updateNftTreasuryWithAndWithoutAdminKey() { - final var newTokenTreasury = "newTokenTreasury"; - final var NO_ADMIN_TOKEN = "noAdminKeyToken"; - final AtomicReference noAdminKeyToken = new AtomicReference<>(); - final AtomicReference nftToken = new AtomicReference<>(); - return defaultHapiSpec("UpdateNftTreasuryWithAndWithoutAdminKey") - .given( - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(newTokenTreasury).keyShape(ED25519_ON).maxAutomaticTokenAssociations(6), - newKeyNamed(MULTI_KEY).shape(ED25519_ON), - cryptoCreate(ACCOUNT).key(MULTI_KEY).balance(ONE_MILLION_HBARS), - uploadInitCode(TOKEN_UPDATE_CONTRACT), - contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), - tokenCreate(NO_ADMIN_TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .treasury(TOKEN_TREASURY) - .initialSupply(0) - .supplyKey(MULTI_KEY) - .exposingCreatedIdTo(id -> noAdminKeyToken.set(asToken(id))), - tokenCreate(VANILLA_TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .treasury(TOKEN_TREASURY) - .initialSupply(0) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .pauseKey(MULTI_KEY) - .exposingCreatedIdTo(id -> nftToken.set(asToken(id))), - mintToken(VANILLA_TOKEN, List.of(ByteString.copyFromUtf8("nft0"))), - tokenAssociate(newTokenTreasury, VANILLA_TOKEN), - mintToken(NO_ADMIN_TOKEN, List.of(ByteString.copyFromUtf8("nft1")))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_UPDATE_CONTRACT, - "updateTokenTreasury", - HapiParserUtil.asHeadlongAddress(asAddress(noAdminKeyToken.get())), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(newTokenTreasury)))) - .via("noAdminKey") - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT, newTokenTreasury) - .alsoSigningWithFullPrefix(ACCOUNT, newTokenTreasury) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_UPDATE_CONTRACT, - "updateTokenTreasury", - HapiParserUtil.asHeadlongAddress(asAddress(nftToken.get())), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(newTokenTreasury)))) - .via("tokenUpdateTxn") - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT, newTokenTreasury) - .alsoSigningWithFullPrefix(ACCOUNT, newTokenTreasury)))) - .then( - childRecordsCheck( - "noAdminKey", - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(TOKEN_IS_IMMUTABLE)), - getTokenNftInfo(VANILLA_TOKEN, 1) - .hasAccountID(newTokenTreasury) - .logged(), - getAccountBalance(TOKEN_TREASURY).hasTokenBalance(VANILLA_TOKEN, 0), - getAccountBalance(newTokenTreasury).hasTokenBalance(VANILLA_TOKEN, 1), - getTokenInfo(VANILLA_TOKEN) - .hasTreasury(newTokenTreasury) - .hasPauseStatus(TokenPauseStatus.Unpaused) - .logged(), - getTokenNftInfo(VANILLA_TOKEN, 1) - .hasAccountID(newTokenTreasury) - .logged()); - } - - public HapiSpec updateWithTooLongNameAndSymbol() { - final var tooLongString = "ORIGINAL" + TxnUtils.randomUppercase(101); - final var tooLongSymbolTxn = "tooLongSymbolTxn"; - final AtomicReference vanillaTokenID = new AtomicReference<>(); - return defaultHapiSpec("updateWithTooLongNameAndSymbol") - .given( - cryptoCreate(TOKEN_TREASURY), - newKeyNamed(MULTI_KEY).shape(ED25519_ON), - cryptoCreate(ACCOUNT).key(MULTI_KEY).balance(ONE_MILLION_HBARS), - uploadInitCode(TOKEN_UPDATE_CONTRACT), - contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .initialSupply(1000) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .pauseKey(MULTI_KEY) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - tokenAssociate(ACCOUNT, VANILLA_TOKEN)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_UPDATE_CONTRACT, - "checkNameAndSymbolLength", - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - tooLongString, - CUSTOM_SYMBOL) - .via(UPDATE_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_UPDATE_CONTRACT, - "checkNameAndSymbolLength", - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - CUSTOM_NAME, - tooLongString) - .via(tooLongSymbolTxn) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) - .then(withOpContext((spec, opLog) -> allRunFor( - spec, - childRecordsCheck( - UPDATE_TXN, - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(TOKEN_NAME_TOO_LONG)), - childRecordsCheck( - tooLongSymbolTxn, - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(TOKEN_SYMBOL_TOO_LONG))))); - } - - private HapiSpec updateTokenWithKeysNegative() { - final var updateTokenWithKeysFunc = "updateTokenWithKeys"; - final var NO_FEE_SCHEDULE_KEY_TXN = "NO_FEE_SCHEDULE_KEY_TXN"; - final var NO_PAUSE_KEY_TXN = "NO_PAUSE_KEY_TXN"; - final var NO_KYC_KEY_TXN = "NO_KYC_KEY_TXN"; - final var NO_WIPE_KEY_TXN = "NO_WIPE_KEY_TXN"; - final var NO_FREEZE_KEY_TXN = "NO_FREEZE_KEY_TXN"; - final var NO_SUPPLY_KEY_TXN = "NO_SUPPLY_KEY_TXN"; - final List> tokenList = new ArrayList<>(); - final AtomicReference vanillaTokenID = new AtomicReference<>(); - return defaultHapiSpec("updateTokenWithKeysNegative") - .given( - newKeyNamed(ED25519KEY).shape(ED25519), - newKeyNamed(ECDSA_KEY).shape(SECP256K1), - newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), - newKeyNamed(MULTI_KEY).shape(ED25519_ON), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(ACCOUNT) - .balance(ONE_MILLION_HBARS) - .key(MULTI_KEY) - .maxAutomaticTokenAssociations(100), - cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), - uploadInitCode(TOKEN_UPDATE_CONTRACT), - contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .feeScheduleKey(MULTI_KEY) - .exposingCreatedIdTo(id -> tokenList.add(new AtomicReference<>(asToken(id)))), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .feeScheduleKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .exposingCreatedIdTo(id -> tokenList.add(new AtomicReference<>(asToken(id)))), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .feeScheduleKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .wipeKey(MULTI_KEY) - .exposingCreatedIdTo(id -> tokenList.add(new AtomicReference<>(asToken(id)))), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .pauseKey(MULTI_KEY) - .feeScheduleKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .wipeKey(MULTI_KEY) - .exposingCreatedIdTo(id -> tokenList.add(new AtomicReference<>(asToken(id)))), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .freezeKey(MULTI_KEY) - .adminKey(MULTI_KEY) - .pauseKey(MULTI_KEY) - .feeScheduleKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .wipeKey(MULTI_KEY) - .exposingCreatedIdTo(id -> tokenList.add(new AtomicReference<>(asToken(id))))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_UPDATE_CONTRACT, - updateTokenWithKeysFunc, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) - .via(NO_FEE_SCHEDULE_KEY_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_UPDATE_CONTRACT, - updateTokenWithKeysFunc, - HapiParserUtil.asHeadlongAddress( - asAddress(tokenList.get(0).get())), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) - .via(NO_SUPPLY_KEY_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_UPDATE_CONTRACT, - updateTokenWithKeysFunc, - HapiParserUtil.asHeadlongAddress( - asAddress(tokenList.get(1).get())), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) - .via(NO_WIPE_KEY_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_UPDATE_CONTRACT, - updateTokenWithKeysFunc, - HapiParserUtil.asHeadlongAddress( - asAddress(tokenList.get(2).get())), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) - .via(NO_PAUSE_KEY_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_UPDATE_CONTRACT, - updateTokenWithKeysFunc, - HapiParserUtil.asHeadlongAddress( - asAddress(tokenList.get(3).get())), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) - .via(NO_FREEZE_KEY_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_UPDATE_CONTRACT, - updateTokenWithKeysFunc, - HapiParserUtil.asHeadlongAddress( - asAddress(tokenList.get(4).get())), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) - .via(NO_KYC_KEY_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) - .then(withOpContext((spec, ignore) -> allRunFor( - spec, - childRecordsCheck( - NO_FEE_SCHEDULE_KEY_TXN, - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(TOKEN_HAS_NO_FEE_SCHEDULE_KEY)), - childRecordsCheck( - NO_SUPPLY_KEY_TXN, - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(TOKEN_HAS_NO_SUPPLY_KEY)), - childRecordsCheck( - NO_WIPE_KEY_TXN, - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(TOKEN_HAS_NO_WIPE_KEY)), - childRecordsCheck( - NO_PAUSE_KEY_TXN, - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(TOKEN_HAS_NO_PAUSE_KEY)), - childRecordsCheck( - NO_FREEZE_KEY_TXN, - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(TOKEN_HAS_NO_FREEZE_KEY)), - childRecordsCheck( - NO_KYC_KEY_TXN, - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(TOKEN_HAS_NO_KYC_KEY))))); + List negativeCases() { + return List.of( + updateTokenWithInvalidKeyValues(), + updateNftTokenKeysWithWrongTokenIdAndMissingAdminKey(), + getTokenKeyForNonFungibleNegative()); } private HapiSpec updateTokenWithInvalidKeyValues() { @@ -688,266 +161,7 @@ private HapiSpec updateTokenWithInvalidKeyValues() { .then(sourcing(() -> emptyChildRecordsCheck(UPDATE_TXN, CONTRACT_REVERT_EXECUTED))); } - private HapiSpec updateOnlyTokenKeysAndGetTheUpdatedValues() { - - final AtomicReference vanillaTokenID = new AtomicReference<>(); - return defaultHapiSpec("updateOnlyTokenKeysAndGetTheUpdatedValues") - .given( - newKeyNamed(ED25519KEY).shape(ED25519), - newKeyNamed(ECDSA_KEY).shape(SECP256K1), - newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), - newKeyNamed(MULTI_KEY).shape(ED25519_ON), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(MULTI_KEY), - cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), - uploadInitCode(TOKEN_UPDATE_CONTRACT), - contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .feeScheduleKey(MULTI_KEY) - .pauseKey(MULTI_KEY) - .wipeKey(MULTI_KEY) - .freezeKey(MULTI_KEY) - .kycKey(MULTI_KEY) - .initialSupply(1_000) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - tokenAssociate(ACCOUNT, VANILLA_TOKEN), - grantTokenKyc(VANILLA_TOKEN, ACCOUNT), - cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_UPDATE_CONTRACT, - UPDATE_KEY_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT), - newKeyNamed(DELEGATE_KEY).shape(DELEGATE_CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT)), - newKeyNamed(TOKEN_UPDATE_AS_KEY).shape(CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT)), - contractCall( - TOKEN_UPDATE_CONTRACT, - GET_KEY_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - BigInteger.valueOf(ADMIN_KEY_TYPE)) - .via(GET_ADMIN_KEY_TXN), - contractCall( - TOKEN_UPDATE_CONTRACT, - GET_KEY_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - BigInteger.valueOf(KYC_KEY_TYPE)) - .via(GET_KYC_KEY_TXN), - contractCall( - TOKEN_UPDATE_CONTRACT, - GET_KEY_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - BigInteger.valueOf(FREEZE_KEY_TYPE)) - .via(GET_FREEZE_KEY_TXN), - contractCall( - TOKEN_UPDATE_CONTRACT, - GET_KEY_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - BigInteger.valueOf(WIPE_KEY_TYPE)) - .via(GET_WIPE_KEY_TXN), - contractCall( - TOKEN_UPDATE_CONTRACT, - GET_KEY_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - BigInteger.valueOf(FEE_SCHEDULE_KEY_TYPE)) - .via(GET_FEE_KEY_TXN), - contractCall( - TOKEN_UPDATE_CONTRACT, - GET_KEY_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - BigInteger.valueOf(SUPPLY_KEY_TYPE)) - .via(GET_SUPPLY_KEY_TXN), - contractCall( - TOKEN_UPDATE_CONTRACT, - GET_KEY_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - BigInteger.valueOf(PAUSE_KEY_TYPE)) - .via(GET_PAUSE_KEY_TXN), - contractCallLocal( - TOKEN_UPDATE_CONTRACT, - GET_KEY_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - BigInteger.valueOf(ADMIN_KEY_TYPE))))) - .then(withOpContext((spec, opLog) -> allRunFor( - spec, - getTokenInfo(VANILLA_TOKEN) - .logged() - .hasTokenType(TokenType.FUNGIBLE_COMMON) - .hasSupplyType(TokenSupplyType.INFINITE) - .searchKeysGlobally() - .hasAdminKey(ED25519KEY) - .hasPauseKey(MULTI_KEY) - .hasKycKey(ED25519KEY) - .hasFreezeKey(ECDSA_KEY) - .hasWipeKey(ECDSA_KEY) - .hasFeeScheduleKey(DELEGATE_KEY) - .hasSupplyKey(TOKEN_UPDATE_AS_KEY) - .hasPauseKey(TOKEN_UPDATE_AS_KEY), - childRecordsCheck( - GET_ADMIN_KEY_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue( - spec.registry().getKey(ED25519KEY))))), - childRecordsCheck( - GET_KYC_KEY_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue( - spec.registry().getKey(ED25519KEY))))), - childRecordsCheck( - GET_FREEZE_KEY_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue( - spec.registry().getKey(ECDSA_KEY))))), - childRecordsCheck( - GET_WIPE_KEY_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue( - spec.registry().getKey(ECDSA_KEY))))), - childRecordsCheck( - GET_FEE_KEY_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue( - spec.registry().getKey(DELEGATE_KEY))))), - childRecordsCheck( - GET_SUPPLY_KEY_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue( - spec.registry().getKey(TOKEN_UPDATE_AS_KEY))))), - childRecordsCheck( - GET_PAUSE_KEY_TXN, - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS) - .withTokenKeyValue( - spec.registry().getKey(TOKEN_UPDATE_AS_KEY)))))))); - } - - public HapiSpec updateOnlyKeysForNonFungibleToken() { - final AtomicReference nftToken = new AtomicReference<>(); - return defaultHapiSpec("updateOnlyKeysForNonFungibleToken") - .given( - cryptoCreate(TOKEN_TREASURY), - newKeyNamed(ED25519KEY).shape(ED25519), - newKeyNamed(ECDSA_KEY).shape(SECP256K1), - newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), - newKeyNamed(MULTI_KEY).shape(ED25519_ON), - cryptoCreate(ACCOUNT).key(MULTI_KEY).balance(ONE_MILLION_HBARS), - cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), - uploadInitCode(TOKEN_UPDATE_CONTRACT), - contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), - tokenCreate(NFT_TOKEN) - .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .treasury(TOKEN_TREASURY) - .initialSupply(0) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .feeScheduleKey(MULTI_KEY) - .pauseKey(MULTI_KEY) - .wipeKey(MULTI_KEY) - .freezeKey(MULTI_KEY) - .kycKey(MULTI_KEY) - .exposingCreatedIdTo(id -> nftToken.set(asToken(id))), - mintToken(VANILLA_TOKEN, List.of(ByteString.copyFromUtf8("nft3"))), - tokenAssociate(ACCOUNT, VANILLA_TOKEN)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_UPDATE_CONTRACT, - UPDATE_KEY_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(nftToken.get())), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) - .via(UPDATE_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT), - newKeyNamed(DELEGATE_KEY).shape(DELEGATE_CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT)), - newKeyNamed(TOKEN_UPDATE_AS_KEY).shape(CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT))))) - .then(withOpContext((spec, opLog) -> allRunFor( - spec, - getTokenInfo(NFT_TOKEN) - .logged() - .hasTokenType(TokenType.NON_FUNGIBLE_UNIQUE) - .hasSupplyType(TokenSupplyType.INFINITE) - .searchKeysGlobally() - .hasAdminKey(ED25519KEY) - .hasPauseKey(MULTI_KEY) - .hasKycKey(ED25519KEY) - .hasFreezeKey(ECDSA_KEY) - .hasWipeKey(ECDSA_KEY) - .hasFeeScheduleKey(DELEGATE_KEY) - .hasSupplyKey(TOKEN_UPDATE_AS_KEY) - .hasPauseKey(TOKEN_UPDATE_AS_KEY)))); - } - - public HapiSpec updateNftTokenKeysWithWrongTokenIdAndMissingAdmin() { + public HapiSpec updateNftTokenKeysWithWrongTokenIdAndMissingAdminKey() { final AtomicReference nftToken = new AtomicReference<>(); return defaultHapiSpec("updateNftTokenKeysWithWrongTokenIdAndMissingAdminKey") .given( @@ -1081,113 +295,4 @@ public HapiSpec getTokenKeyForNonFungibleNegative() { CONTRACT_REVERT_EXECUTED, TransactionRecordAsserts.recordWith().status(KEY_NOT_PROVIDED))))); } - - private HapiSpec updateTokenWithoutNameSymbolMemo() { - final var updateTokenWithoutNameSymbolMemoFunc = "updateTokenWithoutNameSymbolMemo"; - final AtomicReference vanillaTokenID = new AtomicReference<>(); - return defaultHapiSpec(updateTokenWithoutNameSymbolMemoFunc) - .given( - newKeyNamed(ED25519KEY).shape(ED25519), - newKeyNamed(ECDSA_KEY).shape(SECP256K1), - newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), - newKeyNamed(MULTI_KEY).shape(ED25519_ON), - cryptoCreate(TOKEN_TREASURY), - cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(MULTI_KEY), - cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), - uploadInitCode(TOKEN_UPDATE_CONTRACT), - contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), - tokenCreate(VANILLA_TOKEN) - .symbol(CUSTOM_SYMBOL) - .entityMemo(CUSTOM_MEMO) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .feeScheduleKey(MULTI_KEY) - .pauseKey(MULTI_KEY) - .wipeKey(MULTI_KEY) - .freezeKey(MULTI_KEY) - .kycKey(MULTI_KEY) - .initialSupply(1_000) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - tokenAssociate(ACCOUNT, VANILLA_TOKEN), - grantTokenKyc(VANILLA_TOKEN, ACCOUNT), - cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - TOKEN_UPDATE_CONTRACT, - updateTokenWithoutNameSymbolMemoFunc, - HapiParserUtil.asHeadlongAddress(new byte[20]), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - AUTO_RENEW_PERIOD) - .via(UPDATE_TXN) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - TOKEN_UPDATE_CONTRACT, - "updateTokenWithoutNameSymbolMemo", - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - spec.registry() - .getKey(ED25519KEY) - .getEd25519() - .toByteArray(), - spec.registry() - .getKey(ECDSA_KEY) - .getECDSASecp256K1() - .toByteArray(), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT))), - HapiParserUtil.asHeadlongAddress( - asAddress(spec.registry().getAccountID(ACCOUNT))), - AUTO_RENEW_PERIOD) - .gas(GAS_TO_OFFER) - .sending(DEFAULT_AMOUNT_TO_SEND) - .signedBy(GENESIS, ACCOUNT) - .alsoSigningWithFullPrefix(ACCOUNT), - newKeyNamed(DELEGATE_KEY).shape(DELEGATE_CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT)), - newKeyNamed(TOKEN_UPDATE_AS_KEY).shape(CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT))))) - .then( - childRecordsCheck( - UPDATE_TXN, - CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(INVALID_TOKEN_ID)), - sourcing(() -> getTokenInfo(VANILLA_TOKEN) - .logged() - .hasTokenType(TokenType.FUNGIBLE_COMMON) - .hasSymbol(CUSTOM_SYMBOL) - .hasName(VANILLA_TOKEN) - .hasEntityMemo(CUSTOM_MEMO) - .hasTreasury(ACCOUNT) - .hasAutoRenewAccount(ACCOUNT) - .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) - .hasSupplyType(TokenSupplyType.INFINITE) - .searchKeysGlobally() - .hasAdminKey(ED25519KEY) - .hasPauseKey(MULTI_KEY) - .hasKycKey(ED25519KEY) - .hasFreezeKey(ECDSA_KEY) - .hasWipeKey(ECDSA_KEY) - .hasFeeScheduleKey(DELEGATE_KEY) - .hasSupplyKey(TOKEN_UPDATE_AS_KEY) - .hasPauseKey(TOKEN_UPDATE_AS_KEY))); - } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenUpdatePrecompileV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenUpdatePrecompileV1SecurityModelSuite.java new file mode 100644 index 000000000000..9b9feb7c2218 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenUpdatePrecompileV1SecurityModelSuite.java @@ -0,0 +1,1021 @@ +/* + * Copyright (C) 2022-2023 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.spec.HapiSpec.propertyPreservingHapiSpec; +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.keys.KeyShape.CONTRACT; +import static com.hedera.services.bdd.spec.keys.KeyShape.DELEGATE_CONTRACT; +import static com.hedera.services.bdd.spec.keys.KeyShape.ED25519; +import static com.hedera.services.bdd.spec.keys.KeyShape.SECP256K1; +import static com.hedera.services.bdd.spec.keys.SigControl.ED25519_ON; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.contractCallLocal; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenNftInfo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +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.grantTokenKyc; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; +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.spec.transactions.TxnVerbs.uploadInitCode; +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.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.asToken; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_TOKEN_ID; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_FEE_SCHEDULE_KEY; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_FREEZE_KEY; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_KYC_KEY; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_PAUSE_KEY; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_SUPPLY_KEY; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_WIPE_KEY; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_IS_IMMUTABLE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_NAME_TOO_LONG; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_SYMBOL_TOO_LONG; +import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; + +import com.google.protobuf.ByteString; +import com.hedera.node.app.hapi.utils.contracts.ParsingConstants; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts; +import com.hedera.services.bdd.spec.transactions.TxnUtils; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.TokenID; +import com.hederahashgraph.api.proto.java.TokenPauseStatus; +import com.hederahashgraph.api.proto.java.TokenSupplyType; +import com.hederahashgraph.api.proto.java.TokenType; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse +public class TokenUpdatePrecompileV1SecurityModelSuite extends HapiSuite { + + private static final Logger log = LogManager.getLogger(TokenUpdatePrecompileV1SecurityModelSuite.class); + + private static final long GAS_TO_OFFER = 4_000_000L; + private static final long AUTO_RENEW_PERIOD = 8_000_000L; + private static final String ACCOUNT = "account"; + private static final String VANILLA_TOKEN = "TokenD"; + private static final String NFT_TOKEN = "TokenD"; + private static final String MULTI_KEY = "multiKey"; + private static final String UPDATE_KEY_FUNC = "tokenUpdateKeys"; + private static final String GET_KEY_FUNC = "getKeyFromToken"; + public static final String TOKEN_UPDATE_CONTRACT = "UpdateTokenInfoContract"; + private static final String UPDATE_TXN = "updateTxn"; + private static final String GET_KYC_KEY_TXN = "getKycTokenKeyTxn"; + private static final String GET_ADMIN_KEY_TXN = "getAdminTokenKeyTxn"; + private static final String GET_PAUSE_KEY_TXN = "getPauseTokenKeyTxn"; + private static final String GET_FREEZE_KEY_TXN = "getFreezeTokenKeyTxn"; + private static final String GET_WIPE_KEY_TXN = "getWipeTokenKeyTxn"; + private static final String GET_FEE_KEY_TXN = "getFeeTokenKeyTxn"; + private static final String GET_SUPPLY_KEY_TXN = "getSupplyTokenKeyTxn"; + private static final long DEFAULT_AMOUNT_TO_SEND = 20 * ONE_HBAR; + private static final String ED25519KEY = "ed25519key"; + private static final String ECDSA_KEY = "ecdsa"; + public static final String TOKEN_UPDATE_AS_KEY = "tokenCreateContractAsKey"; + private static final String DELEGATE_KEY = "tokenUpdateAsKeyDelegate"; + private static final String ACCOUNT_TO_ASSOCIATE = "account3"; + private static final String ACCOUNT_TO_ASSOCIATE_KEY = "associateKey"; + public static final String CUSTOM_NAME = "customName"; + public static final String CUSTOM_SYMBOL = "Ω"; + public static final String CUSTOM_MEMO = "Omega"; + private static final long ADMIN_KEY_TYPE = 1L; + private static final long KYC_KEY_TYPE = 2L; + private static final long FREEZE_KEY_TYPE = 4L; + private static final long WIPE_KEY_TYPE = 8L; + private static final long SUPPLY_KEY_TYPE = 16L; + private static final long FEE_SCHEDULE_KEY_TYPE = 32L; + private static final long PAUSE_KEY_TYPE = 64L; + + public static void main(String... args) { + new TokenUpdatePrecompileV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + protected Logger getResultsLogger() { + return log; + } + + @Override + public List getSpecsInSuite() { + return allOf(positiveCases(), negativeCases()); + } + + List positiveCases() { + return List.of( + updateTokenWithKeysHappyPath(), + updateNftTreasuryWithAndWithoutAdminKey(), + updateOnlyTokenKeysAndGetTheUpdatedValues(), + updateOnlyKeysForNonFungibleToken(), + updateTokenWithoutNameSymbolMemo()); + } + + List negativeCases() { + return List.of(updateWithTooLongNameAndSymbol(), updateTokenWithKeysNegative()); + } + + private HapiSpec updateTokenWithKeysHappyPath() { + final AtomicReference vanillaTokenID = new AtomicReference<>(); + return propertyPreservingHapiSpec("updateTokenWithKeysHappyPath") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ED25519KEY).shape(ED25519), + newKeyNamed(ECDSA_KEY).shape(SECP256K1), + newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), + newKeyNamed(MULTI_KEY).shape(ED25519_ON), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(MULTI_KEY), + cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), + uploadInitCode(TOKEN_UPDATE_CONTRACT), + contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .feeScheduleKey(MULTI_KEY) + .pauseKey(MULTI_KEY) + .wipeKey(MULTI_KEY) + .freezeKey(MULTI_KEY) + .kycKey(MULTI_KEY) + .initialSupply(1_000) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + grantTokenKyc(VANILLA_TOKEN, ACCOUNT), + cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_UPDATE_CONTRACT, + "updateTokenWithAllFields", + HapiParserUtil.asHeadlongAddress(new byte[20]), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD, + CUSTOM_NAME, + CUSTOM_SYMBOL, + CUSTOM_MEMO) + .via(UPDATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_UPDATE_CONTRACT, + "updateTokenWithAllFields", + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD, + CUSTOM_NAME, + CUSTOM_SYMBOL, + CUSTOM_MEMO) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT), + newKeyNamed(DELEGATE_KEY).shape(DELEGATE_CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT)), + newKeyNamed(TOKEN_UPDATE_AS_KEY).shape(CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT))))) + .then( + childRecordsCheck( + UPDATE_TXN, + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith().status(INVALID_TOKEN_ID)), + sourcing(() -> getTokenInfo(VANILLA_TOKEN) + .logged() + .hasTokenType(TokenType.FUNGIBLE_COMMON) + .hasSymbol(CUSTOM_SYMBOL) + .hasName(CUSTOM_NAME) + .hasEntityMemo(CUSTOM_MEMO) + .hasTreasury(ACCOUNT) + .hasAutoRenewAccount(ACCOUNT) + .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) + .hasSupplyType(TokenSupplyType.INFINITE) + .searchKeysGlobally() + .hasAdminKey(ED25519KEY) + .hasPauseKey(MULTI_KEY) + .hasKycKey(ED25519KEY) + .hasFreezeKey(ECDSA_KEY) + .hasWipeKey(ECDSA_KEY) + .hasFeeScheduleKey(DELEGATE_KEY) + .hasSupplyKey(TOKEN_UPDATE_AS_KEY) + .hasPauseKey(TOKEN_UPDATE_AS_KEY))); + } + + public HapiSpec updateNftTreasuryWithAndWithoutAdminKey() { + final var newTokenTreasury = "newTokenTreasury"; + final var NO_ADMIN_TOKEN = "noAdminKeyToken"; + final AtomicReference noAdminKeyToken = new AtomicReference<>(); + final AtomicReference nftToken = new AtomicReference<>(); + return propertyPreservingHapiSpec("updateNftTreasuryWithAndWithoutAdminKey") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(newTokenTreasury).keyShape(ED25519_ON).maxAutomaticTokenAssociations(6), + newKeyNamed(MULTI_KEY).shape(ED25519_ON), + cryptoCreate(ACCOUNT).key(MULTI_KEY).balance(ONE_MILLION_HBARS), + uploadInitCode(TOKEN_UPDATE_CONTRACT), + contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), + tokenCreate(NO_ADMIN_TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .treasury(TOKEN_TREASURY) + .initialSupply(0) + .supplyKey(MULTI_KEY) + .exposingCreatedIdTo(id -> noAdminKeyToken.set(asToken(id))), + tokenCreate(VANILLA_TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .treasury(TOKEN_TREASURY) + .initialSupply(0) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .pauseKey(MULTI_KEY) + .exposingCreatedIdTo(id -> nftToken.set(asToken(id))), + mintToken(VANILLA_TOKEN, List.of(ByteString.copyFromUtf8("nft0"))), + tokenAssociate(newTokenTreasury, VANILLA_TOKEN), + mintToken(NO_ADMIN_TOKEN, List.of(ByteString.copyFromUtf8("nft1")))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_UPDATE_CONTRACT, + "updateTokenTreasury", + HapiParserUtil.asHeadlongAddress(asAddress(noAdminKeyToken.get())), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(newTokenTreasury)))) + .via("noAdminKey") + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT, newTokenTreasury) + .alsoSigningWithFullPrefix(ACCOUNT, newTokenTreasury) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_UPDATE_CONTRACT, + "updateTokenTreasury", + HapiParserUtil.asHeadlongAddress(asAddress(nftToken.get())), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(newTokenTreasury)))) + .via("tokenUpdateTxn") + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT, newTokenTreasury) + .alsoSigningWithFullPrefix(ACCOUNT, newTokenTreasury)))) + .then( + childRecordsCheck( + "noAdminKey", + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith().status(TOKEN_IS_IMMUTABLE)), + getTokenNftInfo(VANILLA_TOKEN, 1) + .hasAccountID(newTokenTreasury) + .logged(), + getAccountBalance(TOKEN_TREASURY).hasTokenBalance(VANILLA_TOKEN, 0), + getAccountBalance(newTokenTreasury).hasTokenBalance(VANILLA_TOKEN, 1), + getTokenInfo(VANILLA_TOKEN) + .hasTreasury(newTokenTreasury) + .hasPauseStatus(TokenPauseStatus.Unpaused) + .logged(), + getTokenNftInfo(VANILLA_TOKEN, 1) + .hasAccountID(newTokenTreasury) + .logged()); + } + + public HapiSpec updateWithTooLongNameAndSymbol() { + final var tooLongString = "ORIGINAL" + TxnUtils.randomUppercase(101); + final var tooLongSymbolTxn = "tooLongSymbolTxn"; + final AtomicReference vanillaTokenID = new AtomicReference<>(); + return propertyPreservingHapiSpec("updateWithTooLongNameAndSymbol") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(TOKEN_TREASURY), + newKeyNamed(MULTI_KEY).shape(ED25519_ON), + cryptoCreate(ACCOUNT).key(MULTI_KEY).balance(ONE_MILLION_HBARS), + uploadInitCode(TOKEN_UPDATE_CONTRACT), + contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .initialSupply(1000) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .pauseKey(MULTI_KEY) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + tokenAssociate(ACCOUNT, VANILLA_TOKEN)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_UPDATE_CONTRACT, + "checkNameAndSymbolLength", + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + tooLongString, + CUSTOM_SYMBOL) + .via(UPDATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_UPDATE_CONTRACT, + "checkNameAndSymbolLength", + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + CUSTOM_NAME, + tooLongString) + .via(tooLongSymbolTxn) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) + .then(withOpContext((spec, opLog) -> allRunFor( + spec, + childRecordsCheck( + UPDATE_TXN, + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith().status(TOKEN_NAME_TOO_LONG)), + childRecordsCheck( + tooLongSymbolTxn, + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith().status(TOKEN_SYMBOL_TOO_LONG))))); + } + + private HapiSpec updateTokenWithKeysNegative() { + final var updateTokenWithKeysFunc = "updateTokenWithKeys"; + final var NO_FEE_SCHEDULE_KEY_TXN = "NO_FEE_SCHEDULE_KEY_TXN"; + final var NO_PAUSE_KEY_TXN = "NO_PAUSE_KEY_TXN"; + final var NO_KYC_KEY_TXN = "NO_KYC_KEY_TXN"; + final var NO_WIPE_KEY_TXN = "NO_WIPE_KEY_TXN"; + final var NO_FREEZE_KEY_TXN = "NO_FREEZE_KEY_TXN"; + final var NO_SUPPLY_KEY_TXN = "NO_SUPPLY_KEY_TXN"; + final List> tokenList = new ArrayList<>(); + final AtomicReference vanillaTokenID = new AtomicReference<>(); + return propertyPreservingHapiSpec("updateTokenWithKeysNegative") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ED25519KEY).shape(ED25519), + newKeyNamed(ECDSA_KEY).shape(SECP256K1), + newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), + newKeyNamed(MULTI_KEY).shape(ED25519_ON), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(ACCOUNT) + .balance(ONE_MILLION_HBARS) + .key(MULTI_KEY) + .maxAutomaticTokenAssociations(100), + cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), + uploadInitCode(TOKEN_UPDATE_CONTRACT), + contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .feeScheduleKey(MULTI_KEY) + .exposingCreatedIdTo(id -> tokenList.add(new AtomicReference<>(asToken(id)))), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .feeScheduleKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .exposingCreatedIdTo(id -> tokenList.add(new AtomicReference<>(asToken(id)))), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .feeScheduleKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .wipeKey(MULTI_KEY) + .exposingCreatedIdTo(id -> tokenList.add(new AtomicReference<>(asToken(id)))), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .pauseKey(MULTI_KEY) + .feeScheduleKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .wipeKey(MULTI_KEY) + .exposingCreatedIdTo(id -> tokenList.add(new AtomicReference<>(asToken(id)))), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .freezeKey(MULTI_KEY) + .adminKey(MULTI_KEY) + .pauseKey(MULTI_KEY) + .feeScheduleKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .wipeKey(MULTI_KEY) + .exposingCreatedIdTo(id -> tokenList.add(new AtomicReference<>(asToken(id))))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_UPDATE_CONTRACT, + updateTokenWithKeysFunc, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) + .via(NO_FEE_SCHEDULE_KEY_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_UPDATE_CONTRACT, + updateTokenWithKeysFunc, + HapiParserUtil.asHeadlongAddress( + asAddress(tokenList.get(0).get())), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) + .via(NO_SUPPLY_KEY_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_UPDATE_CONTRACT, + updateTokenWithKeysFunc, + HapiParserUtil.asHeadlongAddress( + asAddress(tokenList.get(1).get())), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) + .via(NO_WIPE_KEY_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_UPDATE_CONTRACT, + updateTokenWithKeysFunc, + HapiParserUtil.asHeadlongAddress( + asAddress(tokenList.get(2).get())), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) + .via(NO_PAUSE_KEY_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_UPDATE_CONTRACT, + updateTokenWithKeysFunc, + HapiParserUtil.asHeadlongAddress( + asAddress(tokenList.get(3).get())), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) + .via(NO_FREEZE_KEY_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_UPDATE_CONTRACT, + updateTokenWithKeysFunc, + HapiParserUtil.asHeadlongAddress( + asAddress(tokenList.get(4).get())), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) + .via(NO_KYC_KEY_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) + .then(withOpContext((spec, ignore) -> allRunFor( + spec, + childRecordsCheck( + NO_FEE_SCHEDULE_KEY_TXN, + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith().status(TOKEN_HAS_NO_FEE_SCHEDULE_KEY)), + childRecordsCheck( + NO_SUPPLY_KEY_TXN, + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith().status(TOKEN_HAS_NO_SUPPLY_KEY)), + childRecordsCheck( + NO_WIPE_KEY_TXN, + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith().status(TOKEN_HAS_NO_WIPE_KEY)), + childRecordsCheck( + NO_PAUSE_KEY_TXN, + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith().status(TOKEN_HAS_NO_PAUSE_KEY)), + childRecordsCheck( + NO_FREEZE_KEY_TXN, + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith().status(TOKEN_HAS_NO_FREEZE_KEY)), + childRecordsCheck( + NO_KYC_KEY_TXN, + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith().status(TOKEN_HAS_NO_KYC_KEY))))); + } + + private HapiSpec updateOnlyTokenKeysAndGetTheUpdatedValues() { + + final AtomicReference vanillaTokenID = new AtomicReference<>(); + return propertyPreservingHapiSpec("updateOnlyTokenKeysAndGetTheUpdatedValues") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ED25519KEY).shape(ED25519), + newKeyNamed(ECDSA_KEY).shape(SECP256K1), + newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), + newKeyNamed(MULTI_KEY).shape(ED25519_ON), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(MULTI_KEY), + cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), + uploadInitCode(TOKEN_UPDATE_CONTRACT), + contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .feeScheduleKey(MULTI_KEY) + .pauseKey(MULTI_KEY) + .wipeKey(MULTI_KEY) + .freezeKey(MULTI_KEY) + .kycKey(MULTI_KEY) + .initialSupply(1_000) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + grantTokenKyc(VANILLA_TOKEN, ACCOUNT), + cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_UPDATE_CONTRACT, + UPDATE_KEY_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT), + newKeyNamed(DELEGATE_KEY).shape(DELEGATE_CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT)), + newKeyNamed(TOKEN_UPDATE_AS_KEY).shape(CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT)), + contractCall( + TOKEN_UPDATE_CONTRACT, + GET_KEY_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + BigInteger.valueOf(ADMIN_KEY_TYPE)) + .via(GET_ADMIN_KEY_TXN), + contractCall( + TOKEN_UPDATE_CONTRACT, + GET_KEY_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + BigInteger.valueOf(KYC_KEY_TYPE)) + .via(GET_KYC_KEY_TXN), + contractCall( + TOKEN_UPDATE_CONTRACT, + GET_KEY_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + BigInteger.valueOf(FREEZE_KEY_TYPE)) + .via(GET_FREEZE_KEY_TXN), + contractCall( + TOKEN_UPDATE_CONTRACT, + GET_KEY_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + BigInteger.valueOf(WIPE_KEY_TYPE)) + .via(GET_WIPE_KEY_TXN), + contractCall( + TOKEN_UPDATE_CONTRACT, + GET_KEY_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + BigInteger.valueOf(FEE_SCHEDULE_KEY_TYPE)) + .via(GET_FEE_KEY_TXN), + contractCall( + TOKEN_UPDATE_CONTRACT, + GET_KEY_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + BigInteger.valueOf(SUPPLY_KEY_TYPE)) + .via(GET_SUPPLY_KEY_TXN), + contractCall( + TOKEN_UPDATE_CONTRACT, + GET_KEY_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + BigInteger.valueOf(PAUSE_KEY_TYPE)) + .via(GET_PAUSE_KEY_TXN), + contractCallLocal( + TOKEN_UPDATE_CONTRACT, + GET_KEY_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + BigInteger.valueOf(ADMIN_KEY_TYPE))))) + .then(withOpContext((spec, opLog) -> allRunFor( + spec, + getTokenInfo(VANILLA_TOKEN) + .logged() + .hasTokenType(TokenType.FUNGIBLE_COMMON) + .hasSupplyType(TokenSupplyType.INFINITE) + .searchKeysGlobally() + .hasAdminKey(ED25519KEY) + .hasPauseKey(MULTI_KEY) + .hasKycKey(ED25519KEY) + .hasFreezeKey(ECDSA_KEY) + .hasWipeKey(ECDSA_KEY) + .hasFeeScheduleKey(DELEGATE_KEY) + .hasSupplyKey(TOKEN_UPDATE_AS_KEY) + .hasPauseKey(TOKEN_UPDATE_AS_KEY), + childRecordsCheck( + GET_ADMIN_KEY_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue( + spec.registry().getKey(ED25519KEY))))), + childRecordsCheck( + GET_KYC_KEY_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue( + spec.registry().getKey(ED25519KEY))))), + childRecordsCheck( + GET_FREEZE_KEY_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue( + spec.registry().getKey(ECDSA_KEY))))), + childRecordsCheck( + GET_WIPE_KEY_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue( + spec.registry().getKey(ECDSA_KEY))))), + childRecordsCheck( + GET_FEE_KEY_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue( + spec.registry().getKey(DELEGATE_KEY))))), + childRecordsCheck( + GET_SUPPLY_KEY_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue( + spec.registry().getKey(TOKEN_UPDATE_AS_KEY))))), + childRecordsCheck( + GET_PAUSE_KEY_TXN, + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(ParsingConstants.FunctionType.HAPI_GET_TOKEN_KEY) + .withStatus(SUCCESS) + .withTokenKeyValue( + spec.registry().getKey(TOKEN_UPDATE_AS_KEY)))))))); + } + + public HapiSpec updateOnlyKeysForNonFungibleToken() { + final AtomicReference nftToken = new AtomicReference<>(); + return propertyPreservingHapiSpec("updateOnlyKeysForNonFungibleToken") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(TOKEN_TREASURY), + newKeyNamed(ED25519KEY).shape(ED25519), + newKeyNamed(ECDSA_KEY).shape(SECP256K1), + newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), + newKeyNamed(MULTI_KEY).shape(ED25519_ON), + cryptoCreate(ACCOUNT).key(MULTI_KEY).balance(ONE_MILLION_HBARS), + cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), + uploadInitCode(TOKEN_UPDATE_CONTRACT), + contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), + tokenCreate(NFT_TOKEN) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .treasury(TOKEN_TREASURY) + .initialSupply(0) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .feeScheduleKey(MULTI_KEY) + .pauseKey(MULTI_KEY) + .wipeKey(MULTI_KEY) + .freezeKey(MULTI_KEY) + .kycKey(MULTI_KEY) + .exposingCreatedIdTo(id -> nftToken.set(asToken(id))), + mintToken(VANILLA_TOKEN, List.of(ByteString.copyFromUtf8("nft3"))), + tokenAssociate(ACCOUNT, VANILLA_TOKEN)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_UPDATE_CONTRACT, + UPDATE_KEY_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(nftToken.get())), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT)))) + .via(UPDATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT), + newKeyNamed(DELEGATE_KEY).shape(DELEGATE_CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT)), + newKeyNamed(TOKEN_UPDATE_AS_KEY).shape(CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT))))) + .then(withOpContext((spec, opLog) -> allRunFor( + spec, + getTokenInfo(NFT_TOKEN) + .logged() + .hasTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .hasSupplyType(TokenSupplyType.INFINITE) + .searchKeysGlobally() + .hasAdminKey(ED25519KEY) + .hasPauseKey(MULTI_KEY) + .hasKycKey(ED25519KEY) + .hasFreezeKey(ECDSA_KEY) + .hasWipeKey(ECDSA_KEY) + .hasFeeScheduleKey(DELEGATE_KEY) + .hasSupplyKey(TOKEN_UPDATE_AS_KEY) + .hasPauseKey(TOKEN_UPDATE_AS_KEY)))); + } + + private HapiSpec updateTokenWithoutNameSymbolMemo() { + final var updateTokenWithoutNameSymbolMemoFunc = "updateTokenWithoutNameSymbolMemo"; + final AtomicReference vanillaTokenID = new AtomicReference<>(); + return propertyPreservingHapiSpec("updateTokenWithoutNameSymbolMemo") + .preserving(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(ED25519KEY).shape(ED25519), + newKeyNamed(ECDSA_KEY).shape(SECP256K1), + newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), + newKeyNamed(MULTI_KEY).shape(ED25519_ON), + cryptoCreate(TOKEN_TREASURY), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(MULTI_KEY), + cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), + uploadInitCode(TOKEN_UPDATE_CONTRACT), + contractCreate(TOKEN_UPDATE_CONTRACT).gas(GAS_TO_OFFER), + tokenCreate(VANILLA_TOKEN) + .symbol(CUSTOM_SYMBOL) + .entityMemo(CUSTOM_MEMO) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .feeScheduleKey(MULTI_KEY) + .pauseKey(MULTI_KEY) + .wipeKey(MULTI_KEY) + .freezeKey(MULTI_KEY) + .kycKey(MULTI_KEY) + .initialSupply(1_000) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + grantTokenKyc(VANILLA_TOKEN, ACCOUNT), + cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_UPDATE_CONTRACT, + updateTokenWithoutNameSymbolMemoFunc, + HapiParserUtil.asHeadlongAddress(new byte[20]), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD) + .via(UPDATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + TOKEN_UPDATE_CONTRACT, + "updateTokenWithoutNameSymbolMemo", + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_UPDATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .signedBy(GENESIS, ACCOUNT) + .alsoSigningWithFullPrefix(ACCOUNT), + newKeyNamed(DELEGATE_KEY).shape(DELEGATE_CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT)), + newKeyNamed(TOKEN_UPDATE_AS_KEY).shape(CONTRACT.signedWith(TOKEN_UPDATE_CONTRACT))))) + .then( + childRecordsCheck( + UPDATE_TXN, + CONTRACT_REVERT_EXECUTED, + TransactionRecordAsserts.recordWith().status(INVALID_TOKEN_ID)), + sourcing(() -> getTokenInfo(VANILLA_TOKEN) + .logged() + .hasTokenType(TokenType.FUNGIBLE_COMMON) + .hasSymbol(CUSTOM_SYMBOL) + .hasName(VANILLA_TOKEN) + .hasEntityMemo(CUSTOM_MEMO) + .hasTreasury(ACCOUNT) + .hasAutoRenewAccount(ACCOUNT) + .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) + .hasSupplyType(TokenSupplyType.INFINITE) + .searchKeysGlobally() + .hasAdminKey(ED25519KEY) + .hasPauseKey(MULTI_KEY) + .hasKycKey(ED25519KEY) + .hasFreezeKey(ECDSA_KEY) + .hasWipeKey(ECDSA_KEY) + .hasFeeScheduleKey(DELEGATE_KEY) + .hasSupplyKey(TOKEN_UPDATE_AS_KEY) + .hasPauseKey(TOKEN_UPDATE_AS_KEY))); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TopLevelSigsCanBeToggledByPrecompileTypeSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TopLevelSigsCanBeToggledByPrecompileTypeSuite.java index 98cc2ebdf38e..09818deba79c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TopLevelSigsCanBeToggledByPrecompileTypeSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TopLevelSigsCanBeToggledByPrecompileTypeSuite.java @@ -58,13 +58,10 @@ import static com.hedera.services.bdd.suites.contract.precompile.CreatePrecompileSuite.ECDSA_KEY; import static com.hedera.services.bdd.suites.contract.precompile.CreatePrecompileSuite.ED25519KEY; import static com.hedera.services.bdd.suites.contract.precompile.CryptoTransferHTSSuite.DELEGATE_KEY; -import static com.hedera.services.bdd.suites.contract.precompile.DeleteTokenPrecompileSuite.DELETE_TOKEN_CONTRACT; -import static com.hedera.services.bdd.suites.contract.precompile.DeleteTokenPrecompileSuite.TOKEN_DELETE_FUNCTION; import static com.hedera.services.bdd.suites.contract.precompile.FreezeUnfreezeTokenPrecompileSuite.FREEZE_CONTRACT; import static com.hedera.services.bdd.suites.contract.precompile.FreezeUnfreezeTokenPrecompileSuite.TOKEN_FREEZE_FUNC; import static com.hedera.services.bdd.suites.contract.precompile.FreezeUnfreezeTokenPrecompileSuite.TOKEN_UNFREEZE_FUNC; import static com.hedera.services.bdd.suites.contract.precompile.GrantRevokeKycSuite.GRANT_REVOKE_KYC_CONTRACT; -import static com.hedera.services.bdd.suites.contract.precompile.GrantRevokeKycSuite.SECOND_ACCOUNT; import static com.hedera.services.bdd.suites.contract.precompile.GrantRevokeKycSuite.TOKEN_GRANT_KYC; import static com.hedera.services.bdd.suites.contract.precompile.GrantRevokeKycSuite.TOKEN_REVOKE_KYC; import static com.hedera.services.bdd.suites.contract.precompile.PauseUnpauseTokenAccountPrecompileSuite.PAUSE_TOKEN_ACCOUNT_FUNCTION_NAME; @@ -75,16 +72,10 @@ import static com.hedera.services.bdd.suites.contract.precompile.TokenUpdatePrecompileSuite.CUSTOM_SYMBOL; import static com.hedera.services.bdd.suites.contract.precompile.TokenUpdatePrecompileSuite.TOKEN_UPDATE_AS_KEY; import static com.hedera.services.bdd.suites.contract.precompile.TokenUpdatePrecompileSuite.TOKEN_UPDATE_CONTRACT; -import static com.hedera.services.bdd.suites.contract.precompile.WipeTokenAccountPrecompileSuite.ADMIN_ACCOUNT; -import static com.hedera.services.bdd.suites.contract.precompile.WipeTokenAccountPrecompileSuite.GAS_TO_OFFER; -import static com.hedera.services.bdd.suites.contract.precompile.WipeTokenAccountPrecompileSuite.WIPE_CONTRACT; -import static com.hedera.services.bdd.suites.contract.precompile.WipeTokenAccountPrecompileSuite.WIPE_FUNGIBLE_TOKEN; -import static com.hedera.services.bdd.suites.contract.precompile.WipeTokenAccountPrecompileSuite.WIPE_KEY; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; import static com.hedera.services.bdd.suites.crypto.CryptoApproveAllowanceSuite.FREEZE_KEY; import static com.hedera.services.bdd.suites.crypto.CryptoApproveAllowanceSuite.KYC_KEY; import static com.hedera.services.bdd.suites.crypto.CryptoApproveAllowanceSuite.PAUSE_KEY; -import static com.hedera.services.bdd.suites.crypto.CryptoCreateSuite.ACCOUNT; -import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.MULTI_KEY; import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; import static com.hedera.services.bdd.suites.token.TokenTransactSpecs.SUPPLY_KEY; import static com.hedera.services.yahcli.commands.validation.ValidationCommand.TOKEN; @@ -108,9 +99,20 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse public class TopLevelSigsCanBeToggledByPrecompileTypeSuite extends HapiSuite { private static final Logger log = LogManager.getLogger(TopLevelSigsCanBeToggledByPrecompileTypeSuite.class); - private static final String CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS = "contracts.allowSystemUseOfHapiSigs"; + + public static final String DELETE_TOKEN_CONTRACT = "DeleteTokenContract"; + public static final String TOKEN_DELETE_FUNCTION = "tokenDelete"; + public static final String WIPE_CONTRACT = "WipeTokenAccount"; + public static final String ADMIN_ACCOUNT = "admin"; + private static final String ACCOUNT = "anybody"; + private static final String SECOND_ACCOUNT = "anybodySecond"; + public static final String WIPE_KEY = "wipeKey"; + private static final String MULTI_KEY = "purpose"; + public static final int GAS_TO_OFFER = 1_000_000; + public static final String WIPE_FUNGIBLE_TOKEN = "wipeFungibleToken"; public static void main(String... args) { new TopLevelSigsCanBeToggledByPrecompileTypeSuite().runSuiteSync(); @@ -131,7 +133,6 @@ public List getSpecsInSuite() { } private HapiSpec canToggleTopLevelSigUsageForWipePrecompile() { - final String ALLOW_SYSTEM_USE_OF_HAPI_SIGS = CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; final var failedWipeTxn = "failedWipeTxn"; final var succeededWipeTxn = "succeededWipeTxn"; @@ -139,7 +140,7 @@ private HapiSpec canToggleTopLevelSigUsageForWipePrecompile() { final AtomicReference accountID = new AtomicReference<>(); final AtomicReference vanillaTokenID = new AtomicReference<>(); return propertyPreservingHapiSpec("CanToggleTopLevelSigUsageForWipePrecompile") - .preserving(ALLOW_SYSTEM_USE_OF_HAPI_SIGS) + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS) .given( newKeyNamed(WIPE_KEY), cryptoCreate(ADMIN_ACCOUNT).exposingCreatedIdTo(adminAccountID::set), @@ -156,7 +157,7 @@ private HapiSpec canToggleTopLevelSigUsageForWipePrecompile() { tokenAssociate(ACCOUNT, VANILLA_TOKEN), cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT)), // First revoke use of top-level signatures from all precompiles - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) .when( // Trying to wipe token with top-level signatures should fail sourcing(() -> contractCall( @@ -171,7 +172,7 @@ private HapiSpec canToggleTopLevelSigUsageForWipePrecompile() { .gas(GAS_TO_OFFER) .hasKnownStatus(CONTRACT_REVERT_EXECUTED)), // But now restore use of top-level signatures for the token wipe precompile - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenAccountWipe"), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenAccountWipe"), // Now the same call should succeed sourcing(() -> contractCall( WIPE_CONTRACT, @@ -193,13 +194,12 @@ private HapiSpec canToggleTopLevelSigUsageForWipePrecompile() { } private HapiSpec canToggleTopLevelSigUsageForUpdatePrecompile() { - final String ALLOW_SYSTEM_USE_OF_HAPI_SIGS = CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; final var failedUpdateTxn = "failedUpdateTxn"; final var succeededUpdateTxn = "succeededUpdateTxn"; final AtomicReference vanillaTokenID = new AtomicReference<>(); return propertyPreservingHapiSpec("CanToggleTopLevelSigUsageForWipePrecompile") - .preserving(ALLOW_SYSTEM_USE_OF_HAPI_SIGS) + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS) .given( newKeyNamed(ED25519KEY).shape(ED25519), newKeyNamed(ECDSA_KEY).shape(SECP256K1), @@ -226,7 +226,7 @@ private HapiSpec canToggleTopLevelSigUsageForUpdatePrecompile() { grantTokenKyc(VANILLA_TOKEN, ACCOUNT), cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT)), // First revoke use of top-level signatures from all precompiles - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) .when( // Trying to update token with top-level signatures should fail withOpContext((spec, opLog) -> allRunFor( @@ -261,7 +261,7 @@ private HapiSpec canToggleTopLevelSigUsageForUpdatePrecompile() { .hasKnownStatus(CONTRACT_REVERT_EXECUTED), // But now restore use of top-level signatures for // the token update precompile - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenUpdate"), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenUpdate"), contractCall( TOKEN_UPDATE_CONTRACT, "updateTokenWithAllFields", @@ -301,7 +301,6 @@ private HapiSpec canToggleTopLevelSigUsageForUpdatePrecompile() { } private HapiSpec canToggleTopLevelSigUsageForPauseAndUnpausePrecompile() { - final String ALLOW_SYSTEM_USE_OF_HAPI_SIGS = CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; final var failedPauseTxn = "failedPauseTxn"; final var failedUnpauseTxn = "failedUnpauseTxn"; final var succeededPauseTxn = "succeededPauseTxn"; @@ -311,7 +310,7 @@ private HapiSpec canToggleTopLevelSigUsageForPauseAndUnpausePrecompile() { final AtomicReference accountID = new AtomicReference<>(); return propertyPreservingHapiSpec("CanToggleTopLevelSigUsageForPauseAndUnpausePrecompile") - .preserving(ALLOW_SYSTEM_USE_OF_HAPI_SIGS) + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS) .given( newKeyNamed(PAUSE_KEY), newKeyNamed(MULTI_KEY), @@ -329,7 +328,7 @@ private HapiSpec canToggleTopLevelSigUsageForPauseAndUnpausePrecompile() { tokenAssociate(ACCOUNT, VANILLA_TOKEN), cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT)), // First revoke use of top-level signatures from all precompiles - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) .when( // Trying to pause with top-level signatures should fail sourcing(() -> contractCall( @@ -342,7 +341,7 @@ private HapiSpec canToggleTopLevelSigUsageForPauseAndUnpausePrecompile() { .gas(GAS_TO_OFFER) .via(failedPauseTxn)), // But now restore use of top-level signatures for the pause precompile - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenPause"), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenPause"), // Now the same call should succeed sourcing(() -> contractCall( PAUSE_UNPAUSE_CONTRACT, @@ -354,7 +353,7 @@ private HapiSpec canToggleTopLevelSigUsageForPauseAndUnpausePrecompile() { .gas(GAS_TO_OFFER) .via(succeededPauseTxn)), // revoke use of top-level signatures from all precompiles again - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, ""), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, ""), // Now the same call should succeed sourcing(() -> contractCall( PAUSE_UNPAUSE_CONTRACT, @@ -366,7 +365,7 @@ private HapiSpec canToggleTopLevelSigUsageForPauseAndUnpausePrecompile() { .gas(GAS_TO_OFFER) .via(failedUnpauseTxn)), // But now restore use of top-level signatures for the unpause precompile - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenUnpause"), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenUnpause"), // Now the same call should succeed sourcing(() -> contractCall( PAUSE_UNPAUSE_CONTRACT, @@ -439,12 +438,11 @@ private HapiSpec canToggleTopLevelSigUsageForAssociatePrecompile() { } private HapiSpec canToggleTopLevelSigUsageForBurnPrecompile() { - final String ALLOW_SYSTEM_USE_OF_HAPI_SIGS = CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; final var failedBurnTxn = "failedBurnTxn"; final var succeededBurnTxn = "succeededBurnTxn"; return propertyPreservingHapiSpec("CanToggleTopLevelSigUsageForBurnPrecompile") - .preserving(ALLOW_SYSTEM_USE_OF_HAPI_SIGS) + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS) .given( newKeyNamed(MULTI_KEY), newKeyNamed(SUPPLY_KEY), @@ -467,7 +465,7 @@ private HapiSpec canToggleTopLevelSigUsageForBurnPrecompile() { .via(CREATION_TX) .gas(GAS_TO_OFFER))), // First revoke use of top-level signatures from all precompiles - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) .when( // Trying to burn with top-level signatures should fail sourcing(() -> contractCall( @@ -478,7 +476,7 @@ private HapiSpec canToggleTopLevelSigUsageForBurnPrecompile() { .via(failedBurnTxn) .hasKnownStatus(CONTRACT_REVERT_EXECUTED)), // But now restore use of top-level signatures for the burn precompile - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenBurn"), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenBurn"), // Now the same call should succeed sourcing(() -> contractCall( THE_BURN_CONTRACT, BURN_TOKEN_WITH_EVENT, BigInteger.valueOf(10L), new long[0]) @@ -496,7 +494,6 @@ private HapiSpec canToggleTopLevelSigUsageForBurnPrecompile() { } private HapiSpec canToggleTopLevelSigUsageForMintPrecompile() { - final String ALLOW_SYSTEM_USE_OF_HAPI_SIGS = CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; final var tokenToMint = "tokenToMint"; final var failedMintTxn = "failedMintTxn"; final var succeededMintTxn = "succeededMintTxn"; @@ -504,7 +501,7 @@ private HapiSpec canToggleTopLevelSigUsageForMintPrecompile() { final AtomicReference
accountAddress = new AtomicReference<>(); final AtomicReference fungible = new AtomicReference<>(); return propertyPreservingHapiSpec("CanToggleTopLevelSigUsageForMintPrecompile") - .preserving(ALLOW_SYSTEM_USE_OF_HAPI_SIGS) + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS) .given( newKeyNamed(MULTI_KEY), newKeyNamed(SUPPLY_KEY), @@ -525,7 +522,7 @@ private HapiSpec canToggleTopLevelSigUsageForMintPrecompile() { sourcing(() -> contractCreate( MINT_CONTRACT, HapiParserUtil.asHeadlongAddress(asAddress(fungible.get())))), // First revoke use of top-level signatures from all precompiles - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) .when( // Trying to mint with top-level signatures should fail sourcing(() -> contractCall( @@ -535,7 +532,7 @@ private HapiSpec canToggleTopLevelSigUsageForMintPrecompile() { .via(failedMintTxn) .hasKnownStatus(CONTRACT_REVERT_EXECUTED)), // But now restore use of top-level signatures for the mint precompile - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenMint"), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenMint"), // Now the same call should succeed sourcing(() -> contractCall( MINT_CONTRACT, MINT_FUNGIBLE_TOKEN_WITH_EVENT, BigInteger.valueOf(10L)) @@ -552,13 +549,12 @@ private HapiSpec canToggleTopLevelSigUsageForMintPrecompile() { } private HapiSpec canToggleTopLevelSigUsageForDeletePrecompile() { - final String ALLOW_SYSTEM_USE_OF_HAPI_SIGS = CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; final var failedDeleteTxn = "failedDeleteTxn"; final var succeededDeleteTxn = "succeededDeleteTxn"; final AtomicReference accountID = new AtomicReference<>(); final AtomicReference vanillaTokenID = new AtomicReference<>(); return propertyPreservingHapiSpec("CanToggleTopLevelSigUsageForDeletePrecompile") - .preserving(ALLOW_SYSTEM_USE_OF_HAPI_SIGS) + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS) .given( newKeyNamed(MULTI_KEY), cryptoCreate(ACCOUNT) @@ -577,7 +573,7 @@ private HapiSpec canToggleTopLevelSigUsageForDeletePrecompile() { tokenAssociate(ACCOUNT, VANILLA_TOKEN), cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT)), // First revoke use of top-level signatures from all precompiles - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) .when( // Trying to delete with top-level signatures should fail sourcing(() -> contractCall( @@ -589,7 +585,7 @@ private HapiSpec canToggleTopLevelSigUsageForDeletePrecompile() { .via(failedDeleteTxn) .hasKnownStatus(CONTRACT_REVERT_EXECUTED)), // But now restore use of top-level signatures for the delete precompile - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenDelete"), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenDelete"), // Now the same call should succeed sourcing(() -> contractCall( DELETE_TOKEN_CONTRACT, @@ -608,7 +604,6 @@ private HapiSpec canToggleTopLevelSigUsageForDeletePrecompile() { } private HapiSpec canToggleTopLevelSigUsageForFreezeAndUnfreezePrecompile() { - final String ALLOW_SYSTEM_USE_OF_HAPI_SIGS = CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; final var failedFreezeTxn = "failedFreezeTxn"; final var failedUnfreezeTxn = "failedUnfreezeTxn"; final var succeededFreezeTxn = "succeededFreezeTxn"; @@ -618,7 +613,7 @@ private HapiSpec canToggleTopLevelSigUsageForFreezeAndUnfreezePrecompile() { final AtomicReference accountID = new AtomicReference<>(); return propertyPreservingHapiSpec("CanToggleTopLevelSigUsageForFreezeAndUnfreezePrecompile") - .preserving(ALLOW_SYSTEM_USE_OF_HAPI_SIGS) + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS) .given( newKeyNamed(FREEZE_KEY), newKeyNamed(MULTI_KEY), @@ -636,7 +631,7 @@ private HapiSpec canToggleTopLevelSigUsageForFreezeAndUnfreezePrecompile() { tokenAssociate(ACCOUNT, VANILLA_TOKEN), cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT)), // First revoke use of top-level signatures from all precompiles - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) .when( // Trying to freezing with top-level signatures should fail sourcing(() -> contractCall( @@ -650,7 +645,7 @@ private HapiSpec canToggleTopLevelSigUsageForFreezeAndUnfreezePrecompile() { .gas(GAS_TO_OFFER) .via(failedFreezeTxn)), // But now restore use of top-level signatures for the freeze precompile - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenFreezeAccount"), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenFreezeAccount"), // Now the same call should succeed sourcing(() -> contractCall( FREEZE_CONTRACT, @@ -663,7 +658,7 @@ private HapiSpec canToggleTopLevelSigUsageForFreezeAndUnfreezePrecompile() { .gas(GAS_TO_OFFER) .via(succeededFreezeTxn)), // revoke use of top-level signatures from all precompiles again - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, ""), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, ""), // Now the same call should succeed sourcing(() -> contractCall( FREEZE_CONTRACT, @@ -676,7 +671,7 @@ private HapiSpec canToggleTopLevelSigUsageForFreezeAndUnfreezePrecompile() { .gas(GAS_TO_OFFER) .via(failedUnfreezeTxn)), // But now restore use of top-level signatures for the unfreeze precompile - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenUnfreezeAccount"), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenUnfreezeAccount"), // Now the same call should succeed sourcing(() -> contractCall( FREEZE_CONTRACT, @@ -701,7 +696,6 @@ private HapiSpec canToggleTopLevelSigUsageForFreezeAndUnfreezePrecompile() { } private HapiSpec canToggleTopLevelSigUsageForGrantKycAndRevokeKycPrecompile() { - final String ALLOW_SYSTEM_USE_OF_HAPI_SIGS = CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; final var failedGrantTxn = "failedGrantTxn"; final var failedRevokeTxn = "failedRevokeTxn"; final var succeededGrantTxn = "succeededGrantTxn"; @@ -712,7 +706,7 @@ private HapiSpec canToggleTopLevelSigUsageForGrantKycAndRevokeKycPrecompile() { final AtomicReference secondAccountID = new AtomicReference<>(); return propertyPreservingHapiSpec("canToggleTopLevelSigUsageForGrantKycAndRevokeKycPrecompile") - .preserving(ALLOW_SYSTEM_USE_OF_HAPI_SIGS) + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS) .given( newKeyNamed(KYC_KEY), cryptoCreate(ACCOUNT) @@ -732,7 +726,7 @@ private HapiSpec canToggleTopLevelSigUsageForGrantKycAndRevokeKycPrecompile() { tokenAssociate(ACCOUNT, VANILLA_TOKEN), tokenAssociate(SECOND_ACCOUNT, VANILLA_TOKEN), // First revoke use of top-level signatures from all precompiles - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) .when( // Trying to grant kyc with top-level signatures should fail sourcing(() -> contractCall( @@ -746,7 +740,7 @@ private HapiSpec canToggleTopLevelSigUsageForGrantKycAndRevokeKycPrecompile() { .gas(GAS_TO_OFFER) .via(failedGrantTxn)), // But now restore use of top-level signatures for the grant kyc precompile - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenGrantKycToAccount"), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenGrantKycToAccount"), // Now the same call should succeed sourcing(() -> contractCall( GRANT_REVOKE_KYC_CONTRACT, @@ -759,7 +753,7 @@ private HapiSpec canToggleTopLevelSigUsageForGrantKycAndRevokeKycPrecompile() { .gas(GAS_TO_OFFER) .via(succeededGrantTxn)), // revoke use of top-level signatures from all precompiles again - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, ""), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, ""), // Now the same call should succeed sourcing(() -> contractCall( GRANT_REVOKE_KYC_CONTRACT, @@ -772,7 +766,7 @@ private HapiSpec canToggleTopLevelSigUsageForGrantKycAndRevokeKycPrecompile() { .gas(GAS_TO_OFFER) .via(failedRevokeTxn)), // But now restore use of top-level signatures for the revoke kyc precompile - overriding(ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenRevokeKycToAccount"), + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "TokenRevokeKycToAccount"), // Now the same call should succeed sourcing(() -> contractCall( GRANT_REVOKE_KYC_CONTRACT, diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/V1SecurityModelOverrides.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/V1SecurityModelOverrides.java new file mode 100644 index 000000000000..35ddd35e0065 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/V1SecurityModelOverrides.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 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; + +public class V1SecurityModelOverrides { + + public static final String CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS = "contracts.allowSystemUseOfHapiSigs"; + + public static final String CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS = "contracts.maxNumWithHapiSigsAccess"; + public static final String CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF = "10_000_000"; + + private V1SecurityModelOverrides() {} +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/WipeTokenAccountPrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/WipeTokenAccountPrecompileSuite.java index 6a18305a2073..90740bfcf815 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/WipeTokenAccountPrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/WipeTokenAccountPrecompileSuite.java @@ -16,62 +16,14 @@ package com.hedera.services.bdd.suites.contract.precompile; -import static com.google.protobuf.ByteString.copyFromUtf8; -import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; -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.infrastructure.providers.ops.crypto.RandomAccount.INITIAL_BALANCE; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; -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.cryptoUpdate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; -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.spec.transactions.TxnVerbs.uploadInitCode; -import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; -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.childRecordsCheck; -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.asAddress; -import static com.hedera.services.bdd.suites.contract.Utils.asToken; -import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; -import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DOES_NOT_OWN_WIPED_NFT; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_NFT_ID; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_WIPING_AMOUNT; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; -import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; -import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; - import com.hedera.services.bdd.spec.HapiSpec; -import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; import com.hedera.services.bdd.suites.HapiSuite; -import com.hederahashgraph.api.proto.java.AccountID; -import com.hederahashgraph.api.proto.java.TokenID; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class WipeTokenAccountPrecompileSuite extends HapiSuite { private static final Logger log = LogManager.getLogger(WipeTokenAccountPrecompileSuite.class); - public static final String WIPE_CONTRACT = "WipeTokenAccount"; - public static final String ADMIN_ACCOUNT = "admin"; - private static final String ACCOUNT = "anybody"; - private static final String SECOND_ACCOUNT = "anybodySecond"; - public static final String WIPE_KEY = "wipeKey"; - private static final String MULTI_KEY = "purpose"; - public static final int GAS_TO_OFFER = 1_000_000; - public static final String WIPE_FUNGIBLE_TOKEN = "wipeFungibleToken"; - public static final String WIPE_NON_FUNGIBLE_TOKEN = "wipeNonFungibleToken"; public static void main(String... args) { new WipeTokenAccountPrecompileSuite().runSuiteAsync(); @@ -89,255 +41,6 @@ public boolean canRunConcurrent() { @Override public List getSpecsInSuite() { - return List.of(wipeFungibleTokenScenarios(), wipeNonFungibleTokenScenarios()); - } - - private HapiSpec wipeFungibleTokenScenarios() { - final AtomicReference adminAccountID = new AtomicReference<>(); - final AtomicReference accountID = new AtomicReference<>(); - final AtomicReference secondAccountID = new AtomicReference<>(); - final AtomicReference vanillaTokenID = new AtomicReference<>(); - - return defaultHapiSpec("WipeFungibleTokenScenarios") - .given( - newKeyNamed(WIPE_KEY), - cryptoCreate(ADMIN_ACCOUNT).exposingCreatedIdTo(adminAccountID::set), - cryptoCreate(ACCOUNT).exposingCreatedIdTo(accountID::set), - cryptoCreate(SECOND_ACCOUNT).exposingCreatedIdTo(secondAccountID::set), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(VANILLA_TOKEN) - .tokenType(FUNGIBLE_COMMON) - .treasury(TOKEN_TREASURY) - .wipeKey(WIPE_KEY) - .initialSupply(1_000) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - uploadInitCode(WIPE_CONTRACT), - contractCreate(WIPE_CONTRACT), - tokenAssociate(ACCOUNT, VANILLA_TOKEN), - tokenAssociate(SECOND_ACCOUNT, VANILLA_TOKEN), - cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCall( - WIPE_CONTRACT, - WIPE_FUNGIBLE_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - 10L) - .signedBy(GENESIS, ADMIN_ACCOUNT) - .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) - .via("accountDoesNotOwnWipeKeyTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - cryptoUpdate(ADMIN_ACCOUNT).key(WIPE_KEY), - contractCall( - WIPE_CONTRACT, - WIPE_FUNGIBLE_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - 1_000L) - .signedBy(GENESIS, ADMIN_ACCOUNT) - .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) - .via("amountLargerThanBalanceTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - WIPE_CONTRACT, - WIPE_FUNGIBLE_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(secondAccountID.get())), - 10L) - .signedBy(GENESIS, ADMIN_ACCOUNT) - .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) - .via("accountDoesNotOwnTokensTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - WIPE_CONTRACT, - WIPE_FUNGIBLE_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - 10L) - .signedBy(GENESIS, ADMIN_ACCOUNT) - .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) - .via("wipeFungibleTxn") - .gas(GAS_TO_OFFER), - contractCall( - WIPE_CONTRACT, - WIPE_FUNGIBLE_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - 0L) - .signedBy(GENESIS, ADMIN_ACCOUNT) - .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) - .via("wipeFungibleTxnWithZeroAmount") - .gas(GAS_TO_OFFER)))) - .then( - childRecordsCheck( - "accountDoesNotOwnWipeKeyTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_SIGNATURE) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), - childRecordsCheck( - "amountLargerThanBalanceTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_WIPING_AMOUNT) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_WIPING_AMOUNT)))), - childRecordsCheck( - "accountDoesNotOwnTokensTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_WIPING_AMOUNT) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_WIPING_AMOUNT)))), - childRecordsCheck( - "wipeFungibleTxnWithZeroAmount", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)) - .gasUsed(14085L))), - getTokenInfo(VANILLA_TOKEN).hasTotalSupply(990), - getAccountBalance(ACCOUNT).hasTokenBalance(VANILLA_TOKEN, 490)); - } - - private HapiSpec wipeNonFungibleTokenScenarios() { - final AtomicReference adminAccountID = new AtomicReference<>(); - final AtomicReference accountID = new AtomicReference<>(); - final AtomicReference vanillaTokenID = new AtomicReference<>(); - - return defaultHapiSpec("WipeNonFungibleTokenScenarios") - .given( - newKeyNamed(WIPE_KEY), - newKeyNamed(MULTI_KEY), - cryptoCreate(ADMIN_ACCOUNT).exposingCreatedIdTo(adminAccountID::set), - cryptoCreate(ACCOUNT).balance(INITIAL_BALANCE).exposingCreatedIdTo(accountID::set), - cryptoCreate(TOKEN_TREASURY), - tokenCreate(VANILLA_TOKEN) - .tokenType(NON_FUNGIBLE_UNIQUE) - .treasury(TOKEN_TREASURY) - .wipeKey(WIPE_KEY) - .supplyKey(MULTI_KEY) - .initialSupply(0) - .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), - mintToken(VANILLA_TOKEN, List.of(copyFromUtf8("First!"))), - mintToken(VANILLA_TOKEN, List.of(copyFromUtf8("Second!"))), - uploadInitCode(WIPE_CONTRACT), - contractCreate(WIPE_CONTRACT), - tokenAssociate(ACCOUNT, VANILLA_TOKEN), - cryptoTransfer(movingUnique(VANILLA_TOKEN, 1L).between(TOKEN_TREASURY, ACCOUNT))) - .when(withOpContext((spec, opLog) -> { - final var serialNumbers = new long[] {1L}; - allRunFor( - spec, - contractCall( - WIPE_CONTRACT, - WIPE_NON_FUNGIBLE_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - serialNumbers) - .signedBy(GENESIS, ADMIN_ACCOUNT) - .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) - .via("wipeNonFungibleAccountDoesNotOwnWipeKeyTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - cryptoUpdate(ADMIN_ACCOUNT).key(WIPE_KEY), - contractCall( - WIPE_CONTRACT, - WIPE_NON_FUNGIBLE_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - new long[] {2L}) - .signedBy(GENESIS, ADMIN_ACCOUNT) - .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) - .via("wipeNonFungibleAccountDoesNotOwnTheSerialTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - WIPE_CONTRACT, - WIPE_NON_FUNGIBLE_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - new long[] {-2L}) - .signedBy(GENESIS, ADMIN_ACCOUNT) - .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) - .via("wipeNonFungibleNegativeSerialTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - WIPE_CONTRACT, - WIPE_NON_FUNGIBLE_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - new long[] {3L}) - .signedBy(GENESIS, ADMIN_ACCOUNT) - .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) - .via("wipeNonFungibleSerialDoesNotExistsTxn") - .gas(GAS_TO_OFFER) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED), - contractCall( - WIPE_CONTRACT, - WIPE_NON_FUNGIBLE_TOKEN, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), - serialNumbers) - .signedBy(GENESIS, ADMIN_ACCOUNT) - .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) - .via("wipeNonFungibleTxn") - .gas(GAS_TO_OFFER)); - })) - .then( - childRecordsCheck( - "wipeNonFungibleAccountDoesNotOwnWipeKeyTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_SIGNATURE) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), - childRecordsCheck( - "wipeNonFungibleAccountDoesNotOwnTheSerialTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(ACCOUNT_DOES_NOT_OWN_WIPED_NFT) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .withStatus(ACCOUNT_DOES_NOT_OWN_WIPED_NFT)))), - childRecordsCheck( - "wipeNonFungibleNegativeSerialTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_NFT_ID) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_NFT_ID)))), - childRecordsCheck( - "wipeNonFungibleSerialDoesNotExistsTxn", - CONTRACT_REVERT_EXECUTED, - recordWith() - .status(INVALID_NFT_ID) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(INVALID_NFT_ID)))), - childRecordsCheck( - "wipeNonFungibleTxn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult( - htsPrecompileResult().withStatus(SUCCESS)) - .gasUsed(14085L))), - getTokenInfo(VANILLA_TOKEN).hasTotalSupply(1), - getAccountBalance(ACCOUNT).hasTokenBalance(VANILLA_TOKEN, 0)); + return List.of(); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/WipeTokenAccountPrecompileV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/WipeTokenAccountPrecompileV1SecurityModelSuite.java new file mode 100644 index 000000000000..499bc9409bc3 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/WipeTokenAccountPrecompileV1SecurityModelSuite.java @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2021-2023 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.google.protobuf.ByteString.copyFromUtf8; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +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.infrastructure.providers.ops.crypto.RandomAccount.INITIAL_BALANCE; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +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.cryptoUpdate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.mintToken; +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.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +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.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.asToken; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DOES_NOT_OWN_WIPED_NFT; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_NFT_ID; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_WIPING_AMOUNT; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; +import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; + +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.TokenID; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class WipeTokenAccountPrecompileV1SecurityModelSuite extends HapiSuite { + private static final Logger log = LogManager.getLogger(WipeTokenAccountPrecompileV1SecurityModelSuite.class); + public static final String WIPE_CONTRACT = "WipeTokenAccount"; + public static final String ADMIN_ACCOUNT = "admin"; + private static final String ACCOUNT = "anybody"; + private static final String SECOND_ACCOUNT = "anybodySecond"; + public static final String WIPE_KEY = "wipeKey"; + private static final String MULTI_KEY = "purpose"; + public static final int GAS_TO_OFFER = 1_000_000; + public static final String WIPE_FUNGIBLE_TOKEN = "wipeFungibleToken"; + public static final String WIPE_NON_FUNGIBLE_TOKEN = "wipeNonFungibleToken"; + + public static void main(String... args) { + new WipeTokenAccountPrecompileV1SecurityModelSuite().runSuiteSync(); + } + + @Override + protected Logger getResultsLogger() { + return log; + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + public List getSpecsInSuite() { + return List.of(wipeFungibleTokenScenarios(), wipeNonFungibleTokenScenarios()); + } + + private HapiSpec wipeFungibleTokenScenarios() { + final AtomicReference adminAccountID = new AtomicReference<>(); + final AtomicReference accountID = new AtomicReference<>(); + final AtomicReference secondAccountID = new AtomicReference<>(); + final AtomicReference vanillaTokenID = new AtomicReference<>(); + + return propertyPreservingHapiSpec("wipeFungibleTokenScenarios") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenAccountWipe", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(WIPE_KEY), + cryptoCreate(ADMIN_ACCOUNT).exposingCreatedIdTo(adminAccountID::set), + cryptoCreate(ACCOUNT).exposingCreatedIdTo(accountID::set), + cryptoCreate(SECOND_ACCOUNT).exposingCreatedIdTo(secondAccountID::set), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(VANILLA_TOKEN) + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .wipeKey(WIPE_KEY) + .initialSupply(1_000) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + uploadInitCode(WIPE_CONTRACT), + contractCreate(WIPE_CONTRACT), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + tokenAssociate(SECOND_ACCOUNT, VANILLA_TOKEN), + cryptoTransfer(moving(500, VANILLA_TOKEN).between(TOKEN_TREASURY, ACCOUNT))) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + WIPE_CONTRACT, + WIPE_FUNGIBLE_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + 10L) + .signedBy(GENESIS, ADMIN_ACCOUNT) + .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) + .via("accountDoesNotOwnWipeKeyTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + cryptoUpdate(ADMIN_ACCOUNT).key(WIPE_KEY), + contractCall( + WIPE_CONTRACT, + WIPE_FUNGIBLE_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + 1_000L) + .signedBy(GENESIS, ADMIN_ACCOUNT) + .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) + .via("amountLargerThanBalanceTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + WIPE_CONTRACT, + WIPE_FUNGIBLE_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(secondAccountID.get())), + 10L) + .signedBy(GENESIS, ADMIN_ACCOUNT) + .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) + .via("accountDoesNotOwnTokensTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + WIPE_CONTRACT, + WIPE_FUNGIBLE_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + 10L) + .signedBy(GENESIS, ADMIN_ACCOUNT) + .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) + .via("wipeFungibleTxn") + .gas(GAS_TO_OFFER), + contractCall( + WIPE_CONTRACT, + WIPE_FUNGIBLE_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + 0L) + .signedBy(GENESIS, ADMIN_ACCOUNT) + .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) + .via("wipeFungibleTxnWithZeroAmount") + .gas(GAS_TO_OFFER)))) + .then( + childRecordsCheck( + "accountDoesNotOwnWipeKeyTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_SIGNATURE) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), + childRecordsCheck( + "amountLargerThanBalanceTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_WIPING_AMOUNT) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_WIPING_AMOUNT)))), + childRecordsCheck( + "accountDoesNotOwnTokensTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_WIPING_AMOUNT) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_WIPING_AMOUNT)))), + childRecordsCheck( + "wipeFungibleTxnWithZeroAmount", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)) + .gasUsed(14085L))), + getTokenInfo(VANILLA_TOKEN).hasTotalSupply(990), + getAccountBalance(ACCOUNT).hasTokenBalance(VANILLA_TOKEN, 490)); + } + + private HapiSpec wipeNonFungibleTokenScenarios() { + final AtomicReference adminAccountID = new AtomicReference<>(); + final AtomicReference accountID = new AtomicReference<>(); + final AtomicReference vanillaTokenID = new AtomicReference<>(); + + return propertyPreservingHapiSpec("wipeNonFungibleTokenScenarios") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenAccountWipe", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(WIPE_KEY), + newKeyNamed(MULTI_KEY), + cryptoCreate(ADMIN_ACCOUNT).exposingCreatedIdTo(adminAccountID::set), + cryptoCreate(ACCOUNT).balance(INITIAL_BALANCE).exposingCreatedIdTo(accountID::set), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(VANILLA_TOKEN) + .tokenType(NON_FUNGIBLE_UNIQUE) + .treasury(TOKEN_TREASURY) + .wipeKey(WIPE_KEY) + .supplyKey(MULTI_KEY) + .initialSupply(0) + .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + mintToken(VANILLA_TOKEN, List.of(copyFromUtf8("First!"))), + mintToken(VANILLA_TOKEN, List.of(copyFromUtf8("Second!"))), + uploadInitCode(WIPE_CONTRACT), + contractCreate(WIPE_CONTRACT), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + cryptoTransfer(movingUnique(VANILLA_TOKEN, 1L).between(TOKEN_TREASURY, ACCOUNT))) + .when(withOpContext((spec, opLog) -> { + final var serialNumbers = new long[] {1L}; + allRunFor( + spec, + contractCall( + WIPE_CONTRACT, + WIPE_NON_FUNGIBLE_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + serialNumbers) + .signedBy(GENESIS, ADMIN_ACCOUNT) + .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) + .via("wipeNonFungibleAccountDoesNotOwnWipeKeyTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + cryptoUpdate(ADMIN_ACCOUNT).key(WIPE_KEY), + contractCall( + WIPE_CONTRACT, + WIPE_NON_FUNGIBLE_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + new long[] {2L}) + .signedBy(GENESIS, ADMIN_ACCOUNT) + .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) + .via("wipeNonFungibleAccountDoesNotOwnTheSerialTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + WIPE_CONTRACT, + WIPE_NON_FUNGIBLE_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + new long[] {-2L}) + .signedBy(GENESIS, ADMIN_ACCOUNT) + .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) + .via("wipeNonFungibleNegativeSerialTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + WIPE_CONTRACT, + WIPE_NON_FUNGIBLE_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + new long[] {3L}) + .signedBy(GENESIS, ADMIN_ACCOUNT) + .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) + .via("wipeNonFungibleSerialDoesNotExistsTxn") + .gas(GAS_TO_OFFER) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), + contractCall( + WIPE_CONTRACT, + WIPE_NON_FUNGIBLE_TOKEN, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(asAddress(accountID.get())), + serialNumbers) + .signedBy(GENESIS, ADMIN_ACCOUNT) + .alsoSigningWithFullPrefix(ADMIN_ACCOUNT) + .via("wipeNonFungibleTxn") + .gas(GAS_TO_OFFER)); + })) + .then( + childRecordsCheck( + "wipeNonFungibleAccountDoesNotOwnWipeKeyTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_SIGNATURE) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_SIGNATURE)))), + childRecordsCheck( + "wipeNonFungibleAccountDoesNotOwnTheSerialTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(ACCOUNT_DOES_NOT_OWN_WIPED_NFT) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .withStatus(ACCOUNT_DOES_NOT_OWN_WIPED_NFT)))), + childRecordsCheck( + "wipeNonFungibleNegativeSerialTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_NFT_ID) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_NFT_ID)))), + childRecordsCheck( + "wipeNonFungibleSerialDoesNotExistsTxn", + CONTRACT_REVERT_EXECUTED, + recordWith() + .status(INVALID_NFT_ID) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(INVALID_NFT_ID)))), + childRecordsCheck( + "wipeNonFungibleTxn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult( + htsPrecompileResult().withStatus(SUCCESS)) + .gasUsed(14085L))), + getTokenInfo(VANILLA_TOKEN).hasTotalSupply(1), + getAccountBalance(ACCOUNT).hasTokenBalance(VANILLA_TOKEN, 0)); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/traceability/TraceabilitySuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/traceability/TraceabilitySuite.java index 4a0b4c94d52f..b0eb83d0d5de 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/traceability/TraceabilitySuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/traceability/TraceabilitySuite.java @@ -56,6 +56,7 @@ import static com.hedera.services.bdd.suites.contract.Utils.captureOneChildCreate2MetaFor; import static com.hedera.services.bdd.suites.contract.Utils.extractBytecodeUnhexed; import static com.hedera.services.bdd.suites.contract.Utils.getABIFor; +import static com.hedera.services.bdd.suites.contract.Utils.getNestedContractAddress; import static com.hedera.services.bdd.suites.contract.Utils.getResourcePath; import static com.hedera.services.bdd.suites.contract.opcodes.Create2OperationSuite.CONTRACT_REPORTED_ADDRESS_MESSAGE; import static com.hedera.services.bdd.suites.contract.opcodes.Create2OperationSuite.CONTRACT_REPORTED_LOG_MESSAGE; @@ -63,7 +64,6 @@ import static com.hedera.services.bdd.suites.contract.opcodes.Create2OperationSuite.EXPECTED_CREATE2_ADDRESS_MESSAGE; import static com.hedera.services.bdd.suites.contract.opcodes.Create2OperationSuite.GET_ADDRESS; import static com.hedera.services.bdd.suites.contract.opcodes.Create2OperationSuite.GET_BYTECODE; -import static com.hedera.services.bdd.suites.contract.precompile.AssociatePrecompileSuite.getNestedContractAddress; import static com.hedera.services.bdd.suites.crypto.AutoAccountCreationSuite.PARTY; import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.MULTI_KEY; import static com.hedera.services.stream.proto.ContractActionType.CALL; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumSuite.java index 41f0d13cd42f..9866e9c16bc7 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumSuite.java @@ -30,7 +30,6 @@ import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAutoCreatedAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractBytecode; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getLiteralAliasContractInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; @@ -78,8 +77,6 @@ import com.hedera.node.app.hapi.utils.ethereum.EthTxData.EthTransactionType; import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.assertions.ContractInfoAsserts; -import com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts; -import com.hedera.services.bdd.spec.keys.SigControl; import com.hedera.services.bdd.spec.queries.meta.HapiGetTxnRecord; import com.hedera.services.bdd.spec.transactions.TxnUtils; import com.hedera.services.bdd.suites.HapiSuite; @@ -92,12 +89,10 @@ import java.math.BigInteger; import java.util.Arrays; import java.util.List; -import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.tuweni.bytes.Bytes; import org.bouncycastle.util.encoders.Hex; import org.junit.jupiter.api.Assertions; @@ -105,10 +100,9 @@ public class EthereumSuite extends HapiSuite { private static final Logger log = LogManager.getLogger(EthereumSuite.class); - private static final long depositAmount = 20_000L; + private static final long DEPOSIT_AMOUNT = 20_000L; private static final String PAY_RECEIVABLE_CONTRACT = "PayReceivable"; private static final String TOKEN_CREATE_CONTRACT = "NewTokenCreateContract"; - private static final String ERC721_CONTRACT_WITH_HTS_CALLS = "ERC721ContractWithHTSCalls"; private static final String HELLO_WORLD_MINT_CONTRACT = "HelloWorldMint"; public static final long GAS_LIMIT = 1_000_000; @@ -136,12 +130,9 @@ public List getSpecsInSuite() { feePaymentMatrix().stream(), Stream.of( invalidTxData(), - etx007FungibleTokenCreateWithFeesHappyPath(), etx008ContractCreateExecutesWithExpectedRecord(), etx009CallsToTokenAddresses(), etx010TransferToCryptoAccountSucceeds(), - etx012PrecompileCallSucceedsWhenNeededSignatureInEthTxn(), - etx013PrecompileCallSucceedsWhenNeededSignatureInHederaTxn(), etx013PrecompileCallFailsWhenSignatureMissingFromBothEthereumAndHederaTxn(), etx014ContractCreateInheritsSignerProperties(), etx009CallsToTokenAddresses(), @@ -149,7 +140,6 @@ public List getSpecsInSuite() { etx031InvalidNonceEthereumTxFailsAndChargesRelayer(), etxSvc003ContractGetBytecodeQueryReturnsDeployedCode(), sendingLargerBalanceThanAvailableFailsGracefully(), - setApproveForAllUsingLocalNodeSetupPasses(), directTransferWorksForERC20())) .toList(); } @@ -197,206 +187,6 @@ HapiSpec sendingLargerBalanceThanAvailableFailsGracefully() { })); } - HapiSpec setApproveForAllUsingLocalNodeSetupPasses() { - final AtomicReference spenderAutoCreatedAccountId = new AtomicReference<>(); - final AtomicReference tokenCreateContractID = new AtomicReference<>(); - final AtomicReference erc721ContractID = new AtomicReference<>(); - final AtomicReference contractAddressID = new AtomicReference<>(); - final AtomicReference createdTokenAddressString = new AtomicReference<>(); - final String spenderAlias = "spenderAlias"; - final var createTokenContractNum = new AtomicLong(); - return defaultHapiSpec("SetApproveForAllUsingLocalNodeSetupPasses") - .given( - newKeyNamed(SECP_256K1_SOURCE_KEY).shape(SECP_256K1_SHAPE), - newKeyNamed(spenderAlias).shape(SECP_256K1_SHAPE), - cryptoCreate(RELAYER).balance(6 * ONE_MILLION_HBARS), - cryptoTransfer(tinyBarsFromAccountToAlias(GENESIS, SECP_256K1_SOURCE_KEY, ONE_MILLION_HBARS)) - .via(AUTO_ACCOUNT_TRANSACTION_NAME), - cryptoTransfer(tinyBarsFromAccountToAlias(GENESIS, spenderAlias, ONE_HUNDRED_HBARS)) - .via("autoAccountSpender"), - getAliasedAccountInfo(spenderAlias) - .exposingContractAccountIdTo(spenderAutoCreatedAccountId::set), - createLargeFile( - GENESIS, TOKEN_CREATE_CONTRACT, TxnUtils.literalInitcodeFor(TOKEN_CREATE_CONTRACT)), - ethereumContractCreate(TOKEN_CREATE_CONTRACT) - .type(EthTransactionType.EIP1559) - .signingWith(SECP_256K1_SOURCE_KEY) - .payingWith(RELAYER) - .nonce(0) - .bytecode(TOKEN_CREATE_CONTRACT) - .gasPrice(10L) - .maxGasAllowance(ONE_HUNDRED_HBARS) - .gasLimit(1_000_000L) - .gas(1_000_000L) - .hasKnownStatusFrom(SUCCESS) - .exposingNumTo(createTokenContractNum::set), - getContractInfo(TOKEN_CREATE_CONTRACT).exposingEvmAddress(tokenCreateContractID::set)) - .when( - withOpContext((spec, opLog) -> { - var createNFTPublicFunctionCall = ethereumCall( - TOKEN_CREATE_CONTRACT, - "createNonFungibleTokenPublic", - asHeadlongAddress(tokenCreateContractID.get())) - .type(EthTransactionType.EIP1559) - .signingWith(SECP_256K1_SOURCE_KEY) - .payingWith(RELAYER) - .nonce(1) - .gasPrice(10L) - .sending(10000000000L) - .gasLimit(1_000_000L) - .via("createTokenTxn") - .exposingEventDataTo(createdTokenAddressString::set); - - allRunFor(spec, createNFTPublicFunctionCall); - - var uploadEthereumContract = uploadInitCode(ERC721_CONTRACT_WITH_HTS_CALLS); - allRunFor(spec, uploadEthereumContract); - - var createEthereumContract = ethereumContractCreate(ERC721_CONTRACT_WITH_HTS_CALLS) - .type(EthTxData.EthTransactionType.EIP1559) - .signingWith(SECP_256K1_SOURCE_KEY) - .payingWith(RELAYER) - .nonce(2) - .gasPrice(10L) - .maxGasAllowance(ONE_HUNDRED_HBARS) - .gasLimit(1_000_000L) - .hasKnownStatusFrom(SUCCESS); - - var exposeEthereumContractAddress = getContractInfo(ERC721_CONTRACT_WITH_HTS_CALLS) - .exposingEvmAddress(address -> erc721ContractID.set("0x" + address)); - allRunFor(spec, createEthereumContract, exposeEthereumContractAddress); - - var contractInfo = getLiteralAliasContractInfo( - erc721ContractID.get().substring(2)) - .exposingEvmAddress(contractAddressID::set); - allRunFor(spec, contractInfo); - assertEquals(erc721ContractID.get().substring(2), contractAddressID.get()); - }), - withOpContext((spec, opLog) -> { - var associateTokenToERC721 = ethereumCall( - ERC721_CONTRACT_WITH_HTS_CALLS, - "associateTokenPublic", - asHeadlongAddress(erc721ContractID.get()), - asHeadlongAddress(Bytes.wrap(createdTokenAddressString - .get() - .toByteArray()) - .toHexString())) - .type(EthTransactionType.EIP1559) - .signingWith(SECP_256K1_SOURCE_KEY) - .payingWith(GENESIS) - .nonce(3) - .gasPrice(10L) - .gasLimit(1_000_000L) - .via("associateTokenTxn") - .hasKnownStatusFrom(SUCCESS); - - var associateTokenToSpender = ethereumCall( - TOKEN_CREATE_CONTRACT, - "associateTokenPublic", - asHeadlongAddress(spenderAutoCreatedAccountId.get()), - asHeadlongAddress(Bytes.wrap(createdTokenAddressString - .get() - .toByteArray()) - .toHexString())) - .type(EthTransactionType.EIP1559) - .signingWith(spenderAlias) - .payingWith(GENESIS) - .nonce(0) - .gasPrice(10L) - .gasLimit(1_000_000L) - .via("associateTokenTxn") - .hasKnownStatusFrom(SUCCESS); - - var isApprovedForAllBefore = ethereumCall( - ERC721_CONTRACT_WITH_HTS_CALLS, - "ercIsApprovedForAll", - asHeadlongAddress(Bytes.wrap(createdTokenAddressString - .get() - .toByteArray()) - .toHexString()), - asHeadlongAddress(erc721ContractID.get()), - asHeadlongAddress(spenderAutoCreatedAccountId.get())) - .type(EthTransactionType.EIP1559) - .signingWith(SECP_256K1_SOURCE_KEY) - .payingWith(RELAYER) - .nonce(4) - .gasPrice(10L) - .gasLimit(1_000_000L) - .via("ercIsApprovedForAllBeforeTxn") - .hasKnownStatusFrom(SUCCESS) - .logged(); - - var isApprovedForAllBeforeCheck = childRecordsCheck( - "ercIsApprovedForAllBeforeTxn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.ERC_IS_APPROVED_FOR_ALL) - .withIsApprovedForAll(false)))); - - var setApprovalForAll = ethereumCall( - ERC721_CONTRACT_WITH_HTS_CALLS, - "ercSetApprovalForAll", - asHeadlongAddress(Bytes.wrap(createdTokenAddressString - .get() - .toByteArray()) - .toHexString()), - asHeadlongAddress(spenderAutoCreatedAccountId.get()), - true) - .type(EthTransactionType.EIP1559) - .signingWith(SECP_256K1_SOURCE_KEY) - .payingWith(RELAYER) - .nonce(5) - .gasPrice(10L) - .gasLimit(1_000_000L) - .via("ercSetApproveForAllTxn") - .hasKnownStatusFrom(SUCCESS) - .logged(); - - var isApprovedForAllAfter = ethereumCall( - ERC721_CONTRACT_WITH_HTS_CALLS, - "ercIsApprovedForAll", - asHeadlongAddress(Bytes.wrap(createdTokenAddressString - .get() - .toByteArray()) - .toHexString()), - asHeadlongAddress(erc721ContractID.get()), - asHeadlongAddress(spenderAutoCreatedAccountId.get())) - .type(EthTransactionType.EIP1559) - .signingWith(SECP_256K1_SOURCE_KEY) - .payingWith(RELAYER) - .nonce(6) - .gasPrice(10L) - .gasLimit(1_000_000L) - .via("ercIsApprovedForAllAfterTxn") - .hasKnownStatusFrom(SUCCESS) - .logged(); - - var isApprovedForAllAfterCheck = childRecordsCheck( - "ercIsApprovedForAllAfterTxn", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.ERC_IS_APPROVED_FOR_ALL) - .withIsApprovedForAll(true)))); - - allRunFor( - spec, - associateTokenToERC721, - associateTokenToSpender, - isApprovedForAllBefore, - isApprovedForAllBeforeCheck, - setApprovalForAll, - isApprovedForAllAfter, - isApprovedForAllAfterCheck); - })) - .then(withOpContext((spec, opLog) -> {})); - } - HapiSpec etx010TransferToCryptoAccountSucceeds() { String RECEIVER = "RECEIVER"; final String aliasBalanceSnapshot = "aliasBalance"; @@ -505,7 +295,7 @@ HapiSpec matrixedPayerRelayerTest( .accountIsAlias(); final var subop2 = balanceSnapshot(payerBalance, RELAYER); final var subop3 = ethereumCall( - PAY_RECEIVABLE_CONTRACT, "deposit", BigInteger.valueOf(depositAmount)) + PAY_RECEIVABLE_CONTRACT, "deposit", BigInteger.valueOf(DEPOSIT_AMOUNT)) .type(EthTxData.EthTransactionType.EIP1559) .signingWith(SECP_256K1_SOURCE_KEY) .payingWith(RELAYER) @@ -514,7 +304,7 @@ HapiSpec matrixedPayerRelayerTest( .maxGasAllowance(relayerOffered) .maxFeePerGas(senderGasPrice) .gasLimit(GAS_LIMIT) - .sending(depositAmount) + .sending(DEPOSIT_AMOUNT) .hasKnownStatus(success ? ResponseCodeEnum.SUCCESS : ResponseCodeEnum.INSUFFICIENT_TX_FEE); final HapiGetTxnRecord hapiGetTxnRecord = @@ -525,7 +315,7 @@ HapiSpec matrixedPayerRelayerTest( hapiGetTxnRecord.getResponseRecord().getTransactionFee(); final var subop4 = getAutoCreatedAccountBalance(SECP_256K1_SOURCE_KEY) .hasTinyBars( - changeFromSnapshot(senderBalance, success ? (-depositAmount - senderCharged) : 0)); + changeFromSnapshot(senderBalance, success ? (-DEPOSIT_AMOUNT - senderCharged) : 0)); final var subop5 = getAccountBalance(RELAYER) .hasTinyBars(changeFromSnapshot( payerBalance, @@ -623,7 +413,7 @@ HapiSpec etx031InvalidNonceEthereumTxFailsAndChargesRelayer() { .when( balanceSnapshot(relayerSnapshot, RELAYER), balanceSnapshot(senderSnapshot, SECP_256K1_SOURCE_KEY).accountIsAlias(), - ethereumCall(PAY_RECEIVABLE_CONTRACT, "deposit", BigInteger.valueOf(depositAmount)) + ethereumCall(PAY_RECEIVABLE_CONTRACT, "deposit", BigInteger.valueOf(DEPOSIT_AMOUNT)) .type(EthTxData.EthTransactionType.EIP1559) .signingWith(SECP_256K1_SOURCE_KEY) .payingWith(RELAYER) @@ -649,107 +439,6 @@ HapiSpec etx031InvalidNonceEthereumTxFailsAndChargesRelayer() { .has(accountWith().nonce(0L))); } - HapiSpec etx012PrecompileCallSucceedsWhenNeededSignatureInEthTxn() { - final AtomicReference fungible = new AtomicReference<>(); - final String fungibleToken = TOKEN; - final String mintTxn = MINT_TXN; - return defaultHapiSpec("ETX_012_precompileCallSucceedsWhenNeededSignatureInEthTxn") - .given( - newKeyNamed(SECP_256K1_SOURCE_KEY).shape(SECP_256K1_SHAPE), - cryptoCreate(RELAYER).balance(6 * ONE_MILLION_HBARS), - cryptoTransfer(tinyBarsFromAccountToAlias(GENESIS, SECP_256K1_SOURCE_KEY, ONE_HUNDRED_HBARS)) - .via(AUTO_ACCOUNT_TRANSACTION_NAME), - withOpContext((spec, opLog) -> updateSpecFor(spec, SECP_256K1_SOURCE_KEY)), - getTxnRecord(AUTO_ACCOUNT_TRANSACTION_NAME).andAllChildRecords(), - uploadInitCode(HELLO_WORLD_MINT_CONTRACT), - tokenCreate(fungibleToken) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(0) - .adminKey(SECP_256K1_SOURCE_KEY) - .supplyKey(SECP_256K1_SOURCE_KEY) - .exposingCreatedIdTo(idLit -> fungible.set(asToken(idLit)))) - .when( - sourcing(() -> contractCreate( - HELLO_WORLD_MINT_CONTRACT, asHeadlongAddress(asAddress(fungible.get())))), - ethereumCall(HELLO_WORLD_MINT_CONTRACT, "brrr", BigInteger.valueOf(5)) - .type(EthTxData.EthTransactionType.EIP1559) - .signingWith(SECP_256K1_SOURCE_KEY) - .payingWith(RELAYER) - .nonce(0) - .gasPrice(50L) - .maxGasAllowance(FIVE_HBARS) - .gasLimit(1_000_000L) - .via(mintTxn) - .hasKnownStatus(SUCCESS)) - .then(withOpContext((spec, opLog) -> allRunFor( - spec, - getTxnRecord(mintTxn) - .logged() - .hasPriority(recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .logs(inOrder()) - .senderId(spec.registry() - .getAccountID(spec.registry() - .aliasIdFor(SECP_256K1_SOURCE_KEY) - .getAlias() - .toStringUtf8()))) - .ethereumHash(ByteString.copyFrom( - spec.registry().getBytes(ETH_HASH_KEY))))))); - } - - HapiSpec etx013PrecompileCallSucceedsWhenNeededSignatureInHederaTxn() { - final AtomicReference fungible = new AtomicReference<>(); - final String fungibleToken = TOKEN; - final String mintTxn = MINT_TXN; - final String MULTI_KEY = "MULTI_KEY"; - return defaultHapiSpec("ETX_013_precompileCallSucceedsWhenNeededSignatureInHederaTxn") - .given( - newKeyNamed(MULTI_KEY), - newKeyNamed(SECP_256K1_SOURCE_KEY).shape(SECP_256K1_SHAPE), - cryptoCreate(RELAYER).balance(6 * ONE_MILLION_HBARS), - cryptoTransfer(tinyBarsFromAccountToAlias(GENESIS, SECP_256K1_SOURCE_KEY, ONE_HUNDRED_HBARS)) - .via(AUTO_ACCOUNT_TRANSACTION_NAME), - withOpContext((spec, opLog) -> updateSpecFor(spec, SECP_256K1_SOURCE_KEY)), - getTxnRecord(AUTO_ACCOUNT_TRANSACTION_NAME).andAllChildRecords(), - uploadInitCode(HELLO_WORLD_MINT_CONTRACT), - tokenCreate(fungibleToken) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(0) - .adminKey(MULTI_KEY) - .supplyKey(MULTI_KEY) - .exposingCreatedIdTo(idLit -> fungible.set(asToken(idLit)))) - .when( - sourcing(() -> contractCreate( - HELLO_WORLD_MINT_CONTRACT, asHeadlongAddress(asAddress(fungible.get())))), - ethereumCall(HELLO_WORLD_MINT_CONTRACT, "brrr", BigInteger.valueOf(5)) - .type(EthTxData.EthTransactionType.EIP1559) - .signingWith(SECP_256K1_SOURCE_KEY) - .payingWith(RELAYER) - .alsoSigningWithFullPrefix(MULTI_KEY) - .nonce(0) - .gasPrice(50L) - .maxGasAllowance(FIVE_HBARS) - .gasLimit(1_000_000L) - .via(mintTxn) - .hasKnownStatus(SUCCESS)) - .then(withOpContext((spec, opLog) -> allRunFor( - spec, - getTxnRecord(mintTxn) - .logged() - .hasPriority(recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .logs(inOrder()) - .senderId(spec.registry() - .getAccountID(spec.registry() - .aliasIdFor(SECP_256K1_SOURCE_KEY) - .getAlias() - .toStringUtf8()))) - .ethereumHash(ByteString.copyFrom( - spec.registry().getBytes(ETH_HASH_KEY))))))); - } - HapiSpec etx013PrecompileCallFailsWhenSignatureMissingFromBothEthereumAndHederaTxn() { final AtomicReference fungible = new AtomicReference<>(); final String fungibleToken = TOKEN; @@ -917,73 +606,6 @@ final var record = op.getResponseRecord(); .then(); } - private HapiSpec etx007FungibleTokenCreateWithFeesHappyPath() { - final var createdTokenNum = new AtomicLong(); - final var feeCollectorAndAutoRenew = "feeCollectorAndAutoRenew"; - final var contract = "TokenCreateContract"; - final var EXISTING_TOKEN = "EXISTING_TOKEN"; - final var firstTxn = "firstCreateTxn"; - final long DEFAULT_AMOUNT_TO_SEND = 20 * ONE_HBAR; - - return defaultHapiSpec("ETX_007_fungibleTokenCreateWithFeesHappyPath") - .given( - newKeyNamed(SECP_256K1_SOURCE_KEY).shape(SECP_256K1_SHAPE), - cryptoCreate(RELAYER).balance(6 * ONE_MILLION_HBARS), - cryptoTransfer(tinyBarsFromAccountToAlias(GENESIS, SECP_256K1_SOURCE_KEY, ONE_HUNDRED_HBARS)) - .via(AUTO_ACCOUNT_TRANSACTION_NAME), - cryptoCreate(feeCollectorAndAutoRenew) - .keyShape(SigControl.ED25519_ON) - .balance(ONE_HUNDRED_HBARS), - uploadInitCode(contract), - contractCreate(contract).gas(GAS_LIMIT), - tokenCreate(EXISTING_TOKEN).decimals(5), - tokenAssociate(feeCollectorAndAutoRenew, EXISTING_TOKEN)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - ethereumCall( - contract, - "createTokenWithAllCustomFeesAvailable", - spec.registry() - .getKey(SECP_256K1_SOURCE_KEY) - .getECDSASecp256K1() - .toByteArray(), - asHeadlongAddress( - asAddress(spec.registry().getAccountID(feeCollectorAndAutoRenew))), - asHeadlongAddress( - asAddress(spec.registry().getTokenID(EXISTING_TOKEN))), - asHeadlongAddress( - asAddress(spec.registry().getAccountID(feeCollectorAndAutoRenew))), - 8_000_000L) - .via(firstTxn) - .gasLimit(GAS_LIMIT) - .sending(DEFAULT_AMOUNT_TO_SEND) - .alsoSigningWithFullPrefix(feeCollectorAndAutoRenew) - .exposingResultTo(result -> { - log.info("Explicit create result" + " is {}", result[0]); - final var res = (Address) result[0]; - createdTokenNum.set(res.value().longValueExact()); - })))) - .then( - getTxnRecord(firstTxn).andAllChildRecords().logged(), - childRecordsCheck( - firstTxn, - ResponseCodeEnum.SUCCESS, - TransactionRecordAsserts.recordWith().status(ResponseCodeEnum.SUCCESS)), - withOpContext((spec, ignore) -> { - final var op = getTxnRecord(firstTxn); - allRunFor(spec, op); - - final var callResult = op.getResponseRecord().getContractCallResult(); - final var gasUsed = callResult.getGasUsed(); - final var amount = callResult.getAmount(); - final var gasLimit = callResult.getGas(); - Assertions.assertEquals(DEFAULT_AMOUNT_TO_SEND, amount); - Assertions.assertEquals(GAS_LIMIT, gasLimit); - Assertions.assertTrue(gasUsed > 0L); - Assertions.assertTrue(callResult.hasContractID() && callResult.hasSenderId()); - })); - } - private HapiSpec etxSvc003ContractGetBytecodeQueryReturnsDeployedCode() { final var txn = "creation"; final var contract = "EmptyConstructor"; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumV1SecurityModelSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumV1SecurityModelSuite.java new file mode 100644 index 000000000000..be528ba8e15d --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumV1SecurityModelSuite.java @@ -0,0 +1,503 @@ +/* + * Copyright (C) 2022-2023 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.ethereum; + +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +import static com.hedera.services.bdd.spec.assertions.AssertUtils.inOrder; +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.getAliasedAccountInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getLiteralAliasContractInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +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.ethereumCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.ethereumContractCreate; +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.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; +import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromAccountToAlias; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.createLargeFile; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hedera.services.bdd.suites.contract.Utils.asToken; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hedera.services.bdd.suites.crypto.AutoCreateUtils.updateSpecFor; +import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.esaulpaugh.headlong.abi.Address; +import com.google.protobuf.ByteString; +import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; +import com.hedera.node.app.hapi.utils.ethereum.EthTxData; +import com.hedera.node.app.hapi.utils.ethereum.EthTxData.EthTransactionType; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts; +import com.hedera.services.bdd.spec.keys.SigControl; +import com.hedera.services.bdd.spec.transactions.TxnUtils; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; +import com.hederahashgraph.api.proto.java.TokenID; +import com.hederahashgraph.api.proto.java.TokenType; +import java.math.BigInteger; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Assertions; + +@SuppressWarnings("java:S5960") +public class EthereumV1SecurityModelSuite extends HapiSuite { + + private static final Logger log = LogManager.getLogger(EthereumV1SecurityModelSuite.class); + private static final String TOKEN_CREATE_CONTRACT = "NewTokenCreateContract"; + private static final String ERC721_CONTRACT_WITH_HTS_CALLS = "ERC721ContractWithHTSCalls"; + private static final String HELLO_WORLD_MINT_CONTRACT = "HelloWorldMint"; + public static final long GAS_LIMIT = 1_000_000; + + private static final String AUTO_ACCOUNT_TRANSACTION_NAME = "autoAccount"; + private static final String TOKEN = "token"; + private static final String MINT_TXN = "mintTxn"; + + public static void main(String... args) { + new EthereumV1SecurityModelSuite().runSuiteSync(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + public List getSpecsInSuite() { + return List.of( + etx007FungibleTokenCreateWithFeesHappyPath(), + etx012PrecompileCallSucceedsWhenNeededSignatureInEthTxn(), + etx013PrecompileCallSucceedsWhenNeededSignatureInHederaTxn(), + setApproveForAllUsingLocalNodeSetupPasses()); + } + + private HapiSpec setApproveForAllUsingLocalNodeSetupPasses() { + final AtomicReference spenderAutoCreatedAccountId = new AtomicReference<>(); + final AtomicReference tokenCreateContractID = new AtomicReference<>(); + final AtomicReference erc721ContractID = new AtomicReference<>(); + final AtomicReference contractAddressID = new AtomicReference<>(); + final AtomicReference createdTokenAddressString = new AtomicReference<>(); + final String spenderAlias = "spenderAlias"; + final var createTokenContractNum = new AtomicLong(); + return propertyPreservingHapiSpec("SetApproveForAllUsingLocalNodeSetupPasses") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(SECP_256K1_SOURCE_KEY).shape(SECP_256K1_SHAPE), + newKeyNamed(spenderAlias).shape(SECP_256K1_SHAPE), + cryptoCreate(RELAYER).balance(6 * ONE_MILLION_HBARS), + cryptoTransfer(tinyBarsFromAccountToAlias(GENESIS, SECP_256K1_SOURCE_KEY, ONE_MILLION_HBARS)) + .via(AUTO_ACCOUNT_TRANSACTION_NAME), + cryptoTransfer(tinyBarsFromAccountToAlias(GENESIS, spenderAlias, ONE_HUNDRED_HBARS)) + .via("autoAccountSpender"), + getAliasedAccountInfo(spenderAlias) + .exposingContractAccountIdTo(spenderAutoCreatedAccountId::set), + createLargeFile( + GENESIS, TOKEN_CREATE_CONTRACT, TxnUtils.literalInitcodeFor(TOKEN_CREATE_CONTRACT)), + ethereumContractCreate(TOKEN_CREATE_CONTRACT) + .type(EthTransactionType.EIP1559) + .signingWith(SECP_256K1_SOURCE_KEY) + .payingWith(RELAYER) + .nonce(0) + .bytecode(TOKEN_CREATE_CONTRACT) + .gasPrice(10L) + .maxGasAllowance(ONE_HUNDRED_HBARS) + .gasLimit(1_000_000L) + .gas(1_000_000L) + .hasKnownStatusFrom(SUCCESS) + .exposingNumTo(createTokenContractNum::set), + getContractInfo(TOKEN_CREATE_CONTRACT).exposingEvmAddress(tokenCreateContractID::set)) + .when( + withOpContext((spec, opLog) -> { + var createNFTPublicFunctionCall = ethereumCall( + TOKEN_CREATE_CONTRACT, + "createNonFungibleTokenPublic", + asHeadlongAddress(tokenCreateContractID.get())) + .type(EthTransactionType.EIP1559) + .signingWith(SECP_256K1_SOURCE_KEY) + .payingWith(RELAYER) + .nonce(1) + .gasPrice(10L) + .sending(10000000000L) + .gasLimit(1_000_000L) + .via("createTokenTxn") + .exposingEventDataTo(createdTokenAddressString::set); + + allRunFor(spec, createNFTPublicFunctionCall); + + var uploadEthereumContract = uploadInitCode(ERC721_CONTRACT_WITH_HTS_CALLS); + allRunFor(spec, uploadEthereumContract); + + var createEthereumContract = ethereumContractCreate(ERC721_CONTRACT_WITH_HTS_CALLS) + .type(EthTxData.EthTransactionType.EIP1559) + .signingWith(SECP_256K1_SOURCE_KEY) + .payingWith(RELAYER) + .nonce(2) + .gasPrice(10L) + .maxGasAllowance(ONE_HUNDRED_HBARS) + .gasLimit(1_000_000L) + .hasKnownStatusFrom(SUCCESS); + + var exposeEthereumContractAddress = getContractInfo(ERC721_CONTRACT_WITH_HTS_CALLS) + .exposingEvmAddress(address -> erc721ContractID.set("0x" + address)); + allRunFor(spec, createEthereumContract, exposeEthereumContractAddress); + + var contractInfo = getLiteralAliasContractInfo( + erc721ContractID.get().substring(2)) + .exposingEvmAddress(contractAddressID::set); + allRunFor(spec, contractInfo); + assertEquals(erc721ContractID.get().substring(2), contractAddressID.get()); + }), + withOpContext((spec, opLog) -> { + var associateTokenToERC721 = ethereumCall( + ERC721_CONTRACT_WITH_HTS_CALLS, + "associateTokenPublic", + asHeadlongAddress(erc721ContractID.get()), + asHeadlongAddress(Bytes.wrap(createdTokenAddressString + .get() + .toByteArray()) + .toHexString())) + .type(EthTransactionType.EIP1559) + .signingWith(SECP_256K1_SOURCE_KEY) + .payingWith(GENESIS) + .nonce(3) + .gasPrice(10L) + .gasLimit(1_000_000L) + .via("associateTokenTxn") + .hasKnownStatusFrom(SUCCESS); + + var associateTokenToSpender = ethereumCall( + TOKEN_CREATE_CONTRACT, + "associateTokenPublic", + asHeadlongAddress(spenderAutoCreatedAccountId.get()), + asHeadlongAddress(Bytes.wrap(createdTokenAddressString + .get() + .toByteArray()) + .toHexString())) + .type(EthTransactionType.EIP1559) + .signingWith(spenderAlias) + .payingWith(GENESIS) + .nonce(0) + .gasPrice(10L) + .gasLimit(1_000_000L) + .via("associateTokenTxn") + .hasKnownStatusFrom(SUCCESS); + + var isApprovedForAllBefore = ethereumCall( + ERC721_CONTRACT_WITH_HTS_CALLS, + "ercIsApprovedForAll", + asHeadlongAddress(Bytes.wrap(createdTokenAddressString + .get() + .toByteArray()) + .toHexString()), + asHeadlongAddress(erc721ContractID.get()), + asHeadlongAddress(spenderAutoCreatedAccountId.get())) + .type(EthTransactionType.EIP1559) + .signingWith(SECP_256K1_SOURCE_KEY) + .payingWith(RELAYER) + .nonce(4) + .gasPrice(10L) + .gasLimit(1_000_000L) + .via("ercIsApprovedForAllBeforeTxn") + .hasKnownStatusFrom(SUCCESS) + .logged(); + + var isApprovedForAllBeforeCheck = childRecordsCheck( + "ercIsApprovedForAllBeforeTxn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.ERC_IS_APPROVED_FOR_ALL) + .withIsApprovedForAll(false)))); + + var setApprovalForAll = ethereumCall( + ERC721_CONTRACT_WITH_HTS_CALLS, + "ercSetApprovalForAll", + asHeadlongAddress(Bytes.wrap(createdTokenAddressString + .get() + .toByteArray()) + .toHexString()), + asHeadlongAddress(spenderAutoCreatedAccountId.get()), + true) + .type(EthTransactionType.EIP1559) + .signingWith(SECP_256K1_SOURCE_KEY) + .payingWith(RELAYER) + .nonce(5) + .gasPrice(10L) + .gasLimit(1_000_000L) + .via("ercSetApproveForAllTxn") + .hasKnownStatusFrom(SUCCESS) + .logged(); + + var isApprovedForAllAfter = ethereumCall( + ERC721_CONTRACT_WITH_HTS_CALLS, + "ercIsApprovedForAll", + asHeadlongAddress(Bytes.wrap(createdTokenAddressString + .get() + .toByteArray()) + .toHexString()), + asHeadlongAddress(erc721ContractID.get()), + asHeadlongAddress(spenderAutoCreatedAccountId.get())) + .type(EthTransactionType.EIP1559) + .signingWith(SECP_256K1_SOURCE_KEY) + .payingWith(RELAYER) + .nonce(6) + .gasPrice(10L) + .gasLimit(1_000_000L) + .via("ercIsApprovedForAllAfterTxn") + .hasKnownStatusFrom(SUCCESS) + .logged(); + + var isApprovedForAllAfterCheck = childRecordsCheck( + "ercIsApprovedForAllAfterTxn", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.ERC_IS_APPROVED_FOR_ALL) + .withIsApprovedForAll(true)))); + + allRunFor( + spec, + associateTokenToERC721, + associateTokenToSpender, + isApprovedForAllBefore, + isApprovedForAllBeforeCheck, + setApprovalForAll, + isApprovedForAllAfter, + isApprovedForAllAfterCheck); + })) + .then(withOpContext((spec, opLog) -> {})); + } + + private HapiSpec etx012PrecompileCallSucceedsWhenNeededSignatureInEthTxn() { + final AtomicReference fungible = new AtomicReference<>(); + final String fungibleToken = TOKEN; + final String mintTxn = MINT_TXN; + return propertyPreservingHapiSpec("etx012PrecompileCallSucceedsWhenNeededSignatureInEthTxn") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenMint", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(SECP_256K1_SOURCE_KEY).shape(SECP_256K1_SHAPE), + cryptoCreate(RELAYER).balance(6 * ONE_MILLION_HBARS), + cryptoTransfer(tinyBarsFromAccountToAlias(GENESIS, SECP_256K1_SOURCE_KEY, ONE_HUNDRED_HBARS)) + .via(AUTO_ACCOUNT_TRANSACTION_NAME), + withOpContext((spec, opLog) -> updateSpecFor(spec, SECP_256K1_SOURCE_KEY)), + getTxnRecord(AUTO_ACCOUNT_TRANSACTION_NAME).andAllChildRecords(), + uploadInitCode(HELLO_WORLD_MINT_CONTRACT), + tokenCreate(fungibleToken) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(0) + .adminKey(SECP_256K1_SOURCE_KEY) + .supplyKey(SECP_256K1_SOURCE_KEY) + .exposingCreatedIdTo(idLit -> fungible.set(asToken(idLit)))) + .when( + sourcing(() -> contractCreate( + HELLO_WORLD_MINT_CONTRACT, asHeadlongAddress(asAddress(fungible.get())))), + ethereumCall(HELLO_WORLD_MINT_CONTRACT, "brrr", BigInteger.valueOf(5)) + .type(EthTxData.EthTransactionType.EIP1559) + .signingWith(SECP_256K1_SOURCE_KEY) + .payingWith(RELAYER) + .nonce(0) + .gasPrice(50L) + .maxGasAllowance(FIVE_HBARS) + .gasLimit(1_000_000L) + .via(mintTxn) + .hasKnownStatus(SUCCESS)) + .then(withOpContext((spec, opLog) -> allRunFor( + spec, + getTxnRecord(mintTxn) + .logged() + .hasPriority(recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .logs(inOrder()) + .senderId(spec.registry() + .getAccountID(spec.registry() + .aliasIdFor(SECP_256K1_SOURCE_KEY) + .getAlias() + .toStringUtf8()))) + .ethereumHash(ByteString.copyFrom( + spec.registry().getBytes(ETH_HASH_KEY))))))); + } + + private HapiSpec etx013PrecompileCallSucceedsWhenNeededSignatureInHederaTxn() { + final AtomicReference fungible = new AtomicReference<>(); + final String fungibleToken = TOKEN; + final String mintTxn = MINT_TXN; + final String MULTI_KEY = "MULTI_KEY"; + return propertyPreservingHapiSpec("etx013PrecompileCallSucceedsWhenNeededSignatureInHederaTxn") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate,TokenMint", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(MULTI_KEY), + newKeyNamed(SECP_256K1_SOURCE_KEY).shape(SECP_256K1_SHAPE), + cryptoCreate(RELAYER).balance(6 * ONE_MILLION_HBARS), + cryptoTransfer(tinyBarsFromAccountToAlias(GENESIS, SECP_256K1_SOURCE_KEY, ONE_HUNDRED_HBARS)) + .via(AUTO_ACCOUNT_TRANSACTION_NAME), + withOpContext((spec, opLog) -> updateSpecFor(spec, SECP_256K1_SOURCE_KEY)), + getTxnRecord(AUTO_ACCOUNT_TRANSACTION_NAME).andAllChildRecords(), + uploadInitCode(HELLO_WORLD_MINT_CONTRACT), + tokenCreate(fungibleToken) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(0) + .adminKey(MULTI_KEY) + .supplyKey(MULTI_KEY) + .exposingCreatedIdTo(idLit -> fungible.set(asToken(idLit)))) + .when( + sourcing(() -> contractCreate( + HELLO_WORLD_MINT_CONTRACT, asHeadlongAddress(asAddress(fungible.get())))), + ethereumCall(HELLO_WORLD_MINT_CONTRACT, "brrr", BigInteger.valueOf(5)) + .type(EthTxData.EthTransactionType.EIP1559) + .signingWith(SECP_256K1_SOURCE_KEY) + .payingWith(RELAYER) + .alsoSigningWithFullPrefix(MULTI_KEY) + .nonce(0) + .gasPrice(50L) + .maxGasAllowance(FIVE_HBARS) + .gasLimit(1_000_000L) + .via(mintTxn) + .hasKnownStatus(SUCCESS)) + .then(withOpContext((spec, opLog) -> allRunFor( + spec, + getTxnRecord(mintTxn) + .logged() + .hasPriority(recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .logs(inOrder()) + .senderId(spec.registry() + .getAccountID(spec.registry() + .aliasIdFor(SECP_256K1_SOURCE_KEY) + .getAlias() + .toStringUtf8()))) + .ethereumHash(ByteString.copyFrom( + spec.registry().getBytes(ETH_HASH_KEY))))))); + } + + private HapiSpec etx007FungibleTokenCreateWithFeesHappyPath() { + final var createdTokenNum = new AtomicLong(); + final var feeCollectorAndAutoRenew = "feeCollectorAndAutoRenew"; + final var contract = "TokenCreateContract"; + final var EXISTING_TOKEN = "EXISTING_TOKEN"; + final var firstTxn = "firstCreateTxn"; + final long DEFAULT_AMOUNT_TO_SEND = 20 * ONE_HBAR; + + return propertyPreservingHapiSpec("etx007FungibleTokenCreateWithFeesHappyPath") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "ContractCall,CryptoTransfer,TokenAssociateToAccount,TokenCreate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + newKeyNamed(SECP_256K1_SOURCE_KEY).shape(SECP_256K1_SHAPE), + cryptoCreate(RELAYER).balance(6 * ONE_MILLION_HBARS), + cryptoTransfer(tinyBarsFromAccountToAlias(GENESIS, SECP_256K1_SOURCE_KEY, ONE_HUNDRED_HBARS)) + .via(AUTO_ACCOUNT_TRANSACTION_NAME), + cryptoCreate(feeCollectorAndAutoRenew) + .keyShape(SigControl.ED25519_ON) + .balance(ONE_HUNDRED_HBARS), + uploadInitCode(contract), + contractCreate(contract).gas(GAS_LIMIT), + tokenCreate(EXISTING_TOKEN).decimals(5), + tokenAssociate(feeCollectorAndAutoRenew, EXISTING_TOKEN)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + ethereumCall( + contract, + "createTokenWithAllCustomFeesAvailable", + spec.registry() + .getKey(SECP_256K1_SOURCE_KEY) + .getECDSASecp256K1() + .toByteArray(), + asHeadlongAddress( + asAddress(spec.registry().getAccountID(feeCollectorAndAutoRenew))), + asHeadlongAddress( + asAddress(spec.registry().getTokenID(EXISTING_TOKEN))), + asHeadlongAddress( + asAddress(spec.registry().getAccountID(feeCollectorAndAutoRenew))), + 8_000_000L) + .via(firstTxn) + .gasLimit(GAS_LIMIT) + .sending(DEFAULT_AMOUNT_TO_SEND) + .alsoSigningWithFullPrefix(feeCollectorAndAutoRenew) + .exposingResultTo(result -> { + log.info("Explicit create result" + " is {}", result[0]); + final var res = (Address) result[0]; + createdTokenNum.set(res.value().longValueExact()); + })))) + .then( + getTxnRecord(firstTxn).andAllChildRecords().logged(), + childRecordsCheck( + firstTxn, + ResponseCodeEnum.SUCCESS, + TransactionRecordAsserts.recordWith().status(ResponseCodeEnum.SUCCESS)), + withOpContext((spec, ignore) -> { + final var op = getTxnRecord(firstTxn); + allRunFor(spec, op); + + final var callResult = op.getResponseRecord().getContractCallResult(); + final var gasUsed = callResult.getGasUsed(); + final var amount = callResult.getAmount(); + final var gasLimit = callResult.getGas(); + Assertions.assertEquals(DEFAULT_AMOUNT_TO_SEND, amount); + Assertions.assertEquals(GAS_LIMIT, gasLimit); + Assertions.assertTrue(gasUsed > 0L); + Assertions.assertTrue(callResult.hasContractID() && callResult.hasSenderId()); + })); + } + + @Override + protected Logger getResultsLogger() { + return log; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/leaky/LeakyContractTestsSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/leaky/LeakyContractTestsSuite.java index 8d55be3013df..ad964859b452 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/leaky/LeakyContractTestsSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/leaky/LeakyContractTestsSuite.java @@ -95,6 +95,7 @@ import static com.hedera.services.bdd.suites.contract.Utils.asToken; import static com.hedera.services.bdd.suites.contract.Utils.eventSignatureOf; import static com.hedera.services.bdd.suites.contract.Utils.getABIFor; +import static com.hedera.services.bdd.suites.contract.Utils.mirrorAddrWith; import static com.hedera.services.bdd.suites.contract.Utils.parsedToByteString; import static com.hedera.services.bdd.suites.contract.hapi.ContractCallSuite.ACCOUNT_INFO; import static com.hedera.services.bdd.suites.contract.hapi.ContractCallSuite.ACCOUNT_INFO_AFTER_CALL; @@ -145,8 +146,7 @@ import static com.hedera.services.bdd.suites.contract.precompile.ERCPrecompileSuite.TRANSFER_SIGNATURE; import static com.hedera.services.bdd.suites.contract.precompile.ERCPrecompileSuite.TRANSFER_SIG_NAME; import static com.hedera.services.bdd.suites.contract.precompile.LazyCreateThroughPrecompileSuite.FIRST_META; -import static com.hedera.services.bdd.suites.contract.precompile.LazyCreateThroughPrecompileSuite.mirrorAddrWith; -import static com.hedera.services.bdd.suites.contract.precompile.WipeTokenAccountPrecompileSuite.GAS_TO_OFFER; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; import static com.hedera.services.bdd.suites.crypto.CryptoApproveAllowanceSuite.ADMIN_KEY; import static com.hedera.services.bdd.suites.crypto.CryptoApproveAllowanceSuite.FUNGIBLE_TOKEN; import static com.hedera.services.bdd.suites.crypto.CryptoApproveAllowanceSuite.NON_FUNGIBLE_TOKEN; @@ -225,6 +225,7 @@ import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.Assertions; +@SuppressWarnings("java:S1192") // "string literal should not be duplicated" - this rule makes test suites worse public class LeakyContractTestsSuite extends HapiSuite { private static final Logger log = LogManager.getLogger(LeakyContractTestsSuite.class); public static final String CONTRACTS_MAX_REFUND_PERCENT_OF_GAS_LIMIT1 = "contracts.maxRefundPercentOfGasLimit"; @@ -241,6 +242,7 @@ public class LeakyContractTestsSuite extends HapiSuite { private static final String TRANSFER_WORKS_WITH_TOP_LEVEL_SIGNATURES = "transferWorksWithTopLevelSignatures"; private static final String TRANSFER_TOKEN_PUBLIC = "transferTokenPublic"; private static final String HEDERA_ALLOWANCES_IS_ENABLED = "hedera.allowances.isEnabled"; + public static final int GAS_TO_OFFER = 1_000_000; public static void main(String... args) { new LeakyContractTestsSuite().runSuiteSync(); @@ -268,7 +270,6 @@ public List getSpecsInSuite() { contractCreationStoragePriceMatchesFinalExpiry(), createTokenWithInvalidFixedFeeWithERC721Denomination(), maxRefundIsMaxGasRefundConfiguredWhenTXGasPriceIsSmaller(), - accountWithoutAliasCanMakeEthTxnsDueToAutomaticAliasCreation(), createMaxRefundIsMaxGasRefundConfiguredWhenTXGasPriceIsSmaller(), lazyCreateThroughPrecompileNotSupportedWhenFlagDisabled(), evmLazyCreateViaSolidityCall(), @@ -600,12 +601,16 @@ private HapiSpec transferWorksWithTopLevelSignatures() { final AtomicReference accountID = new AtomicReference<>(); final AtomicReference vanillaTokenID = new AtomicReference<>(); final AtomicReference vanillaNftID = new AtomicReference<>(); - return propertyPreservingHapiSpec(TRANSFER_WORKS_WITH_TOP_LEVEL_SIGNATURES) - .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS) + return propertyPreservingHapiSpec("transferWorksWithTopLevelSignatures") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) .given( // enable top level signatures for // transferToken/transferTokens/transferNft/transferNfts - overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CRYPTO_TRANSFER), + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + CRYPTO_TRANSFER, + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + "10_000_000"), newKeyNamed(SUPPLY_KEY), cryptoCreate(ACCOUNT).exposingCreatedIdTo(accountID::set), cryptoCreate(TOKEN_TREASURY), @@ -831,9 +836,10 @@ HapiSpec payerCannotOverSendValue() { private HapiSpec createTokenWithInvalidFeeCollector() { return propertyPreservingHapiSpec("createTokenWithInvalidFeeCollector") - .preserving(CRYPTO_CREATE_WITH_ALIAS_ENABLED) + .preserving(CRYPTO_CREATE_WITH_ALIAS_ENABLED, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) .given( overriding(CRYPTO_CREATE_WITH_ALIAS_ENABLED, FALSE_VALUE), + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, "10_000_000"), newKeyNamed(ECDSA_KEY).shape(SECP256K1), cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(ECDSA_KEY), uploadInitCode(TOKEN_CREATE_CONTRACT), @@ -879,9 +885,10 @@ private HapiSpec createTokenWithInvalidFixedFeeWithERC721Denomination() { final String feeCollector = ACCOUNT_2; final String someARAccount = "someARAccount"; return propertyPreservingHapiSpec("createTokenWithInvalidFixedFeeWithERC721Denomination") - .preserving(CRYPTO_CREATE_WITH_ALIAS_ENABLED) + .preserving(CRYPTO_CREATE_WITH_ALIAS_ENABLED, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) .given( overriding(CRYPTO_CREATE_WITH_ALIAS_ENABLED, FALSE_VALUE), + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, "10_000_000"), newKeyNamed(ECDSA_KEY).shape(SECP256K1), cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(ECDSA_KEY), cryptoCreate(feeCollector).keyShape(ED25519_ON).balance(ONE_HUNDRED_HBARS), @@ -934,9 +941,10 @@ private HapiSpec createTokenWithInvalidRoyaltyFee() { AtomicReference existingToken = new AtomicReference<>(); final String treasuryAndFeeCollectorKey = "treasuryAndFeeCollectorKey"; return propertyPreservingHapiSpec("createTokenWithInvalidRoyaltyFee") - .preserving(CRYPTO_CREATE_WITH_ALIAS_ENABLED) + .preserving(CRYPTO_CREATE_WITH_ALIAS_ENABLED, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) .given( overriding(CRYPTO_CREATE_WITH_ALIAS_ENABLED, FALSE_VALUE), + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, "10_000_000"), newKeyNamed(ECDSA_KEY).shape(SECP256K1), newKeyNamed(ED25519KEY).shape(ED25519), newKeyNamed(CONTRACT_ADMIN_KEY), @@ -992,9 +1000,10 @@ private HapiSpec nonFungibleTokenCreateWithFeesHappyPath() { final var feeCollector = ACCOUNT_2; final var treasuryAndFeeCollectorKey = "treasuryAndFeeCollectorKey"; return propertyPreservingHapiSpec("nonFungibleTokenCreateWithFeesHappyPath") - .preserving(CRYPTO_CREATE_WITH_ALIAS_ENABLED) + .preserving(CRYPTO_CREATE_WITH_ALIAS_ENABLED, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) .given( overriding(CRYPTO_CREATE_WITH_ALIAS_ENABLED, FALSE_VALUE), + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, "10_000_000"), newKeyNamed(ECDSA_KEY).shape(SECP256K1), newKeyNamed(ED25519KEY).shape(ED25519), newKeyNamed(treasuryAndFeeCollectorKey), @@ -1078,9 +1087,10 @@ private HapiSpec fungibleTokenCreateWithFeesHappyPath() { final var arEd25519Key = "arEd25519Key"; final var initialAutoRenewAccount = "initialAutoRenewAccount"; return propertyPreservingHapiSpec("fungibleTokenCreateWithFeesHappyPath") - .preserving(CRYPTO_CREATE_WITH_ALIAS_ENABLED) + .preserving(CRYPTO_CREATE_WITH_ALIAS_ENABLED, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) .given( overriding(CRYPTO_CREATE_WITH_ALIAS_ENABLED, FALSE_VALUE), + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, "10_000_000"), newKeyNamed(arEd25519Key).shape(ED25519), newKeyNamed(ECDSA_KEY).shape(SECP256K1), cryptoCreate(initialAutoRenewAccount).key(arEd25519Key), @@ -1154,11 +1164,13 @@ private HapiSpec fungibleTokenCreateWithFeesHappyPath() { })); } - HapiSpec accountWithoutAliasCanMakeEthTxnsDueToAutomaticAliasCreation() { + private HapiSpec etx026AccountWithoutAliasCanMakeEthTxnsDueToAutomaticAliasCreation() { final String ACCOUNT = "account"; - return defaultHapiSpec("ETX_026_accountWithoutAliasCanMakeEthTxnsDueToAutomaticAliasCreation") + return propertyPreservingHapiSpec("etx026AccountWithoutAliasCanMakeEthTxnsDueToAutomaticAliasCreation") + .preserving(CRYPTO_CREATE_WITH_ALIAS_ENABLED, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) .given( overriding(CRYPTO_CREATE_WITH_ALIAS_ENABLED, FALSE_VALUE), + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, "10_000_000"), newKeyNamed(SECP_256K1_SOURCE_KEY).shape(SECP_256K1_SHAPE), cryptoCreate(ACCOUNT).key(SECP_256K1_SOURCE_KEY).balance(ONE_HUNDRED_HBARS)) .when(ethereumContractCreate(PAY_RECEIVABLE_CONTRACT) @@ -1742,7 +1754,7 @@ private HapiSpec requiresTopLevelSignatureOrApprovalDependingOnControllingProper final AtomicReference
tokenAddress = new AtomicReference<>(); final var amountPerTransfer = 50L; return propertyPreservingHapiSpec("RequiresTopLevelSignatureOrApprovalDependingOnControllingProperty") - .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS) + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) .given( cryptoCreate(SENDER) .keyShape(SECP256K1_ON) @@ -1763,7 +1775,8 @@ private HapiSpec requiresTopLevelSignatureOrApprovalDependingOnControllingProper uploadInitCode(TRANSFER_CONTRACT), contractCreate(TRANSFER_CONTRACT), // First revoke use of top-level signatures from all precompiles - overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, "")) + overriding(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, ""), + overriding(CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, "10_000_000")) .when( // Then, try to transfer tokens using a top-level signature sourcing(() -> contractCall(TRANSFER_CONTRACT, TRANSFER_MULTIPLE_TOKENS, (Object) new Tuple[] { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/leaky/LeakySecurityModelV1Suite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/leaky/LeakySecurityModelV1Suite.java new file mode 100644 index 000000000000..0a6859c4b799 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/leaky/LeakySecurityModelV1Suite.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 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.leaky; + +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.suites.HapiSuite; +import com.hedera.services.bdd.suites.contract.hapi.ContractCallV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.hapi.ContractCreateV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.opcodes.Create2OperationV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.AssociatePrecompileV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.ContractBurnHTSV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.ContractHTSV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.ContractKeysHTSV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.ContractMintHTSV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.CreatePrecompileV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.CryptoTransferHTSV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.DeleteTokenPrecompileV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.DissociatePrecompileV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.ERCPrecompileV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.FreezeUnfreezeTokenPrecompileV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.GrantRevokeKycV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.LazyCreateThroughPrecompileV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.MixedHTSPrecompileTestsV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.PauseUnpauseTokenAccountPrecompileV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.SigningReqsV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.TokenExpiryInfoV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.TokenInfoHTSV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.TokenUpdatePrecompileV1SecurityModelSuite; +import com.hedera.services.bdd.suites.contract.precompile.WipeTokenAccountPrecompileV1SecurityModelSuite; +import com.hedera.services.bdd.suites.ethereum.EthereumV1SecurityModelSuite; +import com.hedera.services.bdd.suites.token.TokenAssociationV1SecurityModelSpecs; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class LeakySecurityModelV1Suite extends HapiSuite { + + private static final Logger log = LogManager.getLogger(LeakySecurityModelV1Suite.class); + + public static void main(String... args) { + new LeakySecurityModelV1Suite().runSuiteSync(); + } + + @NonNull + final List suites; + + public LeakySecurityModelV1Suite() { + suites = List.of( + new AssociatePrecompileV1SecurityModelSuite(), + new ContractBurnHTSV1SecurityModelSuite(), + new ContractCallV1SecurityModelSuite(), + new ContractCreateV1SecurityModelSuite(), + new ContractHTSV1SecurityModelSuite(), + new ContractKeysHTSV1SecurityModelSuite(), + new ContractMintHTSV1SecurityModelSuite(), + new Create2OperationV1SecurityModelSuite(), + new CreatePrecompileV1SecurityModelSuite(), + new CryptoTransferHTSV1SecurityModelSuite(), + new DeleteTokenPrecompileV1SecurityModelSuite(), + new DissociatePrecompileV1SecurityModelSuite(), + new ERCPrecompileV1SecurityModelSuite(), + new EthereumV1SecurityModelSuite(), + new FreezeUnfreezeTokenPrecompileV1SecurityModelSuite(), + new GrantRevokeKycV1SecurityModelSuite(), + new LazyCreateThroughPrecompileV1SecurityModelSuite(), + new MixedHTSPrecompileTestsV1SecurityModelSuite(), + new PauseUnpauseTokenAccountPrecompileV1SecurityModelSuite(), + new SigningReqsV1SecurityModelSuite(), + new TokenAssociationV1SecurityModelSpecs(), + new TokenExpiryInfoV1SecurityModelSuite(), + new TokenInfoHTSV1SecurityModelSuite(), + new TokenUpdatePrecompileV1SecurityModelSuite(), + new WipeTokenAccountPrecompileV1SecurityModelSuite()); + } + + @Override + public List getSpecsInSuite() { + return suites.stream() + .map(HapiSuite::getSpecsInSuite) + .flatMap(List::stream) + .toList(); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + @Override + protected Logger getResultsLogger() { + return log; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/regression/PrecompileMintThrottlingCheck.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/regression/PrecompileMintThrottlingCheck.java index e5b40878c28a..552532730f3f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/regression/PrecompileMintThrottlingCheck.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/regression/PrecompileMintThrottlingCheck.java @@ -26,7 +26,6 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.runWithProvider; import static com.hedera.services.bdd.suites.contract.precompile.ContractMintHTSSuite.MINT_NFT_CONTRACT; -import static com.hedera.services.bdd.suites.contract.precompile.WipeTokenAccountPrecompileSuite.GAS_TO_OFFER; import static com.hedera.services.bdd.suites.utils.sysfiles.serdes.ThrottleDefsLoader.protoDefsFromResource; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; @@ -61,6 +60,7 @@ public class PrecompileMintThrottlingCheck extends HapiSuite { private static final int EXPECTED_MAX_MINTS_PER_SEC = 50; private static final double ALLOWED_THROTTLE_NOISE_TOLERANCE = 0.05; private static final String NON_FUNGIBLE_TOKEN = "NON_FUNGIBLE_TOKEN"; + public static final int GAS_TO_OFFER = 1_000_000; public static void main(String... args) { new PrecompileMintThrottlingCheck().runSuiteSync(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenAssociationSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenAssociationSpecs.java index 8adcf5c500c7..b5d09a32dc8f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenAssociationSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenAssociationSpecs.java @@ -16,14 +16,12 @@ package com.hedera.services.bdd.suites.token; -import static com.hedera.services.bdd.spec.HapiPropertySource.asHexedSolidityAddress; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; import static com.hedera.services.bdd.spec.assertions.NoTokenTransfers.emptyTokenTransfers; import static com.hedera.services.bdd.spec.assertions.SomeFungibleTransfers.changingFungibleBalances; import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith; import static com.hedera.services.bdd.spec.queries.QueryVerbs.*; import static com.hedera.services.bdd.spec.queries.crypto.ExpectedTokenRel.relationshipWith; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createDefaultContract; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; @@ -36,7 +34,6 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenFreeze; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenUnfreeze; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; -import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; 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.*; @@ -47,9 +44,7 @@ import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; import static org.junit.jupiter.api.Assertions.assertEquals; -import com.esaulpaugh.headlong.abi.Address; import com.google.protobuf.ByteString; -import com.hedera.services.bdd.spec.HapiPropertySource; import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.HapiSpecOperation; import com.hedera.services.bdd.spec.assertions.BaseErroringAssertsProvider; @@ -62,7 +57,6 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -101,8 +95,7 @@ public List getSpecsInSuite() { contractInfoQueriesAsExpected(), dissociateHasExpectedSemanticsForDeletedTokens(), dissociateHasExpectedSemanticsForDissociatedContracts(), - canDissociateFromDeletedTokenWithAlreadyDissociatedTreasury(), - multiAssociationWithSameRepeatedTokenAsExpected()); + canDissociateFromDeletedTokenWithAlreadyDissociatedTreasury()); } @Override @@ -110,45 +103,6 @@ public boolean canRunConcurrent() { return true; } - private HapiSpec multiAssociationWithSameRepeatedTokenAsExpected() { - final var nfToken = "nfToken"; - final var civilian = "civilian"; - final var multiAssociate = "multiAssociate"; - final var theContract = "AssociateDissociate"; - final AtomicReference tokenMirrorAddr = new AtomicReference<>(); - final AtomicReference civilianMirrorAddr = new AtomicReference<>(); - - return defaultHapiSpec("MultiAssociationWithSameRepeatedTokenAsExpected") - .given( - cryptoCreate(civilian) - .exposingCreatedIdTo(id -> civilianMirrorAddr.set(asHexedSolidityAddress(id))), - tokenCreate(nfToken) - .tokenType(NON_FUNGIBLE_UNIQUE) - .supplyKey(GENESIS) - .initialSupply(0) - .exposingCreatedIdTo(idLit -> - tokenMirrorAddr.set(asHexedSolidityAddress(HapiPropertySource.asToken(idLit)))), - uploadInitCode(theContract), - contractCreate(theContract)) - .when(sourcing(() -> contractCall( - theContract, - "tokensAssociate", - asHeadlongAddress(civilianMirrorAddr.get()), - (new Address[] { - asHeadlongAddress(tokenMirrorAddr.get()), asHeadlongAddress(tokenMirrorAddr.get()) - })) - .hasKnownStatus(CONTRACT_REVERT_EXECUTED) - .via(multiAssociate) - .payingWith(civilian) - .gas(4_000_000))) - .then( - childRecordsCheck( - multiAssociate, - CONTRACT_REVERT_EXECUTED, - recordWith().status(TOKEN_ID_REPEATED_IN_TOKEN_LIST)), - getAccountInfo(civilian).hasNoTokenRelationship(nfToken)); - } - public HapiSpec handlesUseOfDefaultTokenId() { return defaultHapiSpec("HandlesUseOfDefaultTokenId") .given() diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenAssociationV1SecurityModelSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenAssociationV1SecurityModelSpecs.java new file mode 100644 index 000000000000..c9f3a4e74b1a --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/token/TokenAssociationV1SecurityModelSpecs.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2020-2023 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.token; + +import static com.hedera.services.bdd.spec.HapiPropertySource.asHexedSolidityAddress; +import static com.hedera.services.bdd.spec.HapiSpec.propertyPreservingHapiSpec; +import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.*; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +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.contract.HapiParserUtil.asHeadlongAddress; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.*; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS; +import static com.hedera.services.bdd.suites.contract.precompile.V1SecurityModelOverrides.CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.*; +import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; + +import com.esaulpaugh.headlong.abi.Address; +import com.hedera.services.bdd.spec.HapiPropertySource; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.suites.HapiSuite; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class TokenAssociationV1SecurityModelSpecs extends HapiSuite { + private static final Logger log = LogManager.getLogger(TokenAssociationV1SecurityModelSpecs.class); + + public static final String VANILLA_TOKEN = "TokenD"; + public static final String MULTI_KEY = "multiKey"; + public static final String SIMPLE = "simple"; + public static final String FREEZE_KEY = "freezeKey"; + + public static void main(String... args) { + final var spec = new TokenAssociationV1SecurityModelSpecs(); + + spec.deferResultsSummary(); + spec.runSuiteSync(); + spec.summarizeDeferredResults(); + } + + @Override + public List getSpecsInSuite() { + return List.of(multiAssociationWithSameRepeatedTokenAsExpected()); + } + + @Override + public boolean canRunConcurrent() { + return false; + } + + private HapiSpec multiAssociationWithSameRepeatedTokenAsExpected() { + final var nfToken = "nfToken"; + final var civilian = "civilian"; + final var multiAssociate = "multiAssociate"; + final var theContract = "AssociateDissociate"; + final AtomicReference tokenMirrorAddr = new AtomicReference<>(); + final AtomicReference civilianMirrorAddr = new AtomicReference<>(); + + return propertyPreservingHapiSpec("MultiAssociationWithSameRepeatedTokenAsExpected") + .preserving(CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS) + .given( + overridingTwo( + CONTRACTS_ALLOW_SYSTEM_USE_OF_HAPI_SIGS, + "CryptoTransfer,TokenAssociateToAccount,TokenCreate", + CONTRACTS_MAX_NUM_WITH_HAPI_SIGS_ACCESS, + CONTRACTS_V1_SECURITY_MODEL_BLOCK_CUTOFF), + cryptoCreate(civilian) + .exposingCreatedIdTo(id -> civilianMirrorAddr.set(asHexedSolidityAddress(id))), + tokenCreate(nfToken) + .tokenType(NON_FUNGIBLE_UNIQUE) + .supplyKey(GENESIS) + .initialSupply(0) + .exposingCreatedIdTo(idLit -> + tokenMirrorAddr.set(asHexedSolidityAddress(HapiPropertySource.asToken(idLit)))), + uploadInitCode(theContract), + contractCreate(theContract)) + .when(sourcing(() -> contractCall( + theContract, + "tokensAssociate", + asHeadlongAddress(civilianMirrorAddr.get()), + (new Address[] { + asHeadlongAddress(tokenMirrorAddr.get()), asHeadlongAddress(tokenMirrorAddr.get()) + })) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED) + .via(multiAssociate) + .payingWith(civilian) + .gas(4_000_000))) + .then( + childRecordsCheck( + multiAssociate, + CONTRACT_REVERT_EXECUTED, + recordWith().status(TOKEN_ID_REPEATED_IN_TOKEN_LIST)), + getAccountInfo(civilian).hasNoTokenRelationship(nfToken)); + } + + @Override + protected Logger getResultsLogger() { + return log; + } +} diff --git a/hedera-node/test-clients/src/main/resource/bootstrap.properties b/hedera-node/test-clients/src/main/resource/bootstrap.properties index e646f45c12af..7d6f12888897 100644 --- a/hedera-node/test-clients/src/main/resource/bootstrap.properties +++ b/hedera-node/test-clients/src/main/resource/bootstrap.properties @@ -63,7 +63,7 @@ balances.compressOnCreation=true cache.records.ttl=180 contracts.allowAutoAssociations=true contracts.allowSystemUseOfHapiSigs=TokenAssociateToAccount,TokenDissociateFromAccount,TokenFreezeAccount,TokenUnfreezeAccount,TokenGrantKycToAccount,TokenRevokeKycFromAccount,TokenAccountWipe,TokenBurn,TokenDelete,TokenMint,TokenUnpause,TokenPause,TokenCreate,TokenUpdate,ContractCall,CryptoTransfer -contracts.maxNumWithHapiSigsAccess=10_000_000 +contracts.maxNumWithHapiSigsAccess=0 contracts.withSpecialHapiSigsAccess= contracts.allowCreate2=true contracts.chainId=295 diff --git a/hedera-node/test-clients/src/test/java/EndToEndPackageRunner.java b/hedera-node/test-clients/src/test/java/EndToEndPackageRunner.java index b7d5f0c58c98..5886853f84f9 100644 --- a/hedera-node/test-clients/src/test/java/EndToEndPackageRunner.java +++ b/hedera-node/test-clients/src/test/java/EndToEndPackageRunner.java @@ -61,8 +61,6 @@ import com.hedera.services.bdd.suites.contract.precompile.CreatePrecompileSuite; import com.hedera.services.bdd.suites.contract.precompile.CryptoTransferHTSSuite; import com.hedera.services.bdd.suites.contract.precompile.DelegatePrecompileSuite; -import com.hedera.services.bdd.suites.contract.precompile.DissociatePrecompileSuite; -import com.hedera.services.bdd.suites.contract.precompile.MixedHTSPrecompileTestsSuite; import com.hedera.services.bdd.suites.contract.records.LogsSuite; import com.hedera.services.bdd.suites.contract.records.RecordsSuite; import com.hedera.services.bdd.suites.contract.traceability.TraceabilitySuite; @@ -272,10 +270,7 @@ Collection contractPrecompileEth() { @TestFactory Collection contractPrecompile2() { return List.of(new DynamicContainer[] { - extractSpecsFromSuite(CryptoTransferHTSSuite::new), - extractSpecsFromSuite(DelegatePrecompileSuite::new), - extractSpecsFromSuite(DissociatePrecompileSuite::new), - extractSpecsFromSuite(MixedHTSPrecompileTestsSuite::new) + extractSpecsFromSuite(CryptoTransferHTSSuite::new), extractSpecsFromSuite(DelegatePrecompileSuite::new), }); } @@ -285,7 +280,6 @@ Collection contractPrecompile2() { @TestFactory Collection contractPrecompile2Eth() { return List.of(new DynamicContainer[] { - extractSpecsFromSuiteForEth(DissociatePrecompileSuite::new), extractSpecsFromSuiteForEth(CryptoTransferHTSSuite::new), extractSpecsFromSuiteForEth(DelegatePrecompileSuite::new) });