In this article, you will learn how to transfer a Hedera Token Service (HTS) token using solidity from one contract to another. 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.
Your workspace should look like this:
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:
// 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:
// 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.
// 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.47900500The new TokenReceiver contract ID is 0.0.47900502
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.
// 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}`);
Token created with ID: 0.0.47900503
And now, you must execute the tokenAssociate function inside your TokenReceiver contract.
// 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());
Token associate transaction status: SUCCESS
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.
// 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());
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.
// 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);
The sender contract balance for token 0.0.47900503 is: 90The receiver contract balance for token 0.0.47900503 is: 10
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.
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.
// 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; }
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.
// Generating token receiver account (Alice) const aliceKey = PrivateKey.generateED25519(); const aliceId = await accountCreator(aliceKey, 20, client);
Now let’s use the ERC standard call to transfer the token from your TokenSender contract to Alice.
// 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());
You can recheck your TokenSender contract’s balance using a ContractInfoQuery() and Alice’s balance using an AccountInfoQuery().
// 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);
The sender contract balance for token 0.0.47900503 is: 80The 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 this code example on GitHub.
Hedera Token Transfer (Hedera documentation)
Supported ERC Token Standards (Hedera documentation)