Hedera Token Services provide several methods to create both Fungible and Non-Fungible Tokens. Each method providing different capabilities for token creation. The following is the list of methods to create tokens.
/// Creates a Fungible Token with the specified properties
/// @param token the basic properties of the token being created
/// @param initialTotalSupply Specifies the initial supply of tokens to be put in circulation. The
/// initial supply is sent to the Treasury Account. The supply is in the lowest denomination possible.
/// @param decimals the number of decimal places a token is divisible by
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
/// @return tokenAddress the created token's address
function createFungibleToken(
HederaToken memory token,
int64 initialTotalSupply,
int32 decimals
) external payable returns (int64 responseCode, address tokenAddress);
/// Creates a Fungible Token with the specified properties
/// @param token the basic properties of the token being created
/// @param initialTotalSupply Specifies the initial supply of tokens to be put in circulation. The
/// initial supply is sent to the Treasury Account. The supply is in the lowest denomination possible.
/// @param decimals the number of decimal places a token is divisible by.
/// @param fixedFees list of fixed fees to apply to the token
/// @param fractionalFees list of fractional fees to apply to the token
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
/// @return tokenAddress the created token's address
function createFungibleTokenWithCustomFees(
HederaToken memory token,
int64 initialTotalSupply,
int32 decimals,
FixedFee[] memory fixedFees,
FractionalFee[] memory fractionalFees
) external payable returns (int64 responseCode, address tokenAddress);
/// Creates an Non Fungible Unique Token with the specified properties
/// @param token the basic properties of the token being created
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
/// @return tokenAddress the created token's address
function createNonFungibleToken(HederaToken memory token)
external
payable
returns (int64 responseCode, address tokenAddress);
/// Creates an Non Fungible Unique Token with the specified properties
/// @param token the basic properties of the token being created
/// @param fixedFees list of fixed fees to apply to the token
/// @param royaltyFees list of royalty fees to apply to the token
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
/// @return tokenAddress the created token's address
function createNonFungibleTokenWithCustomFees(
HederaToken memory token,
FixedFee[] memory fixedFees,
RoyaltyFee[] memory royaltyFees
) external payable returns (int64 responseCode, address tokenAddress);
On the other hand, the method to delete an HTS token, either Fungible or Non-Fungible is the following
/// Operation to delete token
/// @param token The token address to be deleted
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
function deleteToken(address token) external returns (int64 responseCode);
The main goal of this document is to describe how to enable token creation and deletion on HTS emulation.
The introduction of token creation unlocks a new capability: Using the HTS emulation without forking. In order to use HTS emulation, a token must be present in the Ethereum Development Environment network, e.g., Hardhat's EDR or Foundry's Anvil. This can be done in two ways. Fetching the token state from a remote network, or by creating a token locally altogether.
Compare this to the HTS mock present in the Smart Contracts repo. In addition to providing the same features as the HTS mock, we provide forking capabilities as well.
The following auxiliary definitions are needed. See hashgraph/hedera-smart-contracts#1026 for more details.
interface IHtsFungibleToken is IERC20, IHRC719 {}
interface IHtsNonFungibleToken is IERC721, IHRC719 {}
All token creation methods receive a token
, which refers to the HederaToken
used to configure the initial values of the token being created.
They return a tokenAddress
, which will be the address of the newly created token.
A successful call to any token creation method should have the following effect
- The associated bytecode of
tokenAddress
should be that of the Proxy Contract as defined by HIP719 § Specification. Note that thetokenAddress
should be embedded into the Proxy bytecode as noted by the HIP. - The balance of the
token.treasury
should betotalSupply
. - The
token.treasury
address should be associated to the token. - The token should not have any other balances nor associations.
- A non-fungible token should not have any minted NFT.
- An immediate call to
IHederaTokenService(0x167).getTokenInfo(tokenAddress)
should returntoken
. - All methods in either
IHtsFungibleToken(tokenAddress)
orIHtsNonFungibleToken(tokenAddress)
should be available to invoke.
HTS emulation should keep an internal counter to track the next token address to be created.
- In forking mode, this counter should resemble the next token address from the remote network. This can be obtained by querying the Mirror Node for the latest account and token IDs and getting the latest (higher) between these two. The latest account ID can be obtained with
/v1/accounts?order=desc&limit=1
. On the other hand, the latest token ID can be obtained with/v1/tokens?order=desc&limit=1
. - In non-forking mode, this counter should be
0.0.1032
, the first token account ID created by local node when--createInitialResources=true
is specified.
Similarly, a successful call to deleteToken
should have the following effect
- The associated bytecode of
tokenAddress
should be0x
. - No calls to either
IHtsFungibleToken(tokenAddress)
orIHtsNonFungibleToken(tokenAddress)
should be possible.
A deleteToken
operation should leave the Token Creation Counter untouched.
A token create method should first check that token
is valid, e.g., token.name
is not empty.
Then a TokenInfo
variable should be created containing token
and the remaining creation arguments as applicable.
After that, the created TokenInfo
should be copied into internal _tokenInfo
of the tokenAddress
slot space.
In order to achieve that, the Proxy bytecode is deployed at tokenAddress
using vm.etch
.
Then, a call into redirectForToken
is used to copy the TokenInfo
into the storage space of tokenAddress
.
Note
To avoid changing the redirectForToken
and _initTokenData
interaction, we can deploy a temporary contract that copies _tokenInfo
into its own storage space. After that, we deploy the Proxy Contract bytecode at tokenAddress
.
In a previous iteration, a temporary contract was used to copy _tokenInfo
into the token's storage space.
However, this did not play well using Hardhat.
Replacing the contract bytecode from within the EVM is only possible in Hardhat when the cache is disabled.
But disabling the cache would have a heavy performance penalty for users.
That is why we decided to go with the single deployment approach.
The main issue with the Hardhat plugin is how to deploy the HIP 719 Proxy Contract into a predefined address, tokenAddress
.
As expected, this is not possible using only vanilla EVM.
As we saw in the previous section, the Foundry library leverages Foundry cheatcode vm.etch
to deploy the Proxy Contract to a predefined address.
This can be done regardless of the forking environment.
However, the Hardhat plugin works in a completely different manner.
If fork is disabled, we simply cannot deploy into a predefined address as mentioned above.
We cannot use hardhat_setcode
either.
This is because there is no way to hook into EVM internal calls (the same reason we had to introduce the JSON-RPC Forwarder in the first place).
A possible solution could be to use the JSON-RPC Forwarder with no backend Mirror Node.
Then we can introduce a dummy slot for eth_getStorageAt
that signals the creation of a token (see § Token Creation Counter).
The JSON-RPC Forwarder can then return the Proxy Contract bytecode when a request eth_getCode(tokenAddress)
is performed.
This means that even if the user selects a non-forking environment, Hardhat will run in a forked one, against a no-backend JSON-RPC Forwarder.