After I finished my first dApp "Decentralized Bookstore", I have been searching for a more web3 native use case. Inspired by this news article, I decided to develop a POC dApp to learn and explore:
The POC dApp is called "Decentralized Antique Auction", allowing anyone to auction/bid for antiques using cryptocurrencies. An NFT is minted the first time when the antique is listed by its initial owner with a starting price, a reserve price, a royalty percentage and the auction end date/time. Once listed, others can place bids before the auction ends. When the auction ends,
An antique owner (Account 1) sells a Ceramic Guan Yin Statue from Qing Dynasty at the dApp.
The antique owner imports the minted NFT in the MetaMask wallet.
The first bidder (Account 2) bids at starting price.
The second bidder (Account 3) bids at the reserve price.
At the auction end date, the seller (Account 1) ends the auction, after which the antique is removed from the auction list.
From My Bids tab, the new antique owner (Account 3) can resell the newly acquired antique. Since the Antique NFT is already minted, its meta data (e.g. name, description, image and sale royalty) cannot be changed.
After auction ends, the failed bidder (Account 2) withdraws the bid amount held by smart contract.
Fungible means to be the same or interchangeable. For example, Ethereum tokens, all the members of a particular token class, have the same value. The same can be said of Cardano tokens. Fungible tokens are interchangeable 1:1.
With this in mind, NFTs are unique; each one is different. Every single token has unique characteristics and values. The types of things that can be NFTs are collectible cards, artworks, airplane tickets, etc. They are all clearly distinguishable from one another and are not interchangeable. Think of Non-Fungible Tokens (NFTs) as rare collectibles; each has unique characteristics, unusual attributes, and most times, its metadata.
ERC stands for Ethereum Request for Comment, and 721 is the proposal identifier number. ERCs are application-level standards in the Ethereum ecosystem, they can be a smart contract standard for tokens such as ERC-20, the author of an ERC is responsible for building consensus with the Ethereum community and once the proposal is reviewed and approved by the community it becomes a standard. You can track the recent ERC proposal here. ERC-721 was created to propose the functionality to track and transfer NFTs within smart contracts.
ERC-721 is an open standard that describes how to build Non-Fungible tokens on EVM (Ethereum Virtual Machine) compatible blockchains; it is a standard interface for Non-Fungible tokens; it has a set of rules which make it easy to work with NFTs. NFTs are not only of ERC-721 type; they can also be ERC-1155 tokens.
Digital collectibles compatible with the ERC-721 standard have become very popular since the launch of Cryptokitties and have moved forward towards mass adoption in recent years. Here is a brief history of the ERC-721 standard and NFT:
Before creating our NFT, we need to host our art for NFT and create a metadata file; for this, we’ll use IPFS - a peer-to-peer file storing and sharing distributed system. This happens at screenshot “Sell Antique - 3 of 6” after Submit button is clicked, enabled by the following frontend code:
import createNFTMetaDataURIViaPinata from '../libs/createNFTMetaDataURI'
...
const nftData = {
receiverAddress: this.userData.address,
metaData: {
name: this.name,
description: this.description,
image: this.file,
attributes: [
{
trait_type: 'creator',
value: this.userData.address,
},
{
trait_type: 'royalty',
value: this.royalty + '%',
},
{
trait_type: 'createdAt',
value: new Date().toUTCString(),
},
],
},
}
console.log(`nftData: ${JSON.stringify(nftData)}`)
return new Promise((resolve, reject) => {
createNFTMetaDataURIViaPinata(nftData, (result) => {
resolve(result)
}).catch((error) => {
console.error(error)
reject(error)
})
})
import axios from 'axios'
import pinataSDK from '@pinata/sdk'
const PINATA_API_KEY = import.meta.env.VITE_PINATA_API_KEY
const PINATA_SECRET_API_KEY = import.meta.env.VITE_PINATA_SECRET_API_KEY
const pinata = pinataSDK(PINATA_API_KEY, PINATA_SECRET_API_KEY)
const createNFTMetaDataURI = async function (nftData, callback) {
const nftMetaData = nftData.metaData
console.log(nftMetaData)
// convert file into binary
const data = new FormData()
data.append('title', nftMetaData.image.name)
data.append('file', nftMetaData.image)
const url = 'https://api.pinata.cloud/pinning/pinFileToIPFS'
// pass binary data into post request
const result = await axios.post(url, data, {
maxContentLength: -1,
headers: {
'Content-Type': `multipart/form-data; boundary=${data._boundary}`,
pinata_api_key: PINATA_API_KEY,
pinata_secret_api_key: PINATA_SECRET_API_KEY,
path: 'd-antique-auction',
},
})
nftMetaData.image = `ipfs://${result.data.IpfsHash}`
//Call Pinata to get NFT metadata uri
const options = {
pinataMetadata: {
name: nftMetaData.name,
},
pinataOptions: {
cidVersion: 0,
},
}
pinata
.pinJSONToIPFS(nftMetaData, options)
.then((result) => {
//handle results here
console.log(`pinJSONToIPFS:\n${JSON.stringify(result, 0, 2)}`)
const uri = 'ipfs://' + result.IpfsHash
callback(uri)
})
.catch((err) => {
//handle error here
console.error(err)
throw err
})
}
export default createNFTMetaDataURI
Once we obtained tokenMetaDataURI, we can mint Antique NFT easily, thanks to OpenZeppelin/openzeppelin-contracts library.
import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import '@openzeppelin/contracts/utils/Counters.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol';
contract AntiqueNFT is
ERC721URIStorage,
Ownable
{
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
constructor() ERC721('My Antiques', 'ANTIQUE-NFT') {
}
function mintNFT(address recipient, string memory tokenURI)
public
override
returns (uint256)
{
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(recipient, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
}
Finalized on July 24, 2021, ERC-2981 is a standard that focuses on signalling to market participants the royalty payment information for a given NFT or set of NFTs. It allows for attaching royalty information to the token itself, thereby making use of a common data store and computation layer that all participants can access — the Ethereum blockchain.
It’s pretty simple. The crux of the problem can be solved by overriding a single Solidity function in your smart contract.
interface IERC2981 {
/// @notice Called with the sale price to determine how much royalty is owed and to whom.
/// @param _tokenId - the NFT asset queried for royalty information
/// @param _salePrice - the sale price of the NFT asset specified by _tokenId
/// @return receiver - address of who should be sent the royalty payment
/// @return royaltyAmount - the royalty payment amount for _salePrice
function royaltyInfo(
uint256 _tokenId,
uint256 _salePrice
) external view returns (
address receiver,
uint256 royaltyAmount
);
}
In this POC I followed this example implementation and implemented Antique NFT royaltyies on a per token basis.
contract AntiqueNFT is
ERC721URIStorage,
Ownable,
ERC2981PerTokenRoyalties
{
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
address public marketContract;
constructor(address _marketContract) ERC721('My Antiques', 'ANTIQUE-NFT') {
}
/// @inheritdoc ERC165
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(IERC165, ERC721, ERC2981Base)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
/// @notice Mint one token to `to`
/// @param to the recipient of the token
/// @param royaltyRecipient the recipient for royalties (if royaltyValue > 0)
/// @param royaltyValue the royalties asked for (EIP2981)
function mintNFTWithRoyalty(
address to,
address royaltyRecipient,
uint256 royaltyValue,
string memory tokenURI
) public override returns (uint256) {
uint256 tokenId = mintNFT(to, tokenURI);
if (royaltyValue > 0) {
_setTokenRoyalty(tokenId, royaltyRecipient, royaltyValue);
}
return tokenId;
}
}
Unlike artworks, usually there are no real authors or IP owners for antiques. In the POC, whenever an auction successfully ends, the royalty is received by the contract owner, i.e., the account used to deploy the smart contracts.
function sellAntique(
string memory tokenURI,
uint256 startingPrice,
uint256 reservePrice,
uint256 royaltyValue,
uint256 auctionEndTime
) public returns (bool success) {
AnyNFT anyNFT = AnyNFT(nftContract);
uint256 tokenId = anyNFT.mintNFTWithRoyalty(
msg.sender,
contractOwner,
royaltyValue,
tokenURI
);
return
_newAntique(
tokenId,
tokenURI,
startingPrice,
reservePrice,
auctionEndTime
);
}
contract AntiqueNFT is
ERC721URIStorage,
Ownable,
ERC2981PerTokenRoyalties
{
...
/// @inheritdoc ERC165
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(IERC165, ERC721, ERC2981Base)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
function mintNFT(address recipient, string memory tokenURI)
public
override
returns (uint256)
{
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(recipient, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
/// @notice Mint one token to `to`
/// @param to the recipient of the token
/// @param royaltyRecipient the recipient for royalties (if royaltyValue > 0)
/// @param royaltyValue the royalties asked for (EIP2981)
function mintNFTWithRoyalty(
address to,
address royaltyRecipient,
uint256 royaltyValue,
string memory tokenURI
) public override returns (uint256) {
uint256 tokenId = mintNFT(to, tokenURI);
if (royaltyValue > 0) {
_setTokenRoyalty(tokenId, royaltyRecipient, royaltyValue);
}
return tokenId;
}
...
}
In real scenarios, before being listed in AntiqueMarketplace, an antique should have been authenticated by an expert or organization which might be the right royalty receiver. The antique authentication info should be embedded in the token metadata for anyone to verify its authenticity.
As EVM Smart Contract has a size limit of 24K, the backend logic has to be split to two smart contracts: AntiqueMarketplace and AntiqueNFT. To check smart contract sizes, run:
yarn size
or
$ truffle run contract-size --checkMaxSize
AntiqueMarketplace and AntiqueNFT have to work closely to properly support the business logic. I use Dependency Injection design pattern to correctly set up both contracts at the deployment time.
AnyNFT.sol:
import '@openzeppelin/contracts/token/ERC721/IERC721.sol';
import './IERC2981Royalties.sol';
abstract contract AnyNFT is IERC721, IERC2981Royalties {
function mintNFT(address recipient, string memory tokenURI)
public
virtual
returns (uint256);
function mintNFTWithRoyalty(
address to,
address royaltyRecipient,
uint256 royaltyValue,
string memory tokenURI
) public virtual returns (uint256);
}
abstract contract AnyNFTMarket {
function NFTContractDeployed(address _nftContract)
public
virtual
returns (bool);
function endAntiqueAuction(uint256 _antiqueId)
public
virtual
returns (
address,
address,
uint256,
address,
uint256
);
}
AntiqueNFT.sol:
contract AntiqueNFT is
AnyNFT,
ERC721URIStorage,
Ownable,
ERC2981PerTokenRoyalties
{
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
address public marketContract;
constructor(address _marketContract) ERC721('My Antiques', 'ANTIQUE-NFT') {
marketContract = _marketContract;
AnyNFTMarket market = AnyNFTMarket(marketContract);
market.NFTContractDeployed(address(this));
}
...
}
AntiqueMarketplace.sol:
contract AntiqueMarketplace is AnyNFTMarket {
address public nftContract;
function NFTContractDeployed(address _nftContract)
public
override
returns (bool)
{
nftContract = _nftContract;
return true;
}
...
}
2_deploy_contracts.js:
var AntiqueNFT = artifacts.require('./AntiqueNFT.sol')
var AntiqueMarketplaceContract = artifacts.require('./AntiqueMarketplace.sol')
module.exports = function (deployer) {
deployer.deploy(AntiqueMarketplaceContract).then(function () {
return deployer.deploy(AntiqueNFT, AntiqueMarketplaceContract.address)
})
}
GitHub repo for both Solidity-based Smart contracts and Vue3-based SPA:
dApp deployed at Netlify:
dApp deployed at IPFS:
https://dweb.link/ipfs/Qmdf1ZftbHnjZDef2oBbTvkEVeQjiCo1H9CPthQbqZMAtv/
The very first My Antiques NFT at OpenSea testnet:
https://testnets.opensea.io/assets/mumbai/0x3fb44bfa72591ff4be8bf048384b17c2fdaf9622/1
Hope you have enjoyed reading and will find this POC useful. If you have any question/comment on the code, feel free to reach out to me via twitter: @inflaton_sg. Have a good one!