How to Deploy and Interact with an ERC-721 Smart Contract on Abstract Using Hardhat and OpenZeppelin

In this article, we will cover how to compile, deploy, and interact with an ERC-721 smart contract built using Solidity and the OpenZeppelin library on the Abstract Sepolia testnet. We will be using Hardhat, zkSync-Ethers, and TypeScript to create out project.
What is Abstract?
Abstract is a Layer 2 (L2) network built on top of Ethereum using the ZK Stack, a modular framework for creating ZK-powered blockchains. Like any L2, its goal is to scale Ethereum’s throughput while significantly reducing gas fees.
Abstract is EVM-compatible, supporting Solidity smart contracts and popular Ethereum development frameworks like Hardhat and Foundry. It also works seamlessly with widely used libraries such as Viem and Ethers.js.
Most existing Ethereum smart contracts will function on Abstract with minimal modifications, making it easy for developers to port applications with little friction. However Abstract smart contracts use different bytecode than the Ethereum Virtual Machine (EVM).
To learn more about Abstract check out this.
What is an ERC-721?
ERC-721 is an Ethereum token standard for non-fungible tokens (NFTs), meaning each token is unique and cannot be replaced. It enables ownership, transfer, and metadata management for digital assets like art, collectibles, and in-game items.
If are new to ERC-721 smart contracts check out these two articles:
- NFT Mastery: Understanding ERC-721 on Ethereum for Starters
- How to Create ERC-721 NFTs on Ethereum with OpenZeppelin: A Step-by-Step Tutorial
Hardhat Project Set Up
Prerequisites
- WSL2 (recommended for Windows users)
- Node.js 22
To set Node 22 in your machine you can use nvm:
nvm install 22
nvm use 22
nvm alias default 22 #global
Create a new directory and navigate into it:
mkdir abstract_nft && cd abstract_nft
Create a new Hardhat project:
npx hardhat init
Recommended setup:
✔ What do you want to do? · Create a TypeScript project
✔ Hardhat project root: · /path/to/my-abstract-project
✔ Do you want to add a .gitignore? (Y/n) · y
✔ Do you ... install ... dependencies with npm ... · y
Install the required dependencies to compile, deploy and interact with smart contracts on Abstract:
npm install -D @matterlabs/hardhat-zksync zksync-ethers@6 ethers@6
Install also dotenv to handle the environment variables:
npm install -D dotenv
Create a .env file in the root directory to store your account’s private key. You can use any Ethereum-compatible wallet, such as MetaMask or Coinbase. Write:
PRIVATE_KEY=your_private_key
Now modify the file hardhat.config.ts :
import { HardhatUserConfig } from "hardhat/config";
import "@matterlabs/hardhat-zksync";
import 'dotenv/config';
const config: HardhatUserConfig = {
zksolc: {
version: "latest",
settings: {
// Note: This must be true to call NonceHolder & ContractDeployer system contracts
enableEraVMExtensions: false,
},
},
defaultNetwork: "abstractTestnet",
networks: {
abstractTestnet: {
url: "https://api.testnet.abs.xyz",
ethNetwork: "sepolia",
zksync: true,
chainId: 11124,
accounts: [process.env.PRIVATE_KEY!]
},
abstractMainnet: {
url: "https://api.mainnet.abs.xyz",
ethNetwork: "mainnet",
zksync: true,
chainId: 2741,
accounts: [process.env.PRIVATE_KEY!]
},
},
etherscan: {
apiKey: {
abstractTestnet: "TACK2D1RGYX9U7MC31SZWWQ7FCWRYQ96AD",
abstractMainnet: "IEYKU3EEM5XCD76N7Y7HF9HG7M9ARZ2H4A",
},
customChains: [
{
network: "abstractTestnet",
chainId: 11124,
urls: {
apiURL: "https://api-sepolia.abscan.org/api",
browserURL: "https://sepolia.abscan.org/",
},
},
{
network: "abstractMainnet",
chainId: 2741,
urls: {
apiURL: "https://api.abscan.org/api",
browserURL: "https://abscan.org/",
},
},
],
},
solidity: {
version: "0.8.24",
},
};
export default config;
Let’s install the OpenZeppelin contracts:
npm install -D @openzeppelin/contracts
This is the ERC 721 smart contract that I have put in the filecontracts/CarsNft.sol :
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.24;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract CarsNft is ERC721, ERC721Enumerable, ERC721URIStorage {
uint256 private _nextTokenId;
constructor()
ERC721("CarsNft", "CARS")
{}
function safeMint(address to, string memory uri) public {
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}
// The following functions are overrides required by Solidity.
function _update(address to, uint256 tokenId, address auth)
internal
override(ERC721, ERC721Enumerable)
returns (address)
{
return super._update(to, tokenId, auth);
}
function _increaseBalance(address account, uint128 value)
internal
override(ERC721, ERC721Enumerable)
{
super._increaseBalance(account, value);
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
As you can see it is a very simple ERC 721 created with the OpenZeppelin Wizard.
Becore compiling it, let’s clear any existing artifacts:
npx hardhat clean
Now we can compile our smart contract with zksolc compiler:
npx hardhat compile --network abstractTestnet
Before deploying our smart contracts, you need some test Ether in your wallet. You can either get Sepolia ETH from a faucet and bridge it using the Arbitrum bridge, or use an Arbitrum faucet to obtain Arbitrum test Ether directly.
Here you can find the Arbitrum faucets page.
Here you can find the testnet Arbitrum bridge.
Before doing that make sure to add the Arbitrum testnet to your wallet by using the informations provided at this page.
In order to deploy our smart contract we need to create a deployment script at /deploy/deploy.ts:
import { Wallet } from "zksync-ethers";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { Deployer } from "@matterlabs/hardhat-zksync";
import "dotenv/config";
// An example of a deploy script that will deploy and call a simple contract.
export default async function (hre: HardhatRuntimeEnvironment) {
console.log(`Running deploy script`);
// Initialize the wallet using your private key.
const wallet = new Wallet(process.env.PRIVATE_KEY!);
// Create deployer object and load the artifact of the contract we want to deploy.
const deployer = new Deployer(hre, wallet);
// Load contract
const artifact = await deployer.loadArtifact("CarsNft");
// Deploy this contract. The returned object will be of a `Contract` type,
// similar to the ones in `ethers`.
const tokenContract = await deployer.deploy(artifact);
console.log(
`${
artifact.contractName
} was deployed to ${await tokenContract.getAddress()}`
);
}
To execute this file you can run:
npx hardhat deploy-zksync --script deploy.ts --network abstractTestnet
It will log something like:
Testnet
Running deploy script
CarsNft was deployed to 0x32d5014AF5387002331d2AaC10fA29a6B5f3E943
And you can check the transaction on Arbitrum scan.

To deploy the code we are using zksync-ethers and you can learn more about it here.
We can also verify our smart contract on the block explorer by running:
npx hardhat verify --network abstractTestnet 0x32d5014AF5387002331d2AaC10fA29a6B5f3E943
You can check the verified code of the previous smart contract here.
To interact with the smart contract I have created the file scripts/interact.ts:
import hre from "hardhat";
const CONTRACT_ADDRESS = "0x32d5014AF5387002331d2AaC10fA29a6B5f3E943";
const MY_ADDRESS = "0x20c6F9006d563240031A1388f4f25726029a6368";
if (!CONTRACT_ADDRESS)
throw "⛔️ Provide address of the contract to interact with!";
async function main() {
console.log(`Running script to interact with contract ${CONTRACT_ADDRESS}`);
// Get the first signer
const [signer] = await hre.ethers.getSigners();
// Get the contract factory and deploy
const CarsNft = await hre.ethers.getContractFactory("CarsNft");
const carsNft = await CarsNft.connect(signer).attach(CONTRACT_ADDRESS);
// Run contract read function name
const name = await carsNft.name();
console.log(`Current name is: ${name}`);
// Run contract write function safeMint
const transaction = await carsNft.safeMint(MY_ADDRESS, "URI of car n.1");
console.log(`Transaction hash of safeMint : ${transaction.hash}`);
await transaction.wait();
// Run contract read function balanceOf
const balance = await carsNft.balanceOf(MY_ADDRESS);
console.log(`Balance of the address ${MY_ADDRESS} is: ${balance.toString()}`);
// Run contract read function tokenURI
const tokenURI = await carsNft.tokenURI(0);
console.log(`Token URI of token 0 is: ${tokenURI}`);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
To run the previous script you can execute:
npx hardhat run scripts/interact.ts
This should be the result:
Running script to interact with contract 0x32d5014AF5387002331d2AaC10fA29a6B5f3E943
Current name is: CarsNft
Transaction hash of safeMint : 0x766938205ed335c769124d99ae917f577b3c259815a629b246589e08ded7eb83
Balance of the address 0x20c6F9006d563240031A1388f4f25726029a6368 is: 1
Token URI of token 0 is: URI of car n.1
In this two pages you can learn more:
- https://docs.zksync.io/zksync-era/tooling/hardhat/guides/getting-started
- https://docs.abs.xyz/build-on-abstract/smart-contracts/hardhat#recommended-hardhat-setup
Github Repository
The Github repository of this project can be found here.
Sources
- https://docs.zksync.io/zk-stack
- https://docs.abs.xyz/what-is-abstract
- https://docs.abs.xyz/build-on-abstract/smart-contracts/hardhat#recommended-hardhat-setup
- https://docs.zksync.io/zksync-era/tooling/hardhat/guides/getting-started
How to Deploy and Interact with an ERC-721 Smart Contract on Abstract Using Hardhat and… was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.