HTS x EVM - Part 3: How to Pause, Freeze, Wipe, and Delete NFTs
In HTS x EVM - Part 2, you learned how to grant / revoke KYC and manage a token using the Hedera Token Service (HTS) System Smart Contract. But those aren't all the token operations you can do!
In this guide, you will learn how to:
Pause a token (stop all operations)
Freeze a token for a specific account
Wipe NFTs from a specific account
Delete a token
Prerequisites
ECDSA account from the Hedera Portal.
Basic understanding of Solidity.
Table of Contents
Step 1: Project Setup
Clone the repository
git clone https://github.com/hedera-dev/hts-evm-hybrid-mint-nfts.git
cd hts-evm-hybrid-mint-nfts
Install dependencies
npm install
Create
.env
file set up environment variables
cp .env.example .env
Edit the .env
file to include your Hedera Testnet account's private key. Use your ECDSA Hex Encoded Private Key when interacting with Hedera's EVM via the JSON-RPC relay.
PRIVATE_KEY=0x
Run the test script
npx hardhat test test/3-PauseFreezeWipeDelete.ts
This script deploys and tests all of the functionality inside of the PauseFreezeWipeDelete
Smart Contract. We'll deep dive into the Smart Contract's functions and corresponding tests below!
Step 2. Creating, minting, and transferring an NFT
These steps of the flow have been covered in the Part 1 and Part 2. The only difference here is that we set a few different keys to handle pausing, freezing, and wiping.
Check out those tutorials for specific details or the contracts/3-PauseFreezeWipeDelete.ts
to see the code for adding the new keys.
Step 3. Pause a Token
Function: pauseToken()
pauseToken()
Purpose: Stops all operations (e.g., mint, transfer, burn, etc.) for the token, across every account.
Key Code Snippet:
function pauseToken() external onlyOwner {
require(tokenAddress != address(0), "Token not created yet");
int response = pauseToken(tokenAddress);
require(response == HederaResponseCodes.SUCCESS, "Failed to pause token");
emit TokenPaused();
}
Test Implementation:
it("should pause the token", async () => {
const pauseTx = await nftContract.pauseToken({
gasLimit: 350_000
});
await expect(pauseTx)
.to.emit(nftContract, "TokenPaused");
// Try to transfer while paused - should fail
const serialNumber = 2n;
await expect(
nftContract.transferNFT(owner.address, serialNumber)
).to.be.reverted;
});
After pausing, we attempt a transfer. The test expects a revert (the transfer fails) because the token is paused.
Step 4. Unpause a Token
Function: unpauseToken()
unpauseToken()
Purpose: Reenables actions to be performed on and with the token
Key Code Snippet:
function unpauseToken() external onlyOwner {
require(tokenAddress != address(0), "Token not created yet");
int response = unpauseToken(tokenAddress);
require(response == HederaResponseCodes.SUCCESS, "Failed to unpause token");
emit TokenUnpaused();
}
Test Implementation:
it("should unpause the token", async () => {
const unpauseTx = await nftContract.unpauseToken({
gasLimit: 350_000
});
await expect(unpauseTx)
.to.emit(nftContract, "TokenUnpaused");
// Transfer should work now
const serialNumber = 2n;
const transferTx = await nftContract.transferNFT(owner.address, serialNumber);
await expect(transferTx)
.to.emit(nftContract, "NFTTransferred")
.withArgs(owner.address, serialNumber);
});
We confirm that normal token operations (e.g., transfers) resume after the unpause
.
Step 5. Freeze a Token for a Specific Account
Function: freezeAccount(address account)
freezeAccount(address account)
Purpose: Freezes a specific account, meaning it can neither send nor receive the token. Freezing is more granular than pausing; it only affects a specific account. This is useful for making soul-bound tokens.
Key Code Snippet:
function freezeAccount(address account) external onlyOwner {
require(tokenAddress != address(0), "Token not created yet");
int response = freezeToken(tokenAddress, account);
require(response == HederaResponseCodes.SUCCESS, "Failed to freeze account");
emit AccountFrozen(account);
}
Test Implementation:
it("should freeze owner", async () => {
const freezeTx = await nftContract.freezeAccount(owner.address, {
gasLimit: 350_000
});
await expect(freezeTx)
.to.emit(nftContract, "AccountFrozen")
.withArgs(owner.address);
// Try to transfer to frozen account - should fail
const serialNumber = 3n;
await expect(
nftContract.transferNFT(owner.address, serialNumber)
).to.be.reverted;
});
We freeze the
owner
account.The test ensures that transferring an NFT to or from this frozen account reverts.
Step 6. Unfreeze a Token for a Specific Account
Function: unfreezeAccount(address account)
unfreezeAccount(address account)
Purpose: Unfreezes the token for a specific account.
Key Code Snippet:
function unfreezeAccount(address account) external onlyOwner {
require(tokenAddress != address(0), "Token not created yet");
int response = unfreezeToken(tokenAddress, account);
require(response == HederaResponseCodes.SUCCESS, "Failed to unfreeze account");
emit AccountUnfrozen(account);
}
Test Implementation:
it("should unfreeze owner", async () => {
const unfreezeTx = await nftContract.unfreezeAccount(owner.address, {
gasLimit: 350_000
});
await expect(unfreezeTx)
.to.emit(nftContract, "AccountUnfrozen")
.withArgs(owner.address);
});
After unfreezing, the owner
account can transact freely again.
Step 7. Wipe a Token
Function: wipeTokenFromAccount(address account, int64[] serialNumbers)
wipeTokenFromAccount(address account, int64[] serialNumbers)
Purpose: Wiping effectively burns that token (i.e., reduces the total supply) from a non-treasury account.
For fungible tokens, specify an amount to wipe; for non-fungible tokens (NFTs), we specify the serial numbers to be wiped.
Key Code Snippet:
function wipeTokenFromAccount(
address account,
int64[] memory serialNumbers
) external onlyOwner {
require(tokenAddress != address(0), "Token not created yet");
int response = wipeTokenAccountNFT(tokenAddress, account, serialNumbers);
require(response == HederaResponseCodes.SUCCESS, "Failed to wipe token");
emit TokenWiped(account, serialNumbers);
}
Test Implementation:
it("should wipe NFT from owner", async () => {
const serialNumbers = [1n];
const wipeTx = await nftContract.wipeTokenFromAccount(
owner.address,
serialNumbers,
{
gasLimit: 350_000
}
);
await expect(wipeTx)
.to.emit(nftContract, "TokenWiped")
.withArgs(owner.address, serialNumbers);
});
We specify which serial numbers we want to wipe. The test confirms the TokenWiped
event is emitted upon success.
Step 8. Delete a Token
Function: deleteToken()
deleteToken()
Purpose: Renders a token completely unusable for future operations. The token still exists on the ledger (you can query it), but all transactions (e.g., minting, transfers, etc.) will fail. The ADMIN key is required to delete.
Key Code Snippet:
deleteToken() external onlyOwner {
require(tokenAddress != address(0), "Token not created yet");
int response = deleteToken(tokenAddress);
require(response == HederaResponseCodes.SUCCESS, "Failed to delete token");
emit TokenDeleted();
}
Test Implementation:
it("should delete the token", async () => {
const deleteTx = await nftContract.deleteToken({
gasLimit: 350_000
});
await expect(deleteTx)
.to.emit(nftContract, "TokenDeleted");
// Try to mint after deletion - should fail
const metadata = [ethers.toUtf8Bytes("ipfs://metadata3")];
await expect(
nftContract.mintNFT(metadata)
).to.be.reverted;
});
Once deleted, attempting further operations like minting will fail.
Conclusion
In this guide, you saw how to replicate key HTS operations (pause, freeze, wipe, delete) directly in a Solidity contract by calling the HTS System Contract functions on Hedera. This approach provides fine-grained control via the contract’s ownership and key management, which is especially useful if you need all relevant HTS functionality in a single deployable smart contract.
Key Takeaways:
If you want to perform the respective operations later, you must set the ADMIN, FREEZE, PAUSE, WIPE, and SUPPLY keys when creating a token via a contract.
Any account that needs to receive or send the token must be associated with it.
Pausing affects all operations globally while freezing targets a single account.
Wiping NFTs effectively burns them, reducing total supply.
Deleting a token makes it unusable for future operations but remains queryable on the ledger.
Additional Resources
Check out our GitHub repo to find the full contract and Hardhat test scripts, along with the configuration files you need to deploy and test on Hedera!
Last updated
Was this helpful?