How to Create an NFT Contract with Oppenzeppelin
Contents
ERC721
-
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import { ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract Wave is ERC721 { constructor() ERC721("Wave", "WV") {} function mint(address to, uint256 tokenId) public { _safeMint(to, tokenId); } }
Access
Owner
-
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import { ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract Wave is ERC721, Ownable { constructor() ERC721("Wave", "WV") Ownable(msg.sender) {} function mint(address to, uint256 tokenId) public onlyOwner { _safeMint(to, tokenId); } }
Minter
-
Add
/interfaces/IMinterManager.sol// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IMinterManager { function isMinter(address account) external view returns (bool); function addMinter(address account) external; function removeMinter(address account) external; } -
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import { ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IMinterManager} from "./interfaces/IMinterManager.sol"; contract Wave is ERC721, Ownable, IMinterManager{ mapping (address => bool) private _minters; constructor() ERC721("Wave", "WV") Ownable(msg.sender) {} modifier onlyMinter(){ require(_minters[msg.sender], "Not minter"); _; } function mint(address to, uint256 tokenId) public onlyMinter { _safeMint(to, tokenId); } function isMinter(address account) external view override returns (bool){ return _minters[account]; } function addMinter(address account) external onlyOwner override { _minters[account] = true; } function removeMinter(address account) external onlyOwner override { delete _minters[account]; } }
Pausable
-
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import { ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IMinterManager} from "./interfaces/IMinterManager.sol"; import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; contract Wave is ERC721, Ownable, IMinterManager, Pausable{ mapping (address => bool) private _minters; constructor() ERC721("Wave", "WV") Ownable(msg.sender) {} modifier onlyMinter(){ require(_minters[msg.sender], "Not minter"); _; } function mint(address to, uint256 tokenId) public whenNotPaused onlyMinter { _safeMint(to, tokenId); } function isMinter(address account) external view returns (bool){ return _minters[account]; } function addMinter(address account) external whenNotPaused onlyOwner override { _minters[account] = true; } function removeMinter(address account) external whenNotPaused onlyOwner override { delete _minters[account]; } function pause() public onlyOwner { _pause(); } function unpause() public onlyOwner { _unpause(); } }
Lockable
-
Add
/interfaces/ILockable.sol// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface ILockable { function lockTokens(uint256[] calldata tokenIds,uint256 lockTime) external; function unlockTokens(uint256[] calldata tokenIds) external; function isTokenLocked(uint256 tokenId) external view returns (bool); } -
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IMinterManager} from "./interfaces/IMinterManager.sol"; import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; import {ILockable} from "./interfaces/ILockable.sol"; contract Wave is ERC721, Ownable, IMinterManager, Pausable, ILockable { mapping(address => bool) private _minters; mapping(uint256 => bool) private _lockedTokens; mapping(uint256 => uint256) private _lockTime; constructor() ERC721("Wave", "WV") Ownable(msg.sender) {} modifier onlyMinter() { require(_minters[msg.sender], "Not minter"); _; } function mint(address to, uint256 tokenId) public whenNotPaused onlyMinter { _safeMint(to, tokenId); } function isMinter(address account) public view returns (bool) { return _minters[account]; } function addMinter(address account) external override whenNotPaused onlyOwner { _minters[account] = true; } function removeMinter(address account) external override whenNotPaused onlyOwner{ delete _minters[account]; } function pause() public onlyOwner { _pause(); } function unpause() public onlyOwner { _unpause(); } function lockTokens(uint256[] calldata tokenIds, uint256 lockTime)external override whenNotPaused{ require(lockTime > 0, "Lock time must be greater than 0"); // change isMinter to public if (isMinter(msg.sender)) { for (uint256 i = 0; i < tokenIds.length; i++) { _requireOwned(tokenIds[i]); _lockedTokens[tokenIds[i]] = true; _lockTime[tokenIds[i]] = block.timestamp + lockTime; } return; } for (uint256 i = 0; i < tokenIds.length; i++) { uint256 tokenId = tokenIds[i]; require( ownerOf(tokenId) == msg.sender, "Only the owner or the minter can modify the lock status" ); _lockedTokens[tokenId] = true; _lockTime[tokenId] = block.timestamp + lockTime; } } function unlockTokens(uint256[] calldata tokenIds)external override whenNotPaused{ if (isMinter(msg.sender)) { for (uint256 i = 0; i < tokenIds.length; i++) { _requireOwned(tokenIds[i]); delete _lockedTokens[tokenIds[i]]; delete _lockTime[tokenIds[i]]; } return; } for (uint256 i = 0; i < tokenIds.length; i++) { uint256 tokenId = tokenIds[i]; require( ownerOf(tokenId) == msg.sender, "Only the owner or the minter can modify the lock status" ); delete _lockedTokens[tokenIds[i]]; delete _lockTime[tokenIds[i]]; } } function isTokenLocked(uint256 tokenId) public view override returns (bool) { _requireOwned(tokenId); return _lockedTokens[tokenId]; } }
Burnable
-
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IMinterManager} from "./interfaces/IMinterManager.sol"; import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; import {ILockable} from "./interfaces/ILockable.sol"; import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol"; contract Wave is ERC721, Ownable, IMinterManager, Pausable, ILockable, ERC721Burnable { mapping(address => bool) private _minters; mapping(uint256 => bool) private _lockedTokens; mapping(uint256 => uint256) private _lockTime; constructor() ERC721("Wave", "WV") Ownable(msg.sender) {} modifier onlyMinter() { require(_minters[msg.sender], "Not minter"); _; } function mint(address to, uint256 tokenId) public whenNotPaused onlyMinter { _safeMint(to, tokenId); } function isMinter(address account) public view returns (bool) { return _minters[account]; } function addMinter(address account) external override whenNotPaused onlyOwner { _minters[account] = true; } function removeMinter(address account) external override whenNotPaused onlyOwner{ delete _minters[account]; } function pause() public onlyOwner { _pause(); } function unpause() public onlyOwner { _unpause(); } function lockTokens(uint256[] calldata tokenIds, uint256 lockTime)external override whenNotPaused{ require(lockTime > 0, "Lock time must be greater than 0"); // change isMinter to public if (isMinter(msg.sender)) { for (uint256 i = 0; i < tokenIds.length; i++) { _requireOwned(tokenIds[i]); _lockedTokens[tokenIds[i]] = true; _lockTime[tokenIds[i]] = block.timestamp + lockTime; } return; } for (uint256 i = 0; i < tokenIds.length; i++) { uint256 tokenId = tokenIds[i]; require(ownerOf(tokenId) == msg.sender, "Not owner or minter"); _lockedTokens[tokenId] = true; _lockTime[tokenId] = block.timestamp + lockTime; } } function unlockTokens(uint256[] calldata tokenIds)external override whenNotPaused{ if (isMinter(msg.sender)) { for (uint256 i = 0; i < tokenIds.length; i++) { _requireOwned(tokenIds[i]); delete _lockedTokens[tokenIds[i]]; delete _lockTime[tokenIds[i]]; } return; } for (uint256 i = 0; i < tokenIds.length; i++) { uint256 tokenId = tokenIds[i]; require(ownerOf(tokenId) == msg.sender, "Not owner or minter"); delete _lockedTokens[tokenIds[i]]; delete _lockTime[tokenIds[i]]; } } function isTokenLocked(uint256 tokenId) public view override returns (bool) { _requireOwned(tokenId); return _lockedTokens[tokenId]; } function burn(uint256 tokenId) public override whenNotPaused { require(ownerOf(tokenId) == msg.sender,"Not owner"); super.burn(tokenId); } }
Meta Transactions
Forwarder Contract
- See the forwarder contract on sepolia
- See the target contract on sepolia
- Let’s say a user wants to call
logSenders()of the target contract - The user generates the signature
- The platform helps forward the call to the target contract by the forwarder contract
- The platform calls
execute()of the forwarder contract and pays the gas -
import { createPublicClient, encodeFunctionData, hashTypedData, http, parseAbiItem, parseAbiParameters, parseSignature, verifyTypedData, } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { sepolia } from 'viem/chains'; async function main() { // generate a random private key as the user const user = privateKeyToAccount('0xfc96a8fd9299be969720c9c26ed7e52df8d2eea51e7c642f10ebac31ad194935'); const targetContract: `0x${string}` = '0x44016C1c7636E3406fa2C70Cf0AF93617b74F99d'; const forwarderContract: `0x${string}` = '0xf43a501ec56D6228eF5008Fbce263A9197CEDC64'; const publicClient = createPublicClient({ chain: sepolia, transport: http('https://sepolia.infura.io/v3/{API_KEY}'), }); const { domain } = await publicClient.getEip712Domain({ address: forwarderContract }); // don't include salt otherwise the ERC2771Forwarder contract will fail to veriry delete domain.salt; const types = { ForwardRequest: parseAbiParameters( 'address from,address to,uint256 value,uint256 gas,uint256 nonce,uint48 deadline,bytes data' ), }; const data = encodeFunctionData({ abi: [parseAbiItem('function logSenders()')], args: [], }); const nonce = await publicClient.getTransactionCount({ address: user.address }); const request = { from: user.address, to: targetContract, value: BigInt(0), gas: BigInt(300000), nonce: BigInt(nonce), deadline: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, // 7 days data: data, }; const signature = await user.signTypedData({ domain, types, primaryType: 'ForwardRequest', message: request }); console.log({ signature, request }); } main();
The Real Msg Sender
- See the transaction log
msg.senderreturns 0xf43a501ec56D6228eF5008Fbce263A9197CEDC64 which is the forwarder contract_msgSender()returns 0xaECbc534702E2eE2c6fD225c715091ecA00aceCA which is the user
- How does
_msgSender()return the user’s address?- the forwarder contract pads the user’s address to the end of the
msg.data// _execute() of ERC2771Forwarder.sol bytes memory data = abi.encodePacked(request.data, request.from); - the target contract parses the user’s address from the
msg.data// _msgSender() of ERC2771Context.sol function _msgSender() internal view virtual override returns (address) { uint256 calldataLength = msg.data.length; uint256 contextSuffixLength = _contextSuffixLength(); if (isTrustedForwarder(msg.sender) && calldataLength >= contextSuffixLength) { return address(bytes20(msg.data[calldataLength - contextSuffixLength:])); } else { return super._msgSender(); } }
- the forwarder contract pads the user’s address to the end of the
Enable Forwarder
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IMinterManager} from "./interfaces/IMinterManager.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {ILockable} from "./interfaces/ILockable.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
contract Wave is ERC721, Ownable, IMinterManager, Pausable, ILockable, ERC721Burnable, ERC2771Context{
mapping(address => bool) private _minters;
mapping(uint256 => bool) private _lockedTokens;
mapping(uint256 => uint256) private _lockTime;
modifier onlyMinter() {
require(_minters[_msgSender()], "Not minter");
_;
}
constructor(address trustedForwarder) ERC721("Wave", "WV") Ownable(_msgSender()) ERC2771Context(trustedForwarder){}
function mint(address to, uint256 tokenId) public whenNotPaused onlyMinter {
_safeMint(to, tokenId);
}
function isMinter(address account) public view returns (bool) {
return _minters[account];
}
function addMinter(address account) external override whenNotPaused onlyOwner {
_minters[account] = true;
}
function removeMinter(address account) external override whenNotPaused onlyOwner{
delete _minters[account];
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
function lockTokens(uint256[] calldata tokenIds, uint256 lockTime)external override whenNotPaused{
require(lockTime > 0, "Lock time must be greater than 0");
// change isMinter to public
if (isMinter(_msgSender())) {
for (uint256 i = 0; i < tokenIds.length; i++) {
_requireOwned(tokenIds[i]);
_lockedTokens[tokenIds[i]] = true;
_lockTime[tokenIds[i]] = block.timestamp + lockTime;
}
return;
}
for (uint256 i = 0; i < tokenIds.length; i++) {
uint256 tokenId = tokenIds[i];
require(ownerOf(tokenId) == _msgSender(), "Not owner or minter");
_lockedTokens[tokenId] = true;
_lockTime[tokenId] = block.timestamp + lockTime;
}
}
function unlockTokens(uint256[] calldata tokenIds)external override whenNotPaused{
if (isMinter(_msgSender())) {
for (uint256 i = 0; i < tokenIds.length; i++) {
_requireOwned(tokenIds[i]);
delete _lockedTokens[tokenIds[i]];
delete _lockTime[tokenIds[i]];
}
return;
}
for (uint256 i = 0; i < tokenIds.length; i++) {
uint256 tokenId = tokenIds[i];
require(ownerOf(tokenId) == _msgSender(), "Not owner or minter");
delete _lockedTokens[tokenIds[i]];
delete _lockTime[tokenIds[i]];
}
}
function isTokenLocked(uint256 tokenId) public view override returns (bool) {
_requireOwned(tokenId);
return _lockedTokens[tokenId];
}
function burn(uint256 tokenId) public override whenNotPaused {
require(ownerOf(tokenId) == _msgSender(),"Not owner");
super.burn(tokenId);
}
function _msgSender()
internal
view
virtual
override(ERC2771Context, Context)
returns (address sender)
{
return super._msgSender();
}
function _msgData()
internal
view
virtual
override(ERC2771Context, Context)
returns (bytes calldata)
{
return super._msgData();
}
function _contextSuffixLength()
internal
view
virtual
override(ERC2771Context, Context)
returns (uint256)
{
return super._contextSuffixLength();
}
}
Upgradable
Initializable
- No constructors can be used in upgradeable contracts
- the codes in the constructor of logic contract are executed only once when deployed
- so the codes won’t be executed again when the proxy contract links to this logic contract
- The
initialize()function is used to replace the constructor function and can be called only once -
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IMinterManager} from "./interfaces/IMinterManager.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {ILockable} from "./interfaces/ILockable.sol"; import {ERC721BurnableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol"; import {ERC2771ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/metatx/ERC2771ContextUpgradeable.sol"; import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; contract Wave is ERC721Upgradeable, OwnableUpgradeable, IMinterManager, PausableUpgradeable, ILockable, ERC721BurnableUpgradeable, ERC2771ContextUpgradeable { mapping(address => bool) private _minters; mapping(uint256 => bool) private _lockedTokens; mapping(uint256 => uint256) private _lockTime; modifier onlyMinter() { require(_minters[_msgSender()], "Not minter"); _; } // {trustedForwarder} is initialized as a immutable variable of Wave, which locates at the code segment // so the proxy can access the {trustedForwarder} without accessing its storage /// @custom:oz-upgrades-unsafe-allow constructor constructor(address trustedForwarder) ERC2771ContextUpgradeable(trustedForwarder){ _disableInitializers(); } function initialize() public initializer { __ERC721_init("Wave", "WV"); __Ownable_init( _msgSender()); __Pausable_init(); __ERC721Burnable_init(); } function mint(address to, uint256 tokenId) public whenNotPaused onlyMinter { _safeMint(to, tokenId); } function isMinter(address account) public view returns (bool) { return _minters[account]; } function addMinter(address account) external override whenNotPaused onlyOwner { _minters[account] = true; } function removeMinter(address account) external override whenNotPaused onlyOwner{ delete _minters[account]; } function pause() public onlyOwner { _pause(); } function unpause() public onlyOwner { _unpause(); } function lockTokens(uint256[] calldata tokenIds, uint256 lockTime) external override whenNotPaused { require(lockTime > 0, "Lock time must be greater than 0"); // change isMinter to public if (isMinter(_msgSender())) { for (uint256 i = 0; i < tokenIds.length; i++) { _requireOwned(tokenIds[i]); _lockedTokens[tokenIds[i]] = true; _lockTime[tokenIds[i]] = block.timestamp + lockTime; } return; } for (uint256 i = 0; i < tokenIds.length; i++) { uint256 tokenId = tokenIds[i]; require(ownerOf(tokenId) == _msgSender(), "Not owner or minter"); _lockedTokens[tokenId] = true; _lockTime[tokenId] = block.timestamp + lockTime; } } function unlockTokens(uint256[] calldata tokenIds) external override whenNotPaused { if (isMinter(_msgSender())) { for (uint256 i = 0; i < tokenIds.length; i++) { _requireOwned(tokenIds[i]); delete _lockedTokens[tokenIds[i]]; delete _lockTime[tokenIds[i]]; } return; } for (uint256 i = 0; i < tokenIds.length; i++) { uint256 tokenId = tokenIds[i]; require(ownerOf(tokenId) == _msgSender(), "Not owner or minter"); delete _lockedTokens[tokenIds[i]]; delete _lockTime[tokenIds[i]]; } } function isTokenLocked(uint256 tokenId) public view override returns (bool) { _requireOwned(tokenId); return _lockedTokens[tokenId]; } function burn(uint256 tokenId) public override { require(ownerOf(tokenId) == msg.sender, "Not owner"); super.burn(tokenId); } function _msgSender() internal view virtual override(ERC2771ContextUpgradeable, ContextUpgradeable) returns (address sender) { return super._msgSender(); } function _msgData() internal view virtual override(ERC2771ContextUpgradeable, ContextUpgradeable) returns (bytes calldata) { return super._msgData(); } function _contextSuffixLength() internal view virtual override(ERC2771ContextUpgradeable, ContextUpgradeable) returns (uint256) { return super._contextSuffixLength(); } }
Proxy
-
Transparent proxy
-
function clashes between proxy and logic contract
- proxy contract has owner() and upgradeTo() functions
- logic contract has owner() and transfer() functions
msg.sender owner() upgradeTo() transfer() owner returns proxy.owner() returns proxy.upgradeTo() fails other returns erc20.owner() fails returns erc20.transfer()
-
@openzeppelin/contracts/proxy/Proxy.sol
- provides a fallback function that delegates all calls
- provides a virtual
_implementationfunction which needs to be overrided
-
@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol
- provides implementation, admin, beacon storage slots
-
@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol
-
see the deployed TransparentUpgradeableProxy
-
see the deployed ProxyAdmin
-
-
UUPS proxy
-
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IMinterManager} from "./interfaces/IMinterManager.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {ILockable} from "./interfaces/ILockable.sol"; import {ERC721BurnableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol"; import {ERC2771ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/metatx/ERC2771ContextUpgradeable.sol"; import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; contract Wave is ERC721Upgradeable, OwnableUpgradeable, IMinterManager, PausableUpgradeable, ILockable, ERC721BurnableUpgradeable, ERC2771ContextUpgradeable, UUPSUpgradeable { mapping(address => bool) private _minters; mapping(uint256 => bool) private _lockedTokens; mapping(uint256 => uint256) private _lockTime; modifier onlyMinter() { require(_minters[_msgSender()], "Not minter"); _; } // {trustedForwarder} is initialized as a immutable variable of Wave, which locates at the code segment // so the proxy can access the {trustedForwarder} without accessing its storage /// @custom:oz-upgrades-unsafe-allow constructor constructor(address trustedForwarder) ERC2771ContextUpgradeable(trustedForwarder) { _disableInitializers(); } function initialize() public initializer { __ERC721_init("Wave", "WV"); __Ownable_init(_msgSender()); __Pausable_init(); __ERC721Burnable_init(); } function mint(address to, uint256 tokenId) public whenNotPaused onlyMinter { _safeMint(to, tokenId); } function isMinter(address account) public view returns (bool) { return _minters[account]; } function addMinter(address account) external override whenNotPaused onlyOwner { _minters[account] = true; } function removeMinter(address account) external override whenNotPaused onlyOwner { delete _minters[account]; } function pause() public onlyOwner { _pause(); } function unpause() public onlyOwner { _unpause(); } function lockTokens(uint256[] calldata tokenIds, uint256 lockTime) external override whenNotPaused { require(lockTime > 0, "Lock time must be greater than 0"); // change isMinter to public if (isMinter(_msgSender())) { for (uint256 i = 0; i < tokenIds.length; i++) { _requireOwned(tokenIds[i]); _lockedTokens[tokenIds[i]] = true; _lockTime[tokenIds[i]] = block.timestamp + lockTime; } return; } for (uint256 i = 0; i < tokenIds.length; i++) { uint256 tokenId = tokenIds[i]; require(ownerOf(tokenId) == _msgSender(), "Not owner or minter"); _lockedTokens[tokenId] = true; _lockTime[tokenId] = block.timestamp + lockTime; } } function unlockTokens(uint256[] calldata tokenIds) external override whenNotPaused { if (isMinter(_msgSender())) { for (uint256 i = 0; i < tokenIds.length; i++) { _requireOwned(tokenIds[i]); delete _lockedTokens[tokenIds[i]]; delete _lockTime[tokenIds[i]]; } return; } for (uint256 i = 0; i < tokenIds.length; i++) { uint256 tokenId = tokenIds[i]; require(ownerOf(tokenId) == _msgSender(), "Not owner or minter"); delete _lockedTokens[tokenIds[i]]; delete _lockTime[tokenIds[i]]; } } function isTokenLocked(uint256 tokenId) public view override returns (bool) { _requireOwned(tokenId); return _lockedTokens[tokenId]; } function burn(uint256 tokenId) public override { require(ownerOf(tokenId) == msg.sender, "Not owner"); super.burn(tokenId); } function _msgSender() internal view virtual override(ERC2771ContextUpgradeable, ContextUpgradeable) returns (address sender) { return super._msgSender(); } function _msgData() internal view virtual override(ERC2771ContextUpgradeable, ContextUpgradeable) returns (bytes calldata) { return super._msgData(); } function _contextSuffixLength() internal view virtual override(ERC2771ContextUpgradeable, ContextUpgradeable) returns (uint256) { return super._contextSuffixLength(); } function _authorizeUpgrade(address) internal override onlyOwner {} } -
see the deployed Wave on the etherscan
-
see the deployed Proxy on the etherscan
-
see the deployed upgraded Wave on the etherscan
-