// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title Private ERC721 Token for COTI Blockchain
* @notice This is a base template for COTI private ERC721 (NFT) tokens
* @dev Uses COTI's confidential types (ctString) for encrypted token URIs
*
* CUSTOMIZATION_MARKERS:
* - MAX_SUPPLY: Add maximum NFT supply cap
* - BURNABLE: Add burn functionality
* - PAUSABLE: Add pause/unpause capability
* - MINTABLE_RESTRICTIONS: Add minting restrictions (owner-only, time-based, etc.)
* - ACCESS_CONTROL: Add role-based access control
*/
contract PrivateERC721 {
string private _name;
string private _symbol;
// Token ownership and approvals
mapping(uint256 => address) private _owners;
mapping(address => uint256) private _balances;
mapping(uint256 => address) private _tokenApprovals;
mapping(address => mapping(address => bool)) private _operatorApprovals;
// Note: In actual COTI contracts, tokenURIs use ctString (confidential string)
// This is handled by COTI's compiler and runtime
mapping(uint256 => string) private _tokenURIs; // In practice, this is ctString
uint256 private _nextTokenId = 1;
address public owner;
// CUSTOMIZATION_MARKER:MAX_SUPPLY
// uint256 public constant MAX_SUPPLY = 10000;
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
event Mint(address indexed to, uint256 indexed tokenId);
// CUSTOMIZATION_MARKER:BURNABLE
// event Burn(uint256 indexed tokenId);
// CUSTOMIZATION_MARKER:PAUSABLE
// bool public paused = false;
// event Paused(address account);
// event Unpaused(address account);
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function");
_;
}
// CUSTOMIZATION_MARKER:PAUSABLE
// modifier whenNotPaused() {
// require(!paused, "Contract is paused");
// _;
// }
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
owner = msg.sender;
}
function name() public view returns (string memory) {
return _name;
}
function symbol() public view returns (string memory) {
return _symbol;
}
function balanceOf(address owner_) public view returns (uint256) {
require(owner_ != address(0), "Balance query for zero address");
return _balances[owner_];
}
function ownerOf(uint256 tokenId) public view returns (address) {
address tokenOwner = _owners[tokenId];
require(tokenOwner != address(0), "Token does not exist");
return tokenOwner;
}
function tokenURI(uint256 tokenId) public view returns (string memory) {
require(_owners[tokenId] != address(0), "Token does not exist");
return _tokenURIs[tokenId];
}
function approve(address to, uint256 tokenId) public {
// CUSTOMIZATION_MARKER:PAUSABLE
// require(!paused, "Contract is paused");
address tokenOwner = ownerOf(tokenId);
require(to != tokenOwner, "Approval to current owner");
require(msg.sender == tokenOwner || isApprovedForAll(tokenOwner, msg.sender),
"Not owner nor approved for all");
_tokenApprovals[tokenId] = to;
emit Approval(tokenOwner, to, tokenId);
}
function getApproved(uint256 tokenId) public view returns (address) {
require(_owners[tokenId] != address(0), "Token does not exist");
return _tokenApprovals[tokenId];
}
function setApprovalForAll(address operator, bool approved) public {
// CUSTOMIZATION_MARKER:PAUSABLE
// require(!paused, "Contract is paused");
require(operator != msg.sender, "Approve to caller");
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
function isApprovedForAll(address owner_, address operator) public view returns (bool) {
return _operatorApprovals[owner_][operator];
}
function transferFrom(address from, address to, uint256 tokenId) public {
// CUSTOMIZATION_MARKER:PAUSABLE
// require(!paused, "Contract is paused");
require(_isApprovedOrOwner(msg.sender, tokenId), "Not owner nor approved");
_transfer(from, to, tokenId);
}
function safeTransferFrom(address from, address to, uint256 tokenId) public {
// CUSTOMIZATION_MARKER:PAUSABLE
// require(!paused, "Contract is paused");
transferFrom(from, to, tokenId);
}
function mint(address to, string memory uri) public onlyOwner {
// CUSTOMIZATION_MARKER:PAUSABLE
// require(!paused, "Contract is paused");
// CUSTOMIZATION_MARKER:MAX_SUPPLY
// require(_nextTokenId <= MAX_SUPPLY, "Minting would exceed max supply");
require(to != address(0), "Cannot mint to zero address");
uint256 tokenId = _nextTokenId;
_nextTokenId++;
_balances[to] += 1;
_owners[tokenId] = to;
_tokenURIs[tokenId] = uri;
emit Mint(to, tokenId);
emit Transfer(address(0), to, tokenId);
}
// CUSTOMIZATION_MARKER:BURNABLE
// function burn(uint256 tokenId) public {
// require(!paused, "Contract is paused");
// require(_isApprovedOrOwner(msg.sender, tokenId), "Not owner nor approved");
//
// address owner_ = ownerOf(tokenId);
//
// _tokenApprovals[tokenId] = address(0);
// _balances[owner_] -= 1;
// delete _owners[tokenId];
// delete _tokenURIs[tokenId];
//
// emit Burn(tokenId);
// emit Transfer(owner_, address(0), tokenId);
// }
// CUSTOMIZATION_MARKER:PAUSABLE
// function pause() public onlyOwner {
// require(!paused, "Contract is already paused");
// paused = true;
// emit Paused(msg.sender);
// }
// function unpause() public onlyOwner {
// require(paused, "Contract is not paused");
// paused = false;
// emit Unpaused(msg.sender);
// }
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {
address tokenOwner = _owners[tokenId];
require(tokenOwner != address(0), "Token does not exist");
return (spender == tokenOwner ||
getApproved(tokenId) == spender ||
isApprovedForAll(tokenOwner, spender));
}
function _transfer(address from, address to, uint256 tokenId) internal {
require(ownerOf(tokenId) == from, "Transfer from incorrect owner");
require(to != address(0), "Transfer to zero address");
_tokenApprovals[tokenId] = address(0);
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
}
}