I have a DApp with a contract that has an approved allowance to spend HTS fungible tokens on behalf of a user. However, the contract keeps reverting with SPENDER_DOES_NOT_HAVE_ALLOWANCE
error.
I have a condition in my code to ask the DApp’s user to approve another allowance transaction if the current allowance is less than the required amount.
if (amountGranted < tokenAmountToSpend) {
// perform allowance tx
}
This seems like it should be sufficient. However, when I query the following endpoint of the mirror node:
/api/v1/account/{accountId}/allowances/tokens
the response’ allowances.amount_granted
value shows the original token allowance granted. But it does not show the updated token allowance. Is there a different endpoint I can call to get the updated (remaining) token allowance?
Steps to reproduce:
https://testnet.mirrornode.hedera.com/api/v1/accounts/${ownerAccountID}/allowances/tokens
and add a condition that checks if amountGranted < tokenAmountToSpend
… if it is, call grantAllowance()
.transfer
where the Hedera account is the caller and the transfer amount is 2.transfer
where the Hedera account is the caller and the transfer amount is 2.SPENDER_DOES_NOT_HAVE_ALLOWANCE
.Smart Contract:
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract TransferCallerTokens {
function transfer(address token, uint256 amount) public {
IERC20(token).transferFrom(msg.sender, address(this), amount);
}
}
Smart contract deployment:
async function deployContract() {
const bytecode = fs.readFileSync(
"TransferCallerTokens_sol_TransferCallerTokens.bin");
// Switch operators (for the client) before executing the transaction,
// as you need to sign using the contract's admin key
client.setOperator(adminAccountId, adminKey);
// Build the transaction
const contractCreate = new ContractCreateFlow()
.setGas(100_000)
.setBytecode(bytecode)
.setAdminKey(adminKey)
const contractCreateTxResponse = await contractCreate.execute(client);
const contractCreateReceipt = await contractCreateTxResponse.getReceipt(client);
const transferCallerTokensContractID = contractCreateReceipt.contractId;
console.log(`The transferCallerTokensContractID: ${transferCallerTokensContractID}`);
}
Using the SDK to create my fungible token. Note: The FT belongs to the user, and not the smart contract above.
// Create a HTS Fungible Token
async function createFungibleToken(
client,
treasuryAccountId,
treasuryAccountPrivateKey,
) {
// Generate supply key
const supplyKeyForFT = PrivateKey.generateED25519();
// Confgiure the token
const createFTokenTxn = await new TokenCreateTransaction()
.setTokenName('LoeweToken')
.setTokenSymbol('LO')
.setTokenType(TokenType.FungibleCommon)
.setDecimals(1)
.setInitialSupply(100)
.setTreasuryAccountId(treasuryAccountId)
.setSupplyKey(supplyKeyForFT)
.setMaxTransactionFee(new Hbar(30))
.freezeWith(client);
// Sign the transaction with the treasury account private key
const createFTokenTxnSigned = await
createFTokenTxn.sign(treasuryAccountPrivateKey);
const createFTokenTxnResponse = await
createFTokenTxnSigned.execute(client);
}
Granting an allowance
// Grant allowance to smart contract
async function grantAllowance() {
const tokenAmount = 2;
const approveAllowanceTx = new AccountAllowanceApproveTransaction()
.approveTokenAllowance(
fungibleTokenId, ownerAccountId, spenderAccountId, tokenAmount
)
.freezeWith(client);
const approveAllowanceTxSign = await approveAllowanceTx
.sign(
PrivateKey.fromString(process.env.MY_PRIVATE_KEY)
);
const approveAllowanceTxResponse = await approveAllowanceTxSign.execute(client);
await approveAllowanceTxResponse.getReceipt(client);
}
Execute the Contract function transfer
// execute transfer of HTS fungible token
async function executeTransferTransaction() {
const amount = 2;
const allowanceInfo = await getAllowance();
const contractAllowanceInfo = allowanceInfo.allowances.find(
(x) => (x.spender === '0.0.15079297' && x.token_id === '0.0.14073131')
);
const contractAmountGranted = contractAllowanceInfo.amount_granted
console.log(`Amount Granted for Contract is: ${contractAmountGranted}`)
if (contractAmountGranted < amount) {
await grantAllowance();
}
const transferFromTx = new ContractExecuteTransaction()
.setContractId(contractID)
.setFunction(
'transfer',
new ContractFunctionParameters()
.addAddress(tokenIDSolidityAddress)
.addUint256(amount),
)
.setGas(3_000_000)
.freezeWith(client);
const transferFromTxResponse = await transferFromTx.execute(client);
const receipt = await transferFromTxResponse.getReceipt(client);
console.log(`Execute Transfer on TransferCallerTokens status ${receipt.status}`);
}
When calling Mirror Node endpoint /api/v1/account/{accountId}/allowances/tokens
the response allowance.amount_granted
does not return the remaining allowance.
The engineering team is aware of and is working on closing that gap. In the meantime, the following workarounds are recommended:
Approach 1: Perform an allowance transaction every time the contract needs to spend on behalf of the dApp user.
This will request the dApp user an approval before executing the smart contract function.
//Create the transaction
const transaction = new AccountAllowanceApproveTransaction()
.approveTokenAllowance(tokenId, ownerAccount, spenderAccountId, tokenAmount);
The pro of approach 1 is you will not have contract reverts due to the fact the allowance has already been spent but the mirror node does not return the updated allowance.
The con is that it is possible that the user does have a sufficient remaining allowance but they are paying for network fee to complete an approval they didn’t need.
Approach 2: Execute a contract function directly to system contract for allowances which will return the the remaining number of tokens that spender
will be allowed to spend on behalf of owner
If you are using Hedera wallets use ContractExecuteTransaction
to call allowance
directly on the FT token.
/*
* Use the SDK to execute a contract function directly to system contract
* to get the remaining number of tokens that spender will be allowed to spend on behalf of owner:
*/
const allowanceTransaction = new ContractExecuteTransaction()
.setContractId(fungibleTokenId)
.setFunction('allowance', new ContractFunctionParameters()
.addAddress(ownerAccountIdSolidityAddress)
.addAddress(spenderAccountIdSolidityAddress))
.setGas(30_000)
.freezeWith(client);
If you are using using EVM wallets use eth_call
to execute the contract function allowance
directly on FT token
async getAllowance(tokenSolidityAddress) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = await provider.getSigner();
// create contract instance for the contract id (token id)
const contract = new ethers.Contract(tokenSolidityAddress, [`function allowance()`], signer);
try {
const txResult = await contract.allowance(ownerAddress, spenderAddress);
return txResult.hash;
} catch (error: any) {
console.warn(error.message ? error.message : error);
return null;
}
}
The con about the second approach is that a user may have to pay twice and sign two transactions: