Deploying Solidity Contracts in Hedera
What is Hedera and how can you use it.
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!
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.
- Gossip (sharing transactions)
- Gossip about gossip (sharing two hashes about the communication)
- 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
And now recently Hedera integrated the Hyperledger Besu EVM, based on an open-source Ethereum client written in Java. You can write smart contracts in Solidity for Hedera with increased performance! The integration is a bit different to other chains though. There's no MetaMask support, instead you need to use Hedera native toolings, e.g. HashPack is a great wallet extension.
Creating a Token using Solidity and HTS
First create an Npm package and install the dependencies:
$ npm init
$ npm install @hashgraph/sdk dotenv solc
Now let's create a Solidity contract TestToken.sol
which uses the Hedera Token Service:
// 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 will connect a Hedera Token for an account with this contract, allowing it to be managed by this contract.
And transferring will be as simple as calling the HederaTokenService.transferToken
.
Now let's create the bytecode:
$ 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:
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
.
<------------------------------------------------------------------->
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
<------------------------------------------------------------------->
And now let's execute the tokenAssociate
function using ContractExecuteTransaction
. This will associate the Solidity contract with the token for Alice.
You should see:
- Token association with Alice's account: SUCCESS
<------------------------------------------------------------------->
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
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.low} \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.low} \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();
And lastly, note that smart contract rent is coming to Hedera, something that used to be discussed for Ethereum.
Using the 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
- from Ethereum/Polygon to Hedera and
- 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.
Solidity Developer