Deploy a Contract Using the Hedera Token Service

Summary

In this example, you will learn how to create a Solidity contract that interacts with the Hedera Token Service (HTS). The initial release of this feature supports token mint, burn, associate, dissociate, and transfer transactions.

The example does not cover the environment setup or creating certain variables that may be seen in the code blocks. The full coding example can be found at the end of the page.

Smart contract entity auto renewal and expiry will be enabled in a future release. Please check out HIP-16 for more information.


Prerequisites

We recommend you complete the following introduction to get a basic understanding of Hedera transactions. This example does not build upon the previous examples.


1. Create Your "HTS" Smart Contract

In this example, you will associate a token to an account and transfer tokens to the associated account by interacting with the HTS contract deployed to Hedera. The HTS contract has three functions that allow you to associate, transfer, and dissociate tokens from a Hedera account.

  • tokenAssociate

  • tokenTransfer

  • tokenDissociate

The HTS.sol will serve as a reference to the contract that was compiled. The HTS.json file contains the data.bytecode.object field that will be used to store the contract bytecode in a file on the Hedera network.

To write a contract using HTS, you will need to add the HTS Solidity support libraries to your project and import them into your contract. Please see the HTS.sol example for reference. The IHederaTokenService.sol will need to be in the same directory as the other two files. An explanation of the functions can be found here.

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.6.12;

import "./HederaTokenService.sol";
import "./HederaResponseCodes.sol";


contract HTS is HederaTokenService {

    function tokenAssociate(address sender, address tokenAddress) external {
        int response = HederaTokenService.associateToken(sender, tokenAddress);

        if (response != HederaResponseCodes.SUCCESS) {
            revert ("Associate Failed");
        }
    }

    function tokenTransfer(address tokenId, address fromAccountId , address toAccountId , int64 tokenAmount) external {
        int response = HederaTokenService.transferToken(tokenId, fromAccountId, toAccountId, tokenAmount);

        if (response != HederaResponseCodes.SUCCESS) {
            revert ("Transfer Failed");
        }
    }

    function tokenDissociate(address sender, address tokenAddress) external {
        int response = HederaTokenService.dissociateToken(sender, tokenAddress);

        if (response != HederaResponseCodes.SUCCESS) {
            revert ("Dissociate Failed");
        }
    }
}

2. Store the Smart Contract Bytecode on Hedera

Create a file using the FileCreateTransaction() API to store the hex-encoded byte code of the "HTS" contract. Once the file is created, you can obtain the file ID from the receipt of the transaction.

Note: The bytecode is required to be hex-encoded. It should not be the actual data the hex represents.

//Import the HTS.json file from the resources folder
ClassLoader cl = HTS.class.getClassLoader();

Gson gson = new Gson();
JsonObject jsonObject;

//Get the json file
InputStream jsonStream = cl.getResourceAsStream("HTS.json");
jsonObject = gson.fromJson(new InputStreamReader(jsonStream, StandardCharsets.UTF_8), JsonObject.class);


//Store the "object" field from the HTS.json file as hex-encoded bytecode
String object = jsonObject.getAsJsonObject("data").getAsJsonObject("bytecode").get("object").getAsString();
byte[] bytecode = object.getBytes(StandardCharsets.UTF_8);

//Create a file on Hedera and store the hex-encoded bytecode
FileCreateTransaction fileCreateTx = new FileCreateTransaction()
        .setKeys(privateKeyTest)
        .setContents(bytecode);

//Submit the file to the Hedera test network
TransactionResponse submitTx = fileCreateTx.execute(client);

//Get the receipt of the file create transaction
TransactionReceipt fileReceipt = submitTx.getReceipt(client);

//Get the file ID
FileId newFileId = fileReceipt.fileId;

//Log the file ID
System.out.println("The smart contract byte code file ID is " + newFileId);

//v2.6.0 Hedera Java SDK

3. Deploy a Hedera Smart Contract

Create the contract and set the file ID to the file that contains the hex-encoded bytecode from the previous step. You will need to set the gas high enough to deploy the contract. The gas should be estimated to be within 25% of the actual gas cost to avoid paying extra gas. You can read more about gas and fees here.

Note: You will need to set the gas value high enough to deploy the contract. If you don't have enough gas, you will receive an INSUFFICIENT_GAS response. If you set the value too high you will be refunded a maximum of 20% of the amount that was set for the transaction.

//Deploy the contract
ContractCreateTransaction contractTx = new ContractCreateTransaction()
        //The contract bytecode file
        .setBytecodeFileId(newFileId)
        //The max gas to reserve for this transaction
        .setGas(2_000_000);

//Submit the transaction to the Hedera test network
TransactionResponse contractResponse = contractTx.execute(client);

//Get the receipt of the file create transaction
TransactionReceipt contractReceipt = contractResponse.getReceipt(client);

//Get the smart contract ID
ContractId newContractId = contractReceipt.contractId;

//Log the smart contract ID
System.out.println("The smart contract ID is " + newContractId);

//v2.6.0 Hedera Java SDK

4. Call the tokenAssociate Contract Function

The tokenAssociate function in the contract was previously used to associate tokens created with the Hedera Token Service (HTS). However, due to a change in the security model, it is no longer possible to associate HTS tokens using this function. Instead, you should use the Hedera SDK to perform token associations. You will pass the token ID and account ID to the function. The parameters must be provided in the order expected by the function to execute successfully.

//Associate the token to an account using the HTS contract
TokenAssociateTransaction transaction = new TokenAssociateTransaction()
        .setAccountId(accountIdTest)
        .setTokenId(Collections.singletonList(tokenId))
        .freezeWith(client);

//Sign with the account key to associate and submit to the Hedera network
TransactionResponse associateTokenResponse = transaction.sign(privateKeyTest).execute(client);

System.out.println("The transaction status: " +associateTokenResponse.getReceipt(client).status);

5. Call the approveTokenAllowance Function

Using the approveTokenAllowance function is a crucial step before initiating a transfer with a smart contract on Hedera. This function grants the necessary permissions from the token owner to authorize the transfer. It serves as a stringent access control measure, ensuring that only approved contracts or accounts can spend the designated tokens. You will pass the owner which is the account that owns the fungible tokens and grants the allowance to the spender, the spender who is the account authorized by the owner to spend fungible tokens from the owner's account. The spender covers the transaction fees for token transfers. And the amount which is the number of tokens the spender is authorized to spend from the owner's account.

// Convert the contract ID to an account ID
AccountId contractIdAsAccountId = AccountId.fromString(newContractId.toString());

//Approve the token allowance
AccountAllowanceApproveTransaction transaction = new AccountAllowanceApproveTransaction()
    .approveHbarAllowance(treasuryAccountId, newContractId, Hbar.from(5));

//Sign the transaction with the owner account key and the transaction fee payer key (client)  
TransactionResponse txResponse = transaction.freezeWith(client).sign(treasuryKey).execute(client);

//Request the receipt of the transaction
TransactionReceipt receipt = txResponse.getReceipt(client);

//Get the transaction consensus status
Status transactionStatus = receipt.status;

System.out.println("The transaction consensus status for the allowance function is " +transactionStatus);

6. Call the tokenTransfer Contract Function

Transfer 100 units of the token to the account that was associated with the token. You will use the ContractExecuteTransaction() API and set the contract function to tokenTransfer. The contract function parameters must be provided in the order of the function expects to receive them.

The transaction must be signed by the account that is sending the tokens. In this case, it is the treasury account.

You can verify the transfer was successful by checking the account token balance!

//Transfer the new token to the account
//Contract function params need to be in the order of the paramters provided in the tokenTransfer contract function
ContractExecuteTransaction tokenTransfer = new ContractExecuteTransaction()
     .setContractId(newContractId)
     .setGas(2_000_000)
     .setFunction("tokenTransfer", new ContractFunctionParameters()
          //The ID of the token
          .addAddress(tokenId.toSolidityAddress())
          //The account to transfer the tokens from
          .addAddress(treasuryAccountId.toSolidityAddress())
          //The account to transfer the tokens to
          .addAddress(accountIdTest.toSolidityAddress())
          //The number of tokens to transfer
         .addInt64(5));

//Sign the token transfer transaction with the treasury account to authorize the transfer and submit
ContractExecuteTransaction signTokenTransfer = tokenTransfer.freezeWith(client).sign(treasuryKey);

//Submit transfer transaction
TransactionResponse submitTransfer = signTokenTransfer.execute(client);

//Get transaction status
Status txStatus = submitTransfer.getReceipt(client).status;

//Verify your account received the 5 tokens
 AccountBalance newAccountBalance = new AccountBalanceQuery()
      .setAccountId(accountIdTest)
      .execute(client);

System.out.println("My new account balance is " +newAccountBalance.tokens);

Note: Check out our smart contract mirror node rest APIs that return information about a contract like contract results and logs!

Congratulations πŸŽ‰! You have learned how to deploy a contract using the Hedera Token Service and completed the following:

  • Associated an HTS token by using the SDK

  • Approved the token allowance so that the contract can transfer tokens

  • Transferred tokens using the deployed contract


Code Check βœ…

Java
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.hedera.hashgraph.sdk.*;

import io.github.cdimascio.dotenv.Dotenv;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;


public class HTS {

    public static void main(String[] args) throws TimeoutException, PrecheckStatusException, ReceiptStatusException, InterruptedException, IOException {

        AccountId accountIdTest = AccountId.fromString(Dotenv.load().get("MY_ACCOUNT_ID"));
        PrivateKey privateKeyTest = PrivateKey.fromString(Dotenv.load().get(MY_PRIVATE_KEY"));

        Client client = Client.forTestnet();
        client.setOperator(accountIdTest, privateKeyTest);

        //Import the HTS.json file from the resources folder
        ClassLoader cl = HTS.class.getClassLoader();

        Gson gson = new Gson();
        JsonObject jsonObject;

        //Get the json file
        InputStream jsonStream = cl.getResourceAsStream("HTS.json");
        jsonObject = gson.fromJson(new InputStreamReader(jsonStream, StandardCharsets.UTF_8), JsonObject.class);


        //Store the "object" field from the HTS.json file as hex-encoded bytecode
        String object = jsonObject.getAsJsonObject("data").getAsJsonObject("bytecode").get("object").getAsString();
        byte[] bytecode = object.getBytes(StandardCharsets.UTF_8);

        //Create a treasury Key
        PrivateKey treasuryKey = PrivateKey.generateED25519();

        //Create a treasury account
        AccountCreateTransaction treasuryAccount = new AccountCreateTransaction()