How to Deploy Smart Contracts on Hedera – Part 2: A Contract with Hedera Token Service Integration
Headshot
Jan 26, 2022
by Ed Marquez
Developer Relations Engineer

In Part 1, you learned how to write a getter-setter smart contract in Solidity and deploy it on the Hedera network. Now let’s write and deploy a Solidity smart contract that integrates with the Hedera Token Service (HTS). Here's the coding tutorial on YouTube:

Recent updates to the Hedera Smart Contract Service include Ethereum virtual machine (EVM) upgrades, database architecture modifications, and support for Hedera Token Service. Whether you’re new to smart contract development or migrating from another smart contract platform, use Hedera to develop and deploy fast, low-cost, and carbon-negative smart contracts.

What You Will Need

Tools

  • Hedera testnet account
  • Development environment
    • This Codesandbox is already setup for you to try this example
      • Remember to provide your testnet account credentials in the .env file
      • And open a new terminal to execute index.js
    • Or if you prefer a local development environment, you’ll need:
  • Documentation and examples

      How to Deploy a Smart Contract

      Remember from Part 1 that you can deploy smart contracts on Hedera in four steps:

      1. Write the contract and compile to bytecode
      2. Add the bytecode file to the Hedera network
      3. Create the smart contract on Hedera
      4. Execute the smart contract

      Since the smart contract will interact with an HTS token, you will create a fungible one in step 2 – don’t worry, we’ll show you how.

      1. Write the Contract and Compile It to Get the Bytecode

      Let’s write a Solidity smart contract that can mint, associate, and transfer an HTS token. For the contract to be able to perform token operations, it’s necessary to have the files listed below in your directory. You can grab these files from this GitHub repository and going under the folders contracts => hts-precompiles.

      1. HederaTokenService.sol
      2. HederaResponseCodes.sol
      3. IHederaTokenService.sol

      Once you have the files in your working directory, import the first two into your contract using import. The third file is a supporting library, so you don’t have to import it but make sure it’s present in the directory.

      The contract MintAssoTransHTS has a state variable, tokenAddress, which is an address passed to the constructor function when the contract is first deployed. In addition, the contract has the functions mintFungibleToken, tokenAssociate, and tokenTransfer. These functions rely on the pre-compiled contract HederaTokenService.sol to perform the respective operations. If the response returned by each operation is not successful, then a failure message is provided.

      Solidity contract:

      Code Snippet Background
      // SPDX-License-Identifier: Apache-2.0
      pragma solidity ^0.8.11;
      pragma experimental ABIEncoderV2;
      
      import "./hip-206/HederaTokenService.sol";
      import "./hip-206/HederaResponseCodes.sol";
      
      
      contract MintAssoTransHTS is HederaTokenService {
      
          address tokenAddress;
      
          constructor(address _tokenAddress) public {
              tokenAddress = _tokenAddress;
           }
      
          function mintFungibleToken(uint64 _amount) external {
              (int response, uint64 newTotalSupply, int64[] memory serialNumbers) = HederaTokenService.mintToken(tokenAddress, _amount, new bytes[](0));
                 
              if (response != HederaResponseCodes.SUCCESS) {
                  revert ("Mint Failed");
              }
          }
      
          function tokenAssociate(address _account) external {
              int response = HederaTokenService.associateToken(_account, tokenAddress);
      
              if (response != HederaResponseCodes.SUCCESS) {
                  revert ("Associate Failed");
              }
          }
      
          function tokenTransfer(address _sender, address _receiver, int64 _amount) external {        
          int response = HederaTokenService.transferToken(tokenAddress, _sender, _receiver, _amount);
          
              if (response != HederaResponseCodes.SUCCESS) {
                  revert ("Transfer Failed");
              }
          }
      }
      

      As you know by now, Solidity code cannot be executed by the EVM. Instead, you need to compile this code to low-level machine code that the EVM can understand. Once that compilation is done, you end up with the bytecode for the smart contract.

      Once you’ve installed the solc package (npm install -g solc), type the following command in the terminal to start the compilation:

      Code Snippet Background
      solcjs --bin MintAssociateTransferHTS.sol
      

      You may receive a few warnings, but that’s ok. If the compilation is successful, you should now have a binary file in your directory called MintAssociateTransferHTS_sol_MintAssoTransHTS.bin, which contains the compiled bytecode.

      Code Snippet Background
      608060405234801561001057600080fd5b50604051610f14380380610f14833981810160405281019061003291906100db565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050610108565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100a88261007d565b9050919050565b6100b88161009d565b81146100c357600080fd5b50565b6000815190506100d5816100af565b92915050565b6000602082840312156100f1576100f0610078565b5b60006100ff848285016100c6565b91505092915050565b610dfd806101176000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80631a2a5e1514610046578063955c12bb14610062578063ef7c7fb01461007e575b600080fd5b610060600480360381019061005b919061067e565b61009a565b005b61007c600480360381019061007791906106e4565b610112565b005b61009860048036038101906100939190610777565b61018e565b005b60006100c68260008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1661025e565b9050601660030b811461010e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161010590610801565b60405180910390fd5b5050565b600061014060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff16858585610376565b9050601660030b8114610188576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161017f9061086d565b60405180910390fd5b50505050565b600080600061020c60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1685600067ffffffffffffffff8111156101d3576101d261088d565b5b60405190808252806020026020018201604052801561020657816020015b60608152602001906001900390816101f15790505b50610494565b925092509250601660030b8314610258576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161024f90610908565b60405180910390fd5b50505050565b600080600061016773ffffffffffffffffffffffffffffffffffffffff166349146bde60e01b8686604051602401610297929190610937565b604051602081830303815290604052907bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161030191906109da565b6000604051808303816000865af19150503d806000811461033e576040519150601f19603f3d011682016040523d82523d6000602084013e610343565b606091505b509150915081610354576015610369565b808060200190518101906103689190610a2a565b5b60030b9250505092915050565b600080600061016773ffffffffffffffffffffffffffffffffffffffff1663eca3691760e01b888888886040516024016103b39493929190610a66565b604051602081830303815290604052907bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161041d91906109da565b6000604051808303816000865af19150503d806000811461045a576040519150601f19603f3d011682016040523d82523d6000602084013e61045f565b606091505b509150915081610470576015610485565b808060200190518101906104849190610a2a565b5b60030b92505050949350505050565b600080606060008061016773ffffffffffffffffffffffffffffffffffffffff1663278e0b8860e01b8989896040516024016104d293929190610bd7565b604051602081830303815290604052907bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161053c91906109da565b6000604051808303816000865af19150503d8060008114610579576040519150601f19603f3d011682016040523d82523d6000602084013e61057e565b606091505b5091509150816105da57601560008067ffffffffffffffff8111156105a6576105a561088d565b5b6040519080825280602002602001820160405280156105d45781602001602082028036833780820191505090505b506105ef565b808060200190518101906105ee9190610d58565b5b8260030b9250809550819650829750505050505093509350939050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061064b82610620565b9050919050565b61065b81610640565b811461066657600080fd5b50565b60008135905061067881610652565b92915050565b60006020828403121561069457610693610616565b5b60006106a284828501610669565b91505092915050565b60008160070b9050919050565b6106c1816106ab565b81146106cc57600080fd5b50565b6000813590506106de816106b8565b92915050565b6000806000606084860312156106fd576106fc610616565b5b600061070b86828701610669565b935050602061071c86828701610669565b925050604061072d868287016106cf565b9150509250925092565b600067ffffffffffffffff82169050919050565b61075481610737565b811461075f57600080fd5b50565b6000813590506107718161074b565b92915050565b60006020828403121561078d5761078c610616565b5b600061079b84828501610762565b91505092915050565b600082825260208201905092915050565b7f4173736f6369617465204661696c656400000000000000000000000000000000600082015250565b60006107eb6010836107a4565b91506107f6826107b5565b602082019050919050565b6000602082019050818103600083015261081a816107de565b9050919050565b7f5472616e73666572204661696c65640000000000000000000000000000000000600082015250565b6000610857600f836107a4565b915061086282610821565b602082019050919050565b600060208201905081810360008301526108868161084a565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4d696e74204661696c6564000000000000000000000000000000000000000000600082015250565b60006108f2600b836107a4565b91506108fd826108bc565b602082019050919050565b60006020820190508181036000830152610921816108e5565b9050919050565b61093181610640565b82525050565b600060408201905061094c6000830185610928565b6109596020830184610928565b9392505050565b600081519050919050565b600081905092915050565b60005b83811015610994578082015181840152602081019050610979565b838111156109a3576000848401525b50505050565b60006109b482610960565b6109be818561096b565b93506109ce818560208601610976565b80840191505092915050565b60006109e682846109a9565b915081905092915050565b60008160030b9050919050565b610a07816109f1565b8114610a1257600080fd5b50565b600081519050610a24816109fe565b92915050565b600060208284031215610a4057610a3f610616565b5b6000610a4e84828501610a15565b91505092915050565b610a60816106ab565b82525050565b6000608082019050610a7b6000830187610928565b610a886020830186610928565b610a956040830185610928565b610aa26060830184610a57565b95945050505050565b610ab481610737565b82525050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b600082825260208201905092915050565b6000601f19601f8301169050919050565b6000610b1382610960565b610b1d8185610ae6565b9350610b2d818560208601610976565b610b3681610af7565b840191505092915050565b6000610b4d8383610b08565b905092915050565b6000602082019050919050565b6000610b6d82610aba565b610b778185610ac5565b935083602082028501610b8985610ad6565b8060005b85811015610bc55784840389528151610ba68582610b41565b9450610bb183610b55565b925060208a01995050600181019050610b8d565b50829750879550505050505092915050565b6000606082019050610bec6000830186610928565b610bf96020830185610aab565b8181036040830152610c0b8184610b62565b9050949350505050565b600081519050610c248161074b565b92915050565b600080fd5b610c3882610af7565b810181811067ffffffffffffffff82111715610c5757610c5661088d565b5b80604052505050565b6000610c6a61060c565b9050610c768282610c2f565b919050565b600067ffffffffffffffff821115610c9657610c9561088d565b5b602082029050602081019050919050565b600080fd5b600081519050610cbb816106b8565b92915050565b6000610cd4610ccf84610c7b565b610c60565b90508083825260208201905060208402830185811115610cf757610cf6610ca7565b5b835b81811015610d205780610d0c8882610cac565b845260208401935050602081019050610cf9565b5050509392505050565b600082601f830112610d3f57610d3e610c2a565b5b8151610d4f848260208601610cc1565b91505092915050565b600080600060608486031215610d7157610d70610616565b5b6000610d7f86828701610a15565b9350506020610d9086828701610c15565b925050604084015167ffffffffffffffff811115610db157610db061061b565b5b610dbd86828701610d2a565b915050925092509256fea2646970667358221220f311f5d6ed573ca99fd8600c44a27f62ed4c696f2eaabd6f4384f2e42b6799de64736f6c634300080b0033
      

      Now in index.js, import and use the file system module (fs) to read the contents of the bytecode file into the variable bytecode.

      Code Snippet Background
      // STEP 1 ===================================
      console.log(`STEP 1 ===================================`);
      const bytecode = fs.readFileSync("./MintAssociateTransferHTS_sol_MintAssoTransHTS.bin");
      console.log(`- Done \n`);
      

      Console output:

      2022 Smart Contracts Part 2 Image 1

      2. Add the Bytecode File to Hedera (and Create a Token!)

      For step 2, create the fungible token using TokenCreateTransaction(), specify some token properties, sign the transaction with the required keys, then execute the transaction and get a receipt with the Hedera client. From the transaction receipt, get the ID of the new token and convert that token ID to Solidity format. You can also do a query to check token attributes, like the initial supply.

      Code Snippet Background
      // STEP 2 ===================================
      console.log(`STEP 2 ===================================`);
      //Create a fungible token
      const tokenCreateTx = await new TokenCreateTransaction()
      	.setTokenName("hbarRocks")
      	.setTokenSymbol("HROK")
      	.setDecimals(0)
      	.setInitialSupply(100)
      	.setTreasuryAccountId(treasuryId)
      	.setAdminKey(treasuryKey)
      	.setSupplyKey(treasuryKey)
      	.freezeWith(client)
      	.sign(treasuryKey);
      const tokenCreateSubmit = await tokenCreateTx.execute(client);
      const tokenCreateRx = await tokenCreateSubmit.getReceipt(client);
      const tokenId = tokenCreateRx.tokenId;
      const tokenAddressSol = tokenId.toSolidityAddress();
      console.log(`- Token ID: ${tokenId}`);
      console.log(`- Token ID in Solidity format: ${tokenAddressSol}`);
      
      // Token query 1
      const tokenInfo1 = await tQueryFcn(tokenId);
      console.log(`- Initial token supply: ${tokenInfo1.totalSupply.low} \n`);
      
      Code Snippet Background
      async function tQueryFcn(tId) {
      	let info = await new TokenInfoQuery().setTokenId(tId).execute(client);
      	return info;
      }
      

      Next, create a file on Hedera to store the smart contract bytecode. The file is created using FileCreateTransaction(). The total size for a given transaction on Hedera is limited to 6 KB. Since the bytecode file is larger than that, just create an empty file and then use FileAppendTransaction() with .setMaxChunks(10) to chunk up and add the contents.

      After that, output the bytecode file ID and a status confirmation to the console.

      Code Snippet Background
      //Create a file on Hedera and store the hex-encoded bytecode
      const fileCreateTx = new FileCreateTransaction().setKeys([operatorKey]);
      const fileSubmit = await fileCreateTx.execute(client);
      const fileCreateRx = await fileSubmit.getReceipt(client);
      const bytecodeFileId = fileCreateRx.fileId;
      console.log(`- The smart contract bytecode file ID is: ${bytecodeFileId}`);
      
      // Append contents to the file
      const fileAppendTx = new FileAppendTransaction()
      	.setFileId(bytecodeFileId)
      	.setContents(bytecode)
      	.setMaxChunks(10)
      	.setMaxTransactionFee(new Hbar(2));
      const fileAppendSubmit = await fileAppendTx.execute(client);
      const fileAppendRx = await fileAppendSubmit.getReceipt(client);
      console.log(`- Content added: ${fileAppendRx.status} \n`);
      

      Console output:

      2022 Smart Contracts Part 2 Image 2

      3. Create the Smart Contract on Hedera

      Now that the bytecode file is on the Hedera network, instantiate the contract using ContractCreateTransaction(). Specify the bytecode file ID based on the previous step, set the maximum amount of gas you’re willing to pay, and pass the token address in Solidity format to the constructor function using .contractFunctionParameters()

      As a convenient alternative to ContractCreateTransaction, you can use ContractCreateFlow() to create the file storing the bytecode and contract in a single step. This single call handles for you the FileCreateTransaction(), FileAppendTransaction(), and ContractCreateTransaction() operations.

      Next, execute the transaction, get a receipt, and obtain the ID of the deployed smart contract from that receipt. You can optionally convert the contract ID to solidity format and output that information to the console.

      Code Snippet Background
      // STEP 3 ===================================
      console.log(`STEP 3 ===================================`);
      // Create the smart contract
      const contractInstantiateTx = new ContractCreateTransaction()
      	.setBytecodeFileId(bytecodeFileId)
      	.setGas(3000000)
      	.setConstructorParameters(new ContractFunctionParameters().addAddress(tokenAddressSol));
      const contractInstantiateSubmit = await contractInstantiateTx.execute(client);
      const contractInstantiateRx = await contractInstantiateSubmit.getReceipt(client);
      const contractId = contractInstantiateRx.contractId;
      const contractAddress = contractId.toSolidityAddress();
      console.log(`- The smart contract ID is: ${contractId}`);
      console.log(`- The smart contract ID in Solidity format is: ${contractAddress} \n`);
      

      After that, use TokenUpdateTransaction() to specify the newly deployed contract as the supply key for the token. This means that the contract manages the token supply (minting and burning operations). Query the token before and after making this update to check the supply key at both points in the process.

      Code Snippet Background
      // Token query 2.1
      const tokenInfo2p1 = await tQueryFcn(tokenId);
      console.log(`- Token supply key: ${tokenInfo2p1.supplyKey.toString()}`);
      
      // Update the fungible token so the smart contract manages the supply
      const tokenUpdateTx = await new TokenUpdateTransaction()
      	.setTokenId(tokenId)
      	.setSupplyKey(contractId)
      	.freezeWith(client)
      	.sign(treasuryKey);
      const tokenUpdateSubmit = await tokenUpdateTx.execute(client);
      const tokenUpdateRx = await tokenUpdateSubmit.getReceipt(client);
      console.log(`- Token update status: ${tokenUpdateRx.status}`);
      
      // Token query 2.2
      const tokenInfo2p2 = await tQueryFcn(tokenId);
      console.log(`- New token supply key: ${tokenInfo2p2.supplyKey.toString()} \n`);
      

      Console output:

      2022 Smart Contracts Part 2 Image 3

      4. Execute the Smart Contract

      Finally, the last step is calling each one of the functions in the contract and querying the token and account balances to confirm the operations occur as expected.

      The mintFungibleToken function in the contract takes as input a uint64 value, which is the number of new tokens being added to the supply. A status confirmation and token query can help confirm that the mint operation was successful.

      Code Snippet Background
      // STEP 4 ===================================
      console.log(`STEP 4 ===================================`);
      //Execute a contract function (mint)
      const contractExecTx = await new ContractExecuteTransaction()
      	.setContractId(contractId)
      	.setGas(3000000)
      	.setFunction("mintFungibleToken", new ContractFunctionParameters().addUint64(150))
      	.setMaxTransactionFee(new Hbar(2));
      const contractExecSubmit = await contractExecTx.execute(client);
      const contractExecRx = await contractExecSubmit.getReceipt(client);
      console.log(`- New tokens minted: ${contractExecRx.status.toString()}`);
      
      // Token query 3
      const tokenInfo3 = await tQueryFcn(tokenId);
      console.log(`- New token supply: ${tokenInfo3.totalSupply.low} \n`);
      

      Next, call the tokenAssociate function of the contract and provide Alice’s account ID in Solidity format as the function parameter, and log a status confirmation to the console.

      Code Snippet Background
      //Execute a contract function (associate)
      const contractExecTx1 = await new ContractExecuteTransaction()
      	.setContractId(contractId)
      	.setGas(3000000)
      	.setFunction("tokenAssociate", new ContractFunctionParameters().addAddress(aliceId.toSolidityAddress()))
      	.setMaxTransactionFee(new Hbar(2))
      	.freezeWith(client);
      const contractExecSign1 = await contractExecTx1.sign(aliceyKey);
      const contractExecSubmit1 = await contractExecSign1.execute(client);
      const contractExecRx1 = await contractExecSubmit1.getReceipt(client);
      console.log(`- Token association with Alice's account: ${contractExecRx1.status.toString()} \n`);
      

      Lastly, execute the tokenTransfer function providing the sender address, receiver address, and amount as the function parameters; sign the transaction with the treasury key, execute, and get a receipt with the client; and then do account balance queries for the treasury and Alice.

      Code Snippet Background
      //Execute a contract function (transfer)
      const contractExecTx2 = await new ContractExecuteTransaction()
      	.setContractId(contractId)
      	.setGas(3000000)
      	.setFunction(
      		"tokenTransfer",
      		new ContractFunctionParameters()
      			.addAddress(treasuryId.toSolidityAddress())
      			.addAddress(aliceId.toSolidityAddress())
      			.addInt64(50)
      	)
      	.setMaxTransactionFee(new Hbar(2))
      	.freezeWith(client);
      const contractExecSign2 = await contractExecTx2.sign(treasuryKey);
      const contractExecSubmit2 = await contractExecSign2.execute(client);
      const contractExecRx2 = await contractExecSubmit2.getReceipt(client);
      
      console.log(`- Token transfer from Treasury to Alice: ${contractExecRx2.status.toString()}`);
      
      tB = await bCheckerFcn(treasuryId);
      aB = await bCheckerFcn(aliceId);
      console.log(`- Treasury balance: ${tB} units of token: ${tokenId}`);
      console.log(`- Alice balance: ${aB} units of token: ${tokenId} \n`);
      
      Code Snippet Background
      async function bCheckerFcn(aId) {
      	let balanceCheckTx = await new AccountBalanceQuery().setAccountId(aId).execute(client);
      	return balanceCheckTx.tokens._map.get(tokenId.toString());
      }
      

      Console output:

      2022 Smart Contracts Part 2 Image 4

      These steps and confirmations show that your smart contract successfully added 150 units to the token supply, associated Alice’s account with the token, and transferred 50 tokens from the treasury to Alice.

      Now you know how to deploy smart contracts that interact with the Hedera Token Service!

      For feedback on this article or future articles you would like to see, let us know via the Hedera Discord server.

      Check Out the Code and Try It

      Check out the code in GitHub

      Try the code in Codesandbox (remember to provide your account credentials in the .env file)

      Continue Learning about Smart Contracts

      What Are Smart Contracts? (Hedera Learning Center)