Hedera is a relatively new chain that exists since a few years, but recently added token service and smart contract capabilities. You can now write and deploy Solidity contracts to it, but it works a little differently than what you might be used to.

Let's take a look!

Hedera Meme

What is the Hedera Network?

Hedera is a 3rd generation blockchain operating on a Proof-of-Stake consensus mechanism called hashgraph, explained below. It results in the highest grade of security possible (ABFT) which allows for honest nodes of a network to guarantee to agree on the timing and order of a set of transactions fairly and securely.

And on top it offers extremely fast transaction speeds and low bandwidth consumption resulting in high-throughput, low fees, and finality in seconds.

Hashgraph Consensus

Hedera is based on the hashgraph consensus algorithm. The video explains it quite well, but it's basically a consensus mechanism where besides sharing transactions, participants also share hashes about the all the communication they've been doing themselves, one hash from themselves and one hash from the last person they talked to previously. This will lead to a single graph of communications identical for all participants  which can then be used for virtual voting to decide on the actual order of transactions.

  1. Gossip (sharing transactions)
  2. Gossip about gossip (sharing two hashes about the communication)
  3. Virtual votes (voting algorithm with pre-determined answers from the gossip of gossip)

Hedera Token Service (HTS)

The Hedera Token Service is available since a year and offers several features for developers:

  • Native tokenization:  Deployed tokens are native to Hedera and offer the same performance, security, and efficiency as the native hbar cryptocurrency.
  • Low, predictable fees: Low and predictable transaction fees on the Hedera public network — it costs less than 1¢ USD to transfer any sum of a tokenized asset.
  • Flexible configurations: Fungible and non-fungible tokens deployed using HTS offers flexible configurations, such as atomic swaps and scheduled transactions.
  • Built-in compliance: Key and token configurations at the account level enable businesses to meet compliance needs, including KYC verification and freeze, token supply management, transfer, and more.

Hyperledger Besu EVM

This article demonstrates the native Hedera SDK route used in 2022. Hedera also supports MetaMask and familiar Ethereum development tools through JSON-RPC relays today. See the Hedera developer documentation for current wallet and EVM setup; the native-SDK example below remains a separate historical workflow.

Token Meme

Creating a Token using Solidity and HTS

First create an Npm package and install the dependencies:

Terminal / configuration
$ npm init
$ npm install @hashgraph/sdk dotenv solc@0.8.13

Now let's create a Solidity contract TestToken.sol which uses the Hedera Token Service:

soliditySolidity 0.8.13
// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;

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

contract TestToken is HederaTokenService {
    address public tokenAddress;
    uint256 public totalSupply;

    constructor(address _tokenAddress) {
        tokenAddress = _tokenAddress;
    }

    function mintFungibleToken(uint64 _amount) external {
        (int256 response, uint64 newTotalSupply, ) = HederaTokenService
            .mintToken(tokenAddress, _amount, new bytes[](0));

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

        totalSupply = newTotalSupply;
    }

    function tokenAssociate(address _account) external {
        int256 response = HederaTokenService.associateToken(
            _account,
            tokenAddress
        );

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

    function tokenTransfer(
        address _sender,
        address _receiver,
        int64 _amount
    ) external {
        int256 response = HederaTokenService.transferToken(
            tokenAddress,
            _sender,
            _receiver,
            _amount
        );

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

You can import the HederaTokenService.sol and HederaResponseCodes.sol and IHederaTokenService.sol from here:



We will have a simple minting function that calls the HederaTokenService.mintToken.





And a simple associate function which associates an account with a token so it can receive and hold that token type. Association is not an approval for this contract to spend the account’s tokens; spending still needs the appropriate signatures or allowance.





And transferring will be as simple as calling the HederaTokenService.transferToken.

Example scope: These imports and SDK calls must be pinned to compatible versions. The small wrapper is a demonstration and does not define a production access-control policy for minting or transfers. Add the intended authorization and test it before adapting the design for an application.

Now let's create the bytecode:

Terminal / configuration
$ npx solcjs --bin TestToken.sol

Great! So now how do we deploy this? The answer is using the Hedera SDK. You can choose from


We'll be using the JavaScript one. First we'll need two or three testnet accounts. Register them here and then create a .env file:

Terminal / configuration
OPERATOR_ID=0.0.34281794
OPERATOR_PBKEY=302...
OPERATOR_PVKEY=302...

TREASURY_ID=0.0.34355971
TREASURY_PBKEY=302...
TREASURY_PVKEY=302...

ALICE_ID=0.0.34281793
ALICE_PBKEY=302...
ALICE_PVKEY=302...

The operator will be the account paying all the fees. If you want to make your testing easier, you could use the same key for Alice and the operator.

And now create a new file upload.js with the following content and run it using node upload.js.

File Upload Meme

<------------------------------------------------------------------->






First let's import our .env configuration and the required functionality from @hashgraph/sdk.





<------------------------------------------------------------------->

Now we can create a client and set our operator. And we'll create two query helper functions that will query the token info and token balances.






<------------------------------------------------------------------->


And then in our main function, we can create the fungible token using the TokenCreateTransaction. We will configure all the token details and add the treasury for it.

You will see something like:

  • Token ID: 0.0.34362534
  • Token ID in Solidity format: 00000000000000000000000000000000020c54a6
  • Initial token supply: 100



<------------------------------------------------------------------->


And then in in our main function, we will read the TestToken bytecode file we previously created. We will use it to upload it to the Hedera File Service, so we can later reference this bytecode.


Uploading a file can be done with FileCreateTransaction and FileAppendTransaction. And you should see:

  • Smart contract bytecode file ID is 0.0.34362535
  • Content added: SUCCESS


<------------------------------------------------------------------->



And now let's actually create the smart contract. You can do so using ContractCreateTransaction and passing the bytecode file id as well as the constructor parameter which is the token address itself.

You will see:

  • The smart contract ID is: 0.0.34362536
  • The smart contract ID in Solidity format is: 00000000000000000000000000000000020c54a8


<------------------------------------------------------------------->


So now we can update the Hedera token, so that the smart contract manages the supply using TokenUpdateTransaction and setSupplyKey. You should see:

  • Token supply key: 302a300506...
  • Token update status: SUCCESS
  • Token supply key: 0.0.34362536


<------------------------------------------------------------------->

And now let's execute the mintFungibleToken function using ContractExecuteTransaction. You should see:

  • New tokens minted: SUCCESS
  • New token supply: 250



<------------------------------------------------------------------->

Now execute tokenAssociate using ContractExecuteTransaction. This associates Alice’s account with the token type so it can receive and hold those tokens; it does not give the Solidity contract general spending authority over Alice’s balance.

The original demonstration reports SUCCESS. Check the actual receipt when running a compatible version of the workflow.

<------------------------------------------------------------------->

And now let's execute the tokenTransfer function using ContractExecuteTransaction


You should see:

  • Token transfer from Treasury to Alice: SUCCESS




<------------------------------------------------------------------->

And in the end we query again the treasury's and Alice's balances. You will see something like:

  • Treasury balance: 200 units of token 0.0.34362534
  • Alice balance: 50 units of token 0.0.34362534
javascript
require("dotenv").config();

const {
  Client,
  AccountId,
  PrivateKey,
  TokenCreateTransaction,
  FileCreateTransaction,
  FileAppendTransaction,
  ContractCreateTransaction,
  ContractFunctionParameters,
  TokenUpdateTransaction,
  ContractExecuteTransaction,
  TokenInfoQuery,
  AccountBalanceQuery,
} = require("@hashgraph/sdk");
const fs = require("fs");

const operatorId = AccountId.fromString(process.env.OPERATOR_ID);
const operatorKey = PrivateKey.fromString(process.env.OPERATOR_PVKEY);
const treasuryId = AccountId.fromString(process.env.TREASURY_ID);
const treasuryKey = PrivateKey.fromString(process.env.TREASURY_PVKEY);
const aliceId = AccountId.fromString(process.env.ALICE_ID);
const aliceKey = PrivateKey.fromString(process.env.ALICE_PVKEY);

const client = Client.forTestnet().setOperator(operatorId, operatorKey);

async function queryTokenInfo(tokenId) {
  let info = await new TokenInfoQuery().setTokenId(tokenId).execute(client);
  return info;
}

async function queryAccountBalance(accountId, tokenId) {
  let balanceCheckTx = await new AccountBalanceQuery()
    .setAccountId(accountId)
    .execute(client);
  return balanceCheckTx.tokens._map.get(tokenId.toString());
}

async function main() {
  const tokenCreateTx = await new TokenCreateTransaction()
    .setTokenName("My new Token!")
    .setTokenSymbol("MNT")
    .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}`);

  const tokenInfo1 = await queryTokenInfo(tokenId);
  console.log(`- Initial token supply: ${tokenInfo1.totalSupply.toString()} \n`);

  const bytecode = fs.readFileSync("./TestToken_sol_TestToken.bin");
  const fileCreateTx = new FileCreateTransaction()
    .setKeys([treasuryKey])
    .freezeWith(client);
  const fileCreateSign = await fileCreateTx.sign(treasuryKey);
  const fileCreateSubmit = await fileCreateSign.execute(client);
  const fileCreateRx = await fileCreateSubmit.getReceipt(client);
  const bytecodeFileId = fileCreateRx.fileId;
  console.log(`- The smart contract bytecode file ID is ${bytecodeFileId}`);

  const fileAppendTx = new FileAppendTransaction()
    .setFileId(bytecodeFileId)
    .setContents(bytecode)
    .setMaxChunks(10)
    .freezeWith(client);
  const fileAppendSign = await fileAppendTx.sign(treasuryKey);
  const fileAppendSubmit = await fileAppendSign.execute(client);
  const fileAppendRx = await fileAppendSubmit.getReceipt(client);
  console.log(`- Content added: ${fileAppendRx.status} \n`);

  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`
  );

  const tokenInfo2p1 = await queryTokenInfo(tokenId);
  console.log(`- Token supply key: ${tokenInfo2p1.supplyKey.toString()}`);

  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}`);

  const tokenInfo2p2 = await queryTokenInfo(tokenId);
  console.log(`- Token supply key: ${tokenInfo2p2.supplyKey.toString()} \n`);

  const contractExecTx = await new ContractExecuteTransaction()
    .setContractId(contractId)
    .setGas(3000000)
    .setFunction(
      "mintFungibleToken",
      new ContractFunctionParameters().addUint64(150)
    );
  const contractExecSubmit = await contractExecTx.execute(client);
  const contractExecRx = await contractExecSubmit.getReceipt(client);
  console.log(`- New tokens minted: ${contractExecRx.status.toString()}`);

  const tokenInfo3 = await queryTokenInfo(tokenId);
  console.log(`- New token supply: ${tokenInfo3.totalSupply.toString()} \n`);

  const contractExecTx1 = await new ContractExecuteTransaction()
    .setContractId(contractId)
    .setGas(3000000)
    .setFunction(
      "tokenAssociate",
      new ContractFunctionParameters().addAddress(aliceId.toSolidityAddress())
    )
    .freezeWith(client);
  const contractExecSign1 = await contractExecTx1.sign(aliceKey);
  const contractExecSubmit1 = await contractExecSign1.execute(client);
  const contractExecRx1 = await contractExecSubmit1.getReceipt(client);
  console.log(
    `- Token association with Alice's account: ${contractExecRx1.status.toString()} \n`
  );

  const contractExecTx2 = await new ContractExecuteTransaction()
    .setContractId(contractId)
    .setGas(3000000)
    .setFunction(
      "tokenTransfer",
      new ContractFunctionParameters()
        .addAddress(treasuryId.toSolidityAddress())
        .addAddress(aliceId.toSolidityAddress())
        .addInt64(50)
    )
    .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()}`
  );

  const treasuryBalance = await queryAccountBalance(treasuryId, tokenId);
  const aliceBalance = await queryAccountBalance(aliceId, tokenId);
  console.log(`- Treasury balance: ${treasuryBalance} units of token ${tokenId}`);
  console.log(`- Alice balance: ${aliceBalance} units of token ${tokenId} \n`);
}

main();

Historical context: The linked “coming” announcement is a 2022 roadmap reference, not a current availability statement. Check Hedera’s current fee and entity-lifecycle documentation before estimating ongoing costs.

And lastly, note that smart contract rent is coming to Hedera, something that used to be discussed for Ethereum.

Bridge

Using the Hedera Bridge

Hedera Bridge

You can use the bridge here to transfer funds in and out of the Hedera Chain either from the Ethereum network or Polygon. It will lock the tokens on the bridge contract in the Ethereum mainnet. The bridge works bidirectionally, meaning you can transfer assets

  1. from Ethereum/Polygon to Hedera and
  2. from Hedera to Ethereum/Polygon


And that's it. Have you used Hedera before yourself? Did I miss anything? Let me know in the comments.