How to Send and Receive Hedera Tokens Using Smart Contracts - Part 2: Solidity
Screen Shot 2022 01 27 at 9 52 08 PM
Aug 16, 2022
by Francesco Coacci
Developer Evangelist

In this article, you will learn how to transfer a Hedera Token Service (HTS) token from a contract to another contract using solidity. You will then use the same contract to transfer a token to a Hedera account using an ERC Standard Call. You can check out other token transfer scenarios on GitHub.

Contract2 Contract
Contract2 Account

Try It Yourself

  • Get a Hedera testnet account
  • Use this Codesandbox to try the following example
    • Fork the sandbox
    • Remember to provide credentials in the .env file
    • Open a new terminal to execute index.js
  • Get the complete code of this example on GitHub

Workspace Structure

Your workspace should look like this:

  • index.js
  • utils.js
  • TokenSender.sol
  • TokenReceiver.sol
  • ExpiryHelper.sol
  • FeeHelper.sol
  • KeyHelper.sol
  • HederaResponseCodes.sol
  • HederaTokenService.sol
  • IHederaTokenService.sol
  • .env
  • /binaries
  • /oz-contracts

Transfer a Hedera Token using Precompiles

1. Create and Store your Contracts on the Hedera Network

Let’s start by creating two solidity contracts. One is called TokenReceiver.sol and the other TokenSender.sol. The TokenRecevier contract contains a function that associates tokens with the contract itself. On the other hand, the TokenSender contract creates and transfers a fungible token.

TokenReceiver.sol:

    Code Snippet Background
    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.6.0 <0.9.0;
     
    import "./HederaTokenService.sol";
    import "./HederaResponseCodes.sol";
     
    contract TokenReceiver is HederaTokenService {
     
       function tokenAssociate(address tokenId) external {
           int response = HederaTokenService.associateToken(address(this), tokenId);
     
           if (response != HederaResponseCodes.SUCCESS) {
               revert ("Associate Failed");
           }
       }
     
    }
    

    TokenSender.sol:

    Code Snippet Background
    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.6.0 <0.9.0;
     
    import './HederaResponseCodes.sol';
    import './IHederaTokenService.sol';
    import './HederaTokenService.sol';
    import './ExpiryHelper.sol';
     
    import './oz-contracts/contracts/token/ERC20/IERC20.sol';
    import './oz-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol';
     
    contract TokenSender is ExpiryHelper {
     
       // create a fungible Token with no custom fees,
       function createFungible(
           string memory name,
           string memory symbol,
           uint initialSupply,
           uint decimals,
           uint32 autoRenewPeriod
       ) external payable returns (address createdTokenAddress) {
     
           IHederaTokenService.HederaToken memory token;
           token.name = name;
           token.symbol = symbol;
           token.treasury = address(this);
     
           // create the expiry schedule for the token using ExpiryHelper
           token.expiry = createAutoRenewExpiry(address(this), autoRenewPeriod);
     
           // call HTS precompiled contract, passing initial supply and decimals
           (int responseCode, address tokenAddress) =
                       HederaTokenService.createFungibleToken(token, initialSupply, decimals);
     
           if (responseCode != HederaResponseCodes.SUCCESS) {
               revert ();
           }
     
           createdTokenAddress = tokenAddress;
       }
     
       function transferPrecompile(address tokenId, address receiver, int64 amount) external {
           int response = HederaTokenService.transferToken(tokenId, address(this), receiver, amount);
      
           if (response != HederaResponseCodes.SUCCESS) {
               revert ("Transfer Failed");
           }
       }
     
       // Transfer token from this contract to the recipient
       function transferERC(address token, address recipient, uint256 amount) public {
           IERC20(token).transfer(recipient, amount);
       }
     
       fallback () external{}
     
    }
    

    As you can see in the TokenSender contract, there are two transfer functions. For now, you can ignore the transferERC function.

    Now that you have two ready-to-go contracts, you can deploy them on the Hedera Network using the ContractCreateFlow() function. Since you must specify a bytecode, you need to compile both your contracts either using Remix IDE or solc. You can also find already compiled contracts on GitHub.

    Code Snippet Background
    // Create TokenSender contract
    const createSenderContract = new ContractCreateFlow()
        .setGas(100000)
        .setBytecode(bytecodeSender);
    const createSenderSubmit = await createSenderContract.execute(client);
    const createSenderRx = await createSenderSubmit.getReceipt(client);
    const contractIdSender = createSenderRx.contractId;
     
    console.log("The new TokenSender contract ID is " + contractIdSender);
     
    // Create TokenReceiver contract
    const createReceiverContract = new ContractCreateFlow()
        .setGas(100000)
        .setBytecode(bytecodeReceiver);
    const createReceiverSubmit = await createReceiverContract.execute(client);
    const createReceiverRx = await createReceiverSubmit.getReceipt(client);
    const contractIdReceiver = createReceiverRx.contractId;
     
    console.log("The new TokenReceiver contract ID is " + contractIdReceiver);
    

    Console Output:

    The new TokenSender contract ID is 0.0.47900500
    The new TokenReceiver contract ID is 0.0.47900502

    2. Create and Associate the Hedera Token

    Before transferring your token, you need to create it. So the first step is to use a ContractExecuteTransaction() to execute the createFungible function inside your TokenSender contract. You might have noticed that neither of your contracts has an admin key. That's because we want to deal with independent entities not managed by anyone. Not only is the TokenSender contract not updateable in any way, but it is also the treasury and auto-renew account of the fungible token you are about to create.

    Code Snippet Background
    // Create FT using TokenSender create function
    const createToken = new ContractExecuteTransaction()
        .setContractId(contractIdSender)
        .setGas(300000) // Increase if revert
        .setPayableAmount(20) // Increase if revert
        .setFunction("createFungible",
            new ContractFunctionParameters()
            .addString("USD Bar") // FT name
            .addString("USDB") // FT symbol
            .addUint256(10000) // FT initial supply
            .addUint256(2) // FT decimals
            .addUint32(7000000)); // auto renew period
     
    const createTokenTx = await createToken.execute(client);
    const createTokenRx = await createTokenTx.getRecord(client);
    const tokenIdSolidityAddr = createTokenRx.contractFunctionResult.getAddress(0);
    const tokenId = TokenId.fromSolidityAddress(tokenIdSolidityAddr);
     
    console.log(`Token created with ID: ${tokenId}`);
    

    Console Output:

    Token created with ID: 0.0.47900503

    And now, you must execute the tokenAssociate function inside your TokenReceiver contract.

    Code Snippet Background
    // Execute token associate function in TokenReceiver
    const tokenAssociate = new ContractExecuteTransaction()
        .setContractId(contractIdReceiver)
        .setGas(1500000)
        .setFunction("tokenAssociate",
            new ContractFunctionParameters()
            .addAddress(tokenId.toSolidityAddress())
        );
    const tokenAssociateTx = await tokenAssociate.execute(client);
    const tokenAssociateRx = await tokenAssociateTx.getReceipt(client);
    const tokenAssociateStatus = tokenAssociateRx.status;
     
    console.log("Token associate transaction status: " + tokenAssociateStatus.toString());
    
    

    Console Output:

    Token associate transaction status: SUCCESS

    3. Transfer Hedera Token to your Contract

    You can now transfer your token to your TokenReceiver contract. You need to specify both the token ID and your receiver ID in solidity format as the transferPrecompile function requires.

    Code Snippet Background
    // Execute token transfer (TokenSender -> TokenReceiver)
    const tokenTransfer = new ContractExecuteTransaction()
        .setContractId(contractIdSender)
        .setGas(1500000)
        .setFunction("transferPrecompile",
            new ContractFunctionParameters()
            .addAddress(tokenId.toSolidityAddress())
            .addAddress(contractIdReceiver.toSolidityAddress())
            .addInt64(1000)
        );
    const tokenTransferTx = await tokenTransfer.execute(client);
    const tokenTransferRx = await tokenTransferTx.getReceipt(client);
    const tokenTransferStatus = tokenTransferRx.status;
     
    console.log("Token transfer transaction status: " + tokenTransferStatus.toString());
    

    Console Output:

    Token transfer transaction status: SUCCESS

    Awesome! Your TokenReceiver contract now owns 10 USDB!

    Now let’s use a ContractInfoQuery() to verify the balances. If you want, you can also use HashScan.

    Code Snippet Background
    // Get TokenSender balance
    const getSender = new ContractInfoQuery()
        .setContractId(contractIdSender);
    
    const senderInfo = await getSender.execute(client);
    const senderBalance = senderInfo.tokenRelationships.get(tokenId).balance / 100;
     
    console.log("The sender contract balance for token " + tokenId + " is: " + senderBalance);
     
    // Get TokenReceiver balance
    const getReceiver = new ContractInfoQuery()
        .setContractId(contractIdReceiver);
     
    const receiverInfo = await getReceiver.execute(client);
    const receiverBalance = receiverInfo.tokenRelationships.get(tokenId).balance / 100;
     
    console.log("The receiver contract balance for token " + tokenId + " is: " + receiverBalance);
    

    Console Output:

    The sender contract balance for token 0.0.47900503 is: 90
    The receiver contract balance for token 0.0.47900503 is: 10

    Transfer a Hedera Token using ERC Standard Calls

    In this section, you’ll use your TokenSender contract again to transfer your previously created token to an account (Alice). Remember the transferERC function? Well, now you will use that ERC standard call to interact with your HTS token as if it were an ERC 20. Inside the /oz-contracts directory, there are contracts imported from OpenZeppelin necessary for the transferERC function to work.

    1. Set Up a Helper Function

    To simplify your main script, you need to create a helper function to create accounts in a file called utils.js. You must specify a private key, an initial balance, and your client. You will also specify an automatic token association number to avoid manually associating your token with Alice’s account.

    Code Snippet Background
    // Creates a new account
    async function accountCreator(privateKey, initialBalance, client) {
       const response = await new AccountCreateTransaction()
           .setInitialBalance(new Hbar(initialBalance))
           .setMaxAutomaticTokenAssociations(10)
           .setKey(privateKey.publicKey)
           .execute(client);
       const receipt = await response.getReceipt(client);
       return receipt.accountId;
    }
    

    2. Define a Hedera Account

    So, let’s use the helper function you just created by specifying a newly generated private key and an initial balance of 20 HBAR provided by the operator.

    Code Snippet Background
    // Generating token receiver account (Alice)
    const aliceKey = PrivateKey.generateED25519();
    const aliceId = await accountCreator(aliceKey, 20, client);
    

    3. Transfer Hedera Token to your Account

    Now let’s use the ERC standard call to transfer the token from your TokenSender contract to Alice.

    Code Snippet Background
    // Execute token transfer from TokenSender to Alice
    const tokenTransferERC = new ContractExecuteTransaction()
        .setContractId(contractIdSender) // Contract ID
        .setGas(4000000) // Increase if reverts
        .setFunction("transferERC",
            new ContractFunctionParameters()
            .addAddress(tokenIdSolidityAddr) // Token ID
            .addAddress(aliceId.toSolidityAddress()) // Token receiver ID
            .addUint256(1000) // Token amount
        );
      
    const tokenTransferERCTx = await tokenTransferERC.execute(client);
    const tokenTransferERCRx = await tokenTransferERCTx.getReceipt(client);
    const tokenTransferERCStatus = tokenTransferERCRx.status;
     
    console.log("Token transfer transaction status: " + tokenTransferERCStatus.toString());
    
    

    Console Output:

    Token transfer transaction status: SUCCESS

    You can recheck your TokenSender contract’s balance using a ContractInfoQuery() and Alice’s balance using an AccountInfoQuery().

    Code Snippet Background
    // Get TokenSender balance
    const getSender2 = new ContractInfoQuery()
        .setContractId(contractIdSender);
     
    const senderInfo2 = await getSender2.execute(client);
    const senderBalance2 = senderInfo2.tokenRelationships.get(tokenId).balance / 100;
     
    console.log("The sender contract balance for token " + tokenId + " is: " + senderBalance2);
     
    // Check Alice's balance
    const query = new AccountInfoQuery()
        .setAccountId(aliceId) // Token receiver ID
    
    const info = await query.execute(client);
    // Get balance and divide by 100 because of token decimals
    const balance = info.tokenRelationships.get(tokenId).balance / 100;
     
    console.log("The account balance for token " + tokenId + " is: " + balance);
    

    Console Output:

    The sender contract balance for token 0.0.47900503 is: 80
    The account balance for token 0.0.47900503 is: 10

    If you reached this point, then you are a Hedera Master! Now you know how to transfer a token using SDKs, Precompile, and ERC standard calls.

    Contact us with any questions about this article using the Hedera Discord Server.

    Check out the code

    Check out this code example on GitHub.

    Continue Learning

    Hedera Token Transfer (Hedera documentation)

    Supported ERC Token Standards (Hedera documentation)