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.
Remember from Part 1 that you can deploy smart contracts on Hedera in four steps:
Write the contract and compile to bytecode
Add the bytecode file to the Hedera network
Create the smart contract on Hedera
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.
HederaTokenService.sol
HederaResponseCodes.sol
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:
// 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:
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.
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.
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.
//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:
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(). 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.
// 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.
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.
// 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.
//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.
//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`);
async function bCheckerFcn(aId) {
let balanceCheckTx = await new AccountBalanceQuery().setAccountId(aId).execute(client);
return balanceCheckTx.tokens._map.get(tokenId.toString());
}
Console output:
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.