Allowing an address to grant token allowance to another address is supported in the ERC-20 (FT) standard. In the case of ERC-721 (NFT) standard one can approve an address for a specific NFTs or approve for all assets to be operated by the designated approved address. This grants the ability to have another address (sender) move tokens from another address (owner) with a high level of autonomy.
Let’s take an example of subscribing to a video streaming service that is built on Hedera and has their own fungible tokens: VID. You, as the owner of 100 VID tokens, would grant the video streaming service an allowance of 5 VID. This allows the video streaming service to automatically remove the allowed amount of tokens from your account without you having to personally interact and approve each transaction. In the web2 world, this type of work flow is done without much thought and is an expected feature from a service. The key difference here is that this is a decentralized workflow that involves smart contracts, accounts, and tokens.
You can treat a Hedera Native Token, Fungible or Non-Fungible, like an ERC token by creating a smart contract that leverages the ERC standard contract calls and using the Hedera SDK. Follow along as we walk through approving an allowance for an account, transferring ownership of tokens using the allowance and removing the previously approved allowance.
Example: Alice Spends FT and NFT on Behalf of the Treasury
This example guides you through the following steps:
Setting up helper functions required to complete the example
Creating Hedera accounts and native tokens (Treasury, Alice, Bob, the HBAR ROCKS FT, and 5 HBAR RULES NFTs)
Treasury approving an allowance of tokens for Alice (50 FT / 3 NFT)
Alice performing an approved token transfer from Treasury to Bob (30 FT / 2 NFT)
Treasury removing the token allowance for Alice
The diagram below depicts a high-level flow of approving token allowances.
Using the Hedera Javascript SDK we will make contract function calls to approve an allowance, transfer tokens to and from accounts, check allowance, and get approved addresses. This combines using the Hedera SDK and ERC standard calls.
Setting up our helper functions
Since we need to create a couple of accounts on top of creating FT and NFT we will first create the functions to help us achieve that. You can swap between the different helper functions by using the tabs on the upper left of the code box.
create account with an initial balance.
create account with an initial balance.
create a simple fungible token type
create simple non-fungible token
create a new nft collection and mint token
// create an account with an initial balance
export const createAccount = async (client: Client, initialBalance: number): Promise<[AccountId, PrivateKey]> => {
const accountPrivateKey = PrivateKey.generateED25519();
const response = await new AccountCreateTransaction()
.setInitialBalance(new Hbar(initialBalance))
.setKey(accountPrivateKey)
.execute(client);
const receipt = await response.getReceipt(client);
if (receipt.accountId === null) {
throw new Error("Somehow accountId is null.");
}
return [receipt.accountId, accountPrivateKey];
};
export const createFungibleToken = async (
client: Client,
treasureyAccId: string | AccountId,
supplyKey: PrivateKey,
treasuryAccPvKey: PrivateKey,
initialSupply: number,
tokenName: string,
tokenSymbol: string,
): Promise<{
tokenId: TokenId,
tokenIdInSolidityFormat: string
}> => {
/*
* Create a transaction with token type fungible
* Returns Fungible Token Id and Token Id in solidity format
*/
const createTokenTxn = await new TokenCreateTransaction()
.setTokenName(tokenName)
.setTokenSymbol(tokenSymbol)
.setTokenType(TokenType.FungibleCommon)
.setInitialSupply(initialSupply)
.setTreasuryAccountId(treasureyAccId)
.setSupplyKey(supplyKey)
.setMaxTransactionFee(new Hbar(30))
.freezeWith(client); //freeze tx from from any further mods.
const createTokenTxnSigned = await createTokenTxn.sign(treasuryAccPvKey);
// submit txn to heder network
const txnResponse = await createTokenTxnSigned.execute(client);
// request receipt of txn
const txnRx = await txnResponse.getReceipt(client);
const txnStatus = txnRx.status.toString();
const tokenId = txnRx.tokenId;
if (tokenId === null) {
throw new Error("Somehow tokenId is null");
}
const tokenIdInSolidityFormat = tokenId.toSolidityAddress();
console.log(
`Token Type Creation was a ${txnStatus} and was created with token id: ${tokenId}`
);
console.log(`Token Id in Solidity format: ${tokenIdInSolidityFormat}`);
return {
tokenId,
tokenIdInSolidityFormat,
};
};
export const createNonFungibleToken = async (
client: Client,
treasureyAccId: string | AccountId,
supplyKey: PrivateKey,
treasuryAccPvKey: PrivateKey,
initialSupply: number,
tokenName: string,
tokenSymbol: string,
): Promise<[TokenId | null, string]> => {
/*
* Create a transaction with token type fungible
* Returns Fungible Token Id and Token Id in solidity format
*/
const createTokenTxn = await new TokenCreateTransaction()
.setTokenName(tokenName)
.setTokenSymbol(tokenSymbol)
.setTokenType(TokenType.NonFungibleUnique)
.setInitialSupply(initialSupply)
.setTreasuryAccountId(treasureyAccId)
.setSupplyKey(supplyKey)
.setMaxTransactionFee(new Hbar(30))
.freezeWith(client); //freeze tx from from any further mods.
const createTokenTxnSigned = await createTokenTxn.sign(treasuryAccPvKey);
// submit txn to hedera network
const txnResponse = await createTokenTxnSigned.execute(client);
// request receipt of txn
const txnRx = await txnResponse.getReceipt(client);
const txnStatus = txnRx.status.toString();
const tokenId = txnRx.tokenId;
if (tokenId === null ) { throw new Error("Somehow tokenId is null.");}
const tokenIdInSolidityFormat = tokenId.toSolidityAddress();
console.log(
`Token Type Creation was a ${txnStatus} and was created with token id: ${tokenId}`
);
console.log(`Token Id in Solidity format: ${tokenIdInSolidityFormat}`);
return [tokenId, tokenIdInSolidityFormat];
};
Create the treasury’s, Alice’s, and Bob’s accounts by awaiting the createAccount helper function. Each account will be created with an initial balance of 100 HBAR.
Next, we create our fungible token with an initial supply of 100. We also generate a supply key in case we want to change the supply and mint more tokens later.
Similar to seeing the helper functions, use the tabs switcher to see the code that will create a new NFT collection called HBAR RULES and mint 5 NFTs.
// create token collection and print initial supply
const txnResponse = await createNewNftCollection(client, 'HBAR RULES', 'HRULES', metadataIPFSUrls, treasuryAccId, treasuryAccPvKey);
const TOKEN_ID_IN_SOLIDITY_FORMAT = txnResponse.tokenId.toSolidityAddress();
console.log(`Token Id in solidity format: ${TOKEN_ID_IN_SOLIDITY_FORMAT}`);
Bob will need to associate with these new tokens in order to receive it. Let’s go ahead and do that next.
// Bob must associate to receive token
await associateToken(client, tokenId, bobAccId, bobAccPvKey)
3. Create our Smart Contract leveraging IERC20 and IERC721 Respectively
After we’ve generated our accounts, and have created both our FTs and NFTs, let’s create two simple smart contracts that will use openzeppelin IERC20.sol and IERC721.sol respectively. Make sure to import openzeppelin IERC20.sol if working with FTs and import IERC721.sol if working with NFTs.
Continue to write the rest of the contracts as follows:
IERC20 Smart Contract
IERC20 Smart Contract
IERC721 Smart Contract
contract ERC20FungibleToken {
/// @notice Approve set amount as the allowance of spender over caller's tokens
/// @param token address of tokens to approve
/// @param spender address who will receive allowance
/// @param amount that the spender is able to use
function approve(address token, address spender, uint256 amount) public returns (bool result) {
(bool success, ) = token.delegatecall(
abi.encodeWithSelector(
IERC20.approve.selector,
spender,
amount
)
);
return success;
}
/// @notice Check a spender accounts allowance
/// @param token address of token you are checking allowance on
/// @param owner address of caller
/// @param spender the address with an allowance
/// @return allowance The allowance of the spender over the callers tokens
function checkAllowance(address token, address owner, address spender) public view returns (uint256 allowance){
return IERC20(token).allowance(owner, spender);
}
/// @notice Transfer the set amount of token from the sender (from) to the recipeint (to) through allowance
/// @param token address of the token
/// @param sender address with allowance (the from address)
/// @param recipient address recieving token (the to address)
/// @return result a bool whether it was successfuly or a failure
function transferFrom(address token, address sender, address recipient, uint256 amount) public returns (bool result) {
(bool success, ) = token.delegatecall(
abi.encodeWithSelector(
IERC20.transferFrom.selector,
sender,
recipient,
amount
)
);
return success;
}
fallback () external{}
}
contract ERC721NonFungibleToken {
/// @notice Approve spender address to spend specific NFTs on behalf of msg.sender
/// @param token address of the token to approve
/// @param spender address of spender
/// @param serialNumber is the tokenId of NFT
function approve(address token, address spender, uint256 serialNumber) public returns (bool result) {
(bool success, ) = token.delegatecall(
abi.encodeWithSelector(
IERC721.approve.selector,
spender,
serialNumber
)
);
return success;
}
/// @notice Get the approved address by serial number for a single NFT
/// @param token address of token to approve
/// @param tokenId represents the NFT serial number
/// @return spender address approved for the NFT, or zero addres if there is none
function getApprovedBySerialNumber(address token, uint256 tokenId) external view returns (address spender) {
return IERC721(token).getApproved(tokenId);
}
/// @notice Transfer NFT to a different owner
/// @param token address of token to transfer
/// @param sender is the address from where the NFT is being transferred from
/// @param recipient is the address to where the NFT is being transferred to
/// @return result success if transfer was successful
function transferFrom(address token, address sender, address recipient, uint256 serialNumber) public returns (bool result) {
(bool success, ) = token.delegatecall(
abi.encodeWithSelector(
IERC721.transferFrom.selector,
sender,
recipient,
serialNumber
)
);
return success;
}
fallback () external{}
}
note: It is imperative to make a delegatecall instead of a call for approve and transferFrom so the sender remains to be the treasury account which will be the one granting the allowance to Alice.
Setting up our smart contract helper functions
Once we have our smart contract created. It is time to deploy them and start making contract calls. Let’s take some time to set up two helper functions for deploying and another one for calling a contract function.
A function to deploy our smart contract
A function to deploy our smart contract
A function to execute a contract function.
/*
* Stores the bytecode and deploys the contract to the Hedera network.
* Return an array with the contractId and contract solidity address.
*
* Note: This single call handles what FileCreateTransaction(), FileAppendTransaction() and
* ContractCreateTransaction() classes do.
*/
const deployContract = async (client, bytecode, gasLimit) => {
const contractCreateFlowTxn = new ContractCreateFlow()
.setBytecode(bytecode)
.setGas(gasLimit);
console.log(`- Deploying smart contract to Hedera network`)
const txnResponse = await contractCreateFlowTxn.execute(client);
const txnReceipt = await txnResponse.getReceipt(client);
const contractId = txnReceipt.contractId;
const contractSolidityAddress = contractId.toSolidityAddress();
console.log(`- The smart contract Id is ${contractId}`);
console.log(`- The smart contract Id in Solidity format is ${contractSolidityAddress}\n`);
return [contractId, contractSolidityAddress];
}
Compile our contract using solc or remix ide. Once that is done let's deploy our smart contract by calling and awaiting the helper function deployContract.
Deploy IERC20 Smart Contract
Deploy IERC20 Smart Contract
Deploy IERC721 Smart Contract
/*
* Read compiled byte code
* Note: You can compile your smart contract on Remix ide or use solc
*/
const bytecode = fs.readFileSync("binaries/contracts_ERC20FungibleToken_sol_ERC20FungibleToken.bin");
// Deploy contract
const gasLimit = 1000000;
const [contractId, contractSolidityAddress] = await deployContract(client, bytecode, gasLimit);
/*
* Read compiled byte code
* Note: You can compile your smart contract on Remix ide or use solc
*/
const bytecode = fs.readFileSync("binaries/contracts_ERC721NonFungibleToken_sol_ERC721NonFungibleToken.bin");
// Deploy contract
const gasLimit = 1000000;
const [contractId, contractSolidityAddress] = await deployContract(client, bytecode, gasLimit);
Before executing contract calls It is important that we set the operator to be the Treasury account.
// set operator to be treasury account (treasury account is now the caller of the smart contract)
client.setOperator(treasuryAccId, treasuryAccPvKey);
4. Execute approve ERC standard contract call
Let’s build our function parameters. The approve function expects the address of the token we are granting approval for. In our code this address is TOKEN_ID_IN_SOLIDITY_FORMAT. Next, we need to add the spender's, Alice, solidity's address.
Lastly, when working with Fungible Tokens we specify the amount of the approved allowance that Alice will be receiving. Alice will receive an allowance of 50 HBAR ROCKS FTs.
When working with Non-Fungible tokens we grant an allowance to specific NFT serial numbers. Alice's account will be approved for the NFTs HBAR RULES with serial #s 1, 3, and 5.
Finally, we can execute a contract call function. We call our helper function executeContractFunction and pass in the client, contractId, gas limit, the function name, function parameters, and the private key that will sign the transaction.
// NFTs with serial #1, #3, and #5 to approve
const nFTsToApprove = [1, 3, 5];
console.log(`------- Start approval of NFTs ------\n`);
for (let i = 0; i < nFTsToApprove.length; i++) {
// Setting the necessary parameters to execute the approve contract function
const approveParams = new ContractFunctionParameters()
.addAddress(TOKEN_ID_IN_SOLIDITY_FORMAT)
.addAddress(ALICE_ACCOUNT_IN_SOLIDITY_FORMAT)
.addUint256(nFTsToApprove[i]);
await executeContractFunction(
client,
contractId,
4_000_000,
'approve',
approveParams,
treasuryAccPvKey);
}
4a. Complete a checkAllowance and getApproved Contract Call
The ERC20 standard gives you the ability to check the approved allowance. We do that by calling the checkAllowance standard call which will return the remaining number of tokens that the spender, Alice, is allowed to spend on behalf of the owner.
In ERC-721, there is no allowance because every NFT is unique, the quantity is none or one. Instead, you call the getApproved standard call which will return the address approved for the specific NFT. In the NFT GetApproved tab, you notice we have an array called contractFunctionRes which will hold the result of our contract call. We then loop through each nft in the collection and call our contract function. This will return the address approved for that NFT. If there is no address, then it will return the zero address: 0x0000000000000000000000000000000000000000
const allowanceParams = new ContractFunctionParameters()
.addAddress(tokenIdInSolidityFormat)
.addAddress(treasuryAccId.toSolidityAddress())
.addAddress(ALICE_ACCOUNT_IN_SOLIDITY_FORMAT);
// check the allowance
const contractFunctionResult = await executeContractFunction(
client,
contractId,
4_000_000,
'checkAllowance',
allowanceParams,
treasuryAccPvKey);
if (contractFunctionResult) {
console.log(`Alice has an allowance of ${contractFunctionResult.getUint256(0)}`);
}
// set the client back to the operator account
client.setOperator(operatorAccountId, operatorPrivateKey);
await checkBalance(treasuryAccId, tokenId, client);
await checkBalance(bobAccId, tokenId, client);
5. Complete the transfer of FTs and NFTs
Make Alice the client in order to do the transferring on behalf of the treasury account.
// make alice the client to excute the contract call.
client.setOperator(aliceAccId, aliceAccPvKey);
Alice will gladly use their allowance to transfer 30 fungible tokens to Bob. When building the contract function parameters for the ERC20 transferFrom contract call we make the from address the treasury accounts id in solidity format, and the to address is Bob’s account id in solidity format. The amount we want to transfer here is 30 FTs.
Switching tabs to the Non-Fungible token code, you see that we have an array and a for loop to transfer the NFTs with serial #s 3 and 5. We also build the contract function parameters by first providing the tokenId in solidity address as the from address, and the to address is Bob's account id in solidity format. We then loop of the array nftsToTransferwhich will ensure we transfer NFT serial #3, and #5 to Bob.
Once we’re ready to execute the transferFrom Contract function, Alice will be the one signing the transaction so make sure we send in Alice's account private key. We’ll also do another quick balance check of the treasury account and bob’s account once the transfer of tokens is complete.
const nftsToTransfer = [3, 5];
console.log(`------- Start transfer of ownership for NFTs ------\n`)
for (let i = 0; i < nftsToTransfer.length; i++) {
// Setting the necessary parameters to execute the approve contract function
const transferFromParams = new ContractFunctionParameters()
.addAddress(TOKEN_ID_IN_SOLIDITY_FORMAT)
.addAddress(treasuryAccId.toSolidityAddress())
.addAddress(bobAccId.toSolidityAddress())
.addUint256(nftsToTransfer[i]);
await executeContractFunction(
client,
contractId,
4_000_000,
'transferFrom',
transferFromParams,
treasuryAccPvKey);
}
await checkAccountBalance(treasuryAccId, txnResponse.tokenId, client);
await checkAccountBalance(bobAccId, txnResponse.tokenId, client);
The output for FT shows that Alice transferred 30 FTs from the Treasury to Bob. Bob is shown to have 30 FTs and the Treasury had 100 FT but now is shown to have 70. As for the NFT's Alice transferred 2 NFT's to Bob from the Treasury. Therefore, the Treasury now has 3 NFTs.
6. Remove FT/NFT Allowance
The Treasury will remove the allowance that Alice has over their Hedera ROCKS fungible tokens by executing another approve contract call function. This time the amount is set to 0. Execute the allowance contract call function to check Alice's allowance.
Similarly, in order to remove the NFT allowance, you must execute an approve contract call where the sender address is the zero address. In addition, completing a transfer of the NFT will remove the approval. We have already transferred NFTs with serial numbers 3 and 5. Therefore, we will only need to remove the approval of NFT serial number 1.
Lastly, execute the getApproved contract function to ensure all the approved addresses returned are the zero address. Be sure to set the client to be the Treasury before executing the call.
// set operator to be treasury account (treasury account is now the caller of the smart contract)
client.setOperator(treasuryAccId, treasuryAccPvKey);
FT Delete Allowance
FT Delete Allowance
NFT Delete Allowance
// remove Alice's allowance
const removeApproveParams = new ContractFunctionParameters()
.addAddress(tokenIdInSolidityFormat)
.addAddress(ALICE_ACCOUNT_IN_SOLIDITY_FORMAT)
.addUint256(0);
await executeContractFunction(
client,
contractId,
4_000_000,
'approve',
removeApproveParams,
treasuryAccPvKey);
// check allowance after it has been removed
const checkallowanceParams = new ContractFunctionParameters()
.addAddress(tokenIdInSolidityFormat)
.addAddress(treasuryAccId.toSolidityAddress())
.addAddress(ALICE_ACCOUNT_IN_SOLIDITY_FORMAT);
const contractFunctionRes = await executeContractFunction(
client,
contractId,
4_000_000,
'checkAllowance',
checkallowanceParams,
treasuryAccPvKey);
if (contractFunctionRes) {
console.log(`Alice has an allowance of ${contractFunctionRes.getUint256(0)}`);
}
client.close();
/*
* Remove NFT approval
*/
const nftsToRemoveApproval = [1];
console.log(`------- Start removal of approval for NFTs -------\n`);
for (let i = 0; i < nftsToRemoveApproval.length; i++) {
// Setting the necessary paramters to execute the approve contract function
const approveParams = new ContractFunctionParameters()
.addAddress(TOKEN_ID_IN_SOLIDITY_FORMAT)
.addAddress('0x0000000000000000000000000000000000000000')
.addUint256(nftsToRemoveApproval[i]);
await executeContractFunction(
client,
contractId,
4_000_000,
'approve',
approveParams,
treasuryAccPvKey);
}
/*
* get the approved address for each NFT in the collection
* returns the approved address or the zero address if there is none
*/
let contractFunctionResult = [];
console.log(` - Get approved address for each NFT in collection ${txnResponse.tokenId} (should all be zero address)`)
for(let serialNum=1; serialNum < 6; serialNum++) {
const getApprovedParams = new ContractFunctionParameters()
.addAddress(TOKEN_ID_IN_SOLIDITY_FORMAT)
.addUint256(serialNum);
contractFunctionResult.push(await executeContractFunction(
client,
contractId,
4_000_000,
'getApprovedBySerialNumber',
getApprovedParams,
treasuryAccPvKey));
}
if (contractFunctionResult) {
contractFunctionResult.forEach((result) => {
console.log(`\n- Approved Address: ${result?.getAddress()}`);
})
}
client.close();
Summary
That concludes the tutorial for granting an allowance to an account, completing a transfer of FT and NFT, and deleting the allowance using ERC standard calls.
Check out the following articles for more step-by-step instructions on: