Hello, I would like to ask for help here.
I want to create a non-transferable NFT, and these are 2 alternatives that I can think of. I am a noob in Solidity so I am not sure which one is better (or if there are other and better ways, please suggest).
Alternative 1. Using ERC721 and override _beforeTokenTransfer
The pros of this one are less work and not being prone to error, but it will have many unused functions ("transfer", "approve" related functions for example).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol";
contract MyContract is Initializable, ERC721Upgradeable, AccessControlUpgradeable, UUPSUpgradeable {
using CountersUpgradeable for CountersUpgradeable.Counter;
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
CountersUpgradeable.Counter private _tokenIdCounter;
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize() initializer public {
__ERC721_init("MyContract", "MTK");
__AccessControl_init();
__UUPSUpgradeable_init();
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
_grantRole(UPGRADER_ROLE, msg.sender);
}
function safeMint(address to) public onlyRole(MINTER_ROLE) {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
}
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId,
uint256 batchSize
) internal override(ERC721Upgradeable) {
require(from == address(0) || to == address(0), "Non transferable");
super._beforeTokenTransfer(from, to, tokenId, batchSize);
}
function _authorizeUpgrade(address newImplementation)
internal
onlyRole(UPGRADER_ROLE)
override
{}
// The following functions are overrides required by Solidity.
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721Upgradeable, AccessControlUpgradeable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
Alternative 2. Use Custom.
The pro is it will not have unused functions, but a lot of work and need to be very careful
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol";
contract MyContract is Initializable, AccessControlUpgradeable, UUPSUpgradeable {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
using CountersUpgradeable for CountersUpgradeable.Counter;
// Token Ids counter.
CountersUpgradeable.Counter private _tokenIdCounter;
using AddressUpgradeable for address;
using StringsUpgradeable for uint256;
// Token name
string private _name;
// Token symbol
string private _symbol;
// Mapping from token ID to owner address
mapping(uint256 => address) private _owners;
// Mapping owner address to token count
mapping(address => uint256) private _balances;
// Optional mapping for token URIs
mapping(uint256 => string) private _tokenURIs;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize() initializer public {
__AccessControl_init();
__UUPSUpgradeable_init();
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
_grantRole(UPGRADER_ROLE, msg.sender);
}
function safeMint(address to, string memory uri) public onlyRole(MINTER_ROLE) {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_mint(to, tokenId);
_setTokenURI(tokenId, uri);
}
function burn(uint256 tokenId) external {
require(msg.sender == _ownerOf(tokenId), "Not an owner");
_burn(tokenId);
}
function balanceOf(address owner) public view returns (uint256) {
require(owner != address(0), "ERC721: address zero is not a valid owner");
return _balances[owner];
}
function ownerOf(uint256 tokenId) public view returns (address) {
address owner = _ownerOf(tokenId);
require(owner != address(0), "ERC721: invalid token ID");
return _ownerOf(tokenId);
}
function name() external view returns (string memory) {
return _name;
}
function symbol() external view returns (string memory) {
return _symbol;
}
function tokenURI(
uint256 tokenId
) external view returns (string memory) {
_requireMinted(tokenId);
return _tokenURIs[tokenId];
}
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
// Check that tokenId was not minted by `_beforeTokenTransfer` hook
require(!_exists(tokenId), "ERC721: token already minted");
unchecked {
// Will not overflow unless all 2**256 token ids are minted to the same owner.
// Given that tokens are minted one by one, it is impossible in practice that
// this ever happens. Might change if we allow batch minting.
// The ERC fails to describe this case.
_balances[to] += 1;
}
_owners[tokenId] = to;
}
function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal {
_requireMinted(tokenId);
_tokenURIs[tokenId] = _tokenURI;
}
function _ownerOf(uint256 tokenId) internal view returns (address) {
return _owners[tokenId];
}
function _requireMinted(uint256 tokenId) internal view {
require(_exists(tokenId), "ERC721: invalid token ID");
}
function _exists(uint256 tokenId) internal view returns (bool) {
return _ownerOf(tokenId) != address(0);
}
function _burn(uint256 tokenId) internal {
unchecked {
// Cannot overflow, as that would require more tokens to be burned/transferred
// out than the owner initially received through minting and transferring in.
_balances[msg.sender] -= 1;
}
delete _owners[tokenId];
// Delete token uri
if (bytes(_tokenURIs[tokenId]).length != 0) {
delete _tokenURIs[tokenId];
}
}
function _authorizeUpgrade(address newImplementation)
internal
onlyRole(UPGRADER_ROLE)
override
{}
}
Thank you in advance.