The implementation of HIP-573 in mainnet release v0.31 (November 10th, 2022) enables token creators whose tokenomics require custom fees and different collection accounts to exempt fee collectors from paying custom fees when exchanging token units.
This example guides you through the following steps:
To simplify creating accounts, you need to create a helper function. In this example, the function is called accountCreator, and the parameters you need to specify are an initial balance and a private key. Note that the token association is automatic to further simplify the tutorial, as you can see below.
const accountCreator = async (initialBalance, privateKey) => { const createAccount = new AccountCreateTransaction() .setInitialBalance(initialBalance) .setKey(privateKey.publicKey) .setAlias(privateKey.publicKey.toEvmAddress()) .setMaxAutomaticTokenAssociations(10) .freezeWith(client); const createAccountTx = await createAccount.execute(client); const createAccountRx = await createAccountTx.getReceipt(client); return createAccountRx.accountId; }
Now, you can use the helper function to create three different accounts that will be the collectors of the token fees.
// STEP 1: Create three accounts using the helper function const accountKey1 = PrivateKey.generateECDSA(); const accountId1 = await accountCreator(50, accountKey1); const accountKey2 = PrivateKey.generateECDSA(); const accountId2 = await accountCreator(50, accountKey2); const accountKey3 = PrivateKey.generateECDSA(); const accountId3 = await accountCreator(50, accountKey3); console.log(`STEP 1 - Three accounts created: \n ${accountId1} \n ${accountId2} \n ${accountId3}\n`);
Console Output:
Let’s define three fractional fees for the fungible token we are about to create. During a token transfer, the first account will receive 1% of the amount, the second 2%, and the third 3%.
Note that the critical factor of this tutorial is using the setAllCollectorsAreExempt() extension method on fractional fee creation, as you can see below.
const fee1 = new CustomFractionalFee() .setFeeCollectorAccountId(accountId1) .setNumerator(1) .setDenominator(100) .setAllCollectorsAreExempt(true); const fee2 = new CustomFractionalFee() .setFeeCollectorAccountId(accountId2) .setNumerator(2) .setDenominator(100) .setAllCollectorsAreExempt(true); const fee3 = new CustomFractionalFee() .setFeeCollectorAccountId(accountId3) .setNumerator(3) .setDenominator(100) .setAllCollectorsAreExempt(true);
Now, you can create the fungible token and specify the fees you just defined. Remember to sign the TokenCreateTransaction() using all the accounts (fee collectors included).
const createToken = new TokenCreateTransaction() .setTokenName("HIP-573 Token") .setTokenSymbol("H573") .setTokenType(TokenType.FungibleCommon) .setTreasuryAccountId(operatorId) .setAutoRenewAccountId(operatorId) .setAdminKey(operatorKey) .setFreezeKey(operatorKey) .setWipeKey(operatorKey) .setInitialSupply(100000000) // Total supply = 100000000 / 10 ^ 2 .setDecimals(2) .setCustomFees([fee1, fee2, fee3]) .setMaxTransactionFee(new Hbar(40)) .freezeWith(client); const createTokenSigned1 = await createToken.sign(accountKey1); const createTokenSigned2 = await createTokenSigned1.sign(accountKey2); const createTokenSigned3 = await createTokenSigned2.sign(accountKey3); const createTokenTx = await createTokenSigned3.execute(client); const createTokenRx = await createTokenTx.getReceipt(client); const tokenId = createTokenRx.tokenId; console.log(`STEP 2 - Token with custom fees created: ${tokenId}\n`);
Before transferring a token from one collection account to another, you first need to transfer some tokens from the treasury account (in this example, the operator) to one of the collection accounts.
// STEP 3: Send token from treasury to one account and from one account to another const transferFromTreasuryTx = await new TransferTransaction() .addTokenTransfer(tokenId, operatorId, -10000) .addTokenTransfer(tokenId, accountId2, 10000) .freezeWith(client) .execute(client); const transferFromTreasuryRx = await transferFromTreasuryTx.getReceipt(client); const transferFromTreasuryStatus = transferFromTreasuryRx.status.toString(); console.log(`STEP 3 \nToken transfer from treasury to account 2: ${transferFromTreasuryStatus}`);
Now that Account 2 has 10.000 fungible tokens, you can try to transfer the tokens from Account 2 to Account 1.
const transferFromAccount2 = await new TransferTransaction() .addTokenTransfer(tokenId, accountId2, -10000) .addTokenTransfer(tokenId, accountId1, 10000) .freezeWith(client) .sign(accountKey2); const transferFromAccount2Tx = await transferFromAccount2.execute(client); const transferFromAccount2Rx = await transferFromAccount2Tx.getReceipt(client); console.log(`Transfer from account 2 to account 1: ${transferFromAccount2Rx.status.toString()}\n`);
Done! Now you need to check each account balance to verify if they are actually exempt from token fees.
// Check collectors account balance (methods will be deprecated soon, use axios and mirror node api) const checkBalance1 = await new AccountBalanceQuery() .setAccountId(accountId1) .execute(client); const balance1 = checkBalance1.tokens._map.get(tokenId.toString()); const checkBalance2 = await new AccountBalanceQuery() .setAccountId(accountId2) .execute(client); const balance2 = checkBalance2.tokens._map.get(tokenId.toString()); const checkBalance3 = await new AccountBalanceQuery() .setAccountId(accountId3) .execute(client); const balance3 = checkBalance3.tokens._map.get(tokenId.toString()); console.log(`Accounts Balance: \nAccount 1: ${balance1} \nAccount 2: ${balance2} \nAccount 3: ${balance3} \n`);
v0.30
v0.31
As you can see in v0.30, the third collector receives 3% of the 10.000 transferred from the second to the first collector. While in v0.31, fee collectors are exempt from fees.
For further questions, don't hesitate to get in touch with us on the Hedera Discord Server.
Check out this code example on GitHub
Hedera Improvement Proposals
Custom Token Fees (Documentation)