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:
- Create a smart contract with a function that transfers tokens from the caller to the contract (itself).
- Deploy the smart contract on Hedera.
- Create a new Hedera account and create FT’s or use an existing account that already owns FTs. Ref: Hedera Account
- Associate the contract with the Hedera accounts FTs you plan to send to it.
- Use the JS SDK to grant an allowance to the contract of with a token amount of 3.
- Query the mirror node for allowances info from:
https://testnet.mirrornode.hedera.com/api/v1/accounts/${ownerAccountID}/allowances/tokens
and add a condition that checks ifamountGranted < tokenAmountToSpend
… if it is, callgrantAllowance()
. - Invoke the smart contract function
transfer
where the Hedera account is the caller and the transfer amount is 2. - This call succeeds.
- Invoke the smart contract function
transfer
where the Hedera account is the caller and the transfer amount is 2. - This call does not succeed: Contract Revert with
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}`);
}
- Network Environment: Hedera Testnet
- Hedera JS SDK Version: 2.29.0