Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"contracts/ArtPrint.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
/**
* @title ArtPrint
* @author Cyber Bandits
* @notice A smart contract that enables NFT owners to create physical art prints of their digital assets
* @dev This contract manages the printing process for NFTs, including payment collection,
* print tracking, and administrative functions. It supports multiple print sizes with
* different pricing tiers and time-based availability windows.
*
* @custom:features
* - Time-gated printing availability (start/end dates)
* - Multiple print sizes with configurable pricing
* - One-time printing per NFT (prevents duplicate prints)
* - Owner verification through NFT ownership
* - Bulk administrative operations
* - ETH and ERC20 token withdrawal capabilities
* - Efficient user print history tracking with pagination
* - Comprehensive statistics and analytics
* - Atomic configuration updates
*
* @custom:security
* - Only NFT owners can print their tokens
* - Only contract owner can perform administrative functions
* - Prevents double-printing of the same NFT
* - Validates payment amounts before processing
*
* @custom:usage
* 1. Contract owner sets up printing parameters (prices, dates, sizes)
* 2. NFT owners call print() with their token ID and desired size
* 3. Contract verifies ownership and collects payment
* 4. Print is recorded and tracked in contract state
* 5. Owner can withdraw collected funds
*/
/**
* @title INFT
* @notice Interface for NFT contracts to verify ownership
* @dev Used to check if the caller owns the NFT they want to print
*/
interface INFT {
/**
* @notice Returns the owner of a specific token ID
* @param tokenId The token ID to check ownership for
* @return owner The address of the token owner
*/
function ownerOf(uint256 tokenId) external view returns (address owner);
}
/**
* @title IERC20
* @notice Interface for ERC20 token operations
* @dev Used for token balance checking and transfers during withdrawal
*/
interface IERC20 {
/**
* @notice Returns the token balance of an account
* @param account The account to check the balance for
* @return The token balance of the account
*/
function balanceOf(address account) external view returns (uint256);
/**
* @notice Transfers tokens to a specified address
* @param to The address to transfer tokens to
* @param amount The amount of tokens to transfer
* @return True if the transfer was successful
*/
function transfer(address to, uint256 amount) external returns (bool);
}
contract ArtPrint {
// ============ Custom Errors ============
/// @notice Thrown when a function is called by someone who is not the contract owner
error NotOwner();
/// @notice Thrown when attempting to print an NFT that has already been printed
error AlreadyPrinted();
/// @notice Thrown when the payment amount is less than the required price for the selected size
error InsufficientPayment();
/// @notice Thrown when an invalid print size is specified (out of range or not available)
error InvalidSize();
/// @notice Thrown when an invalid token ID is provided (0 or greater than totalSupply)
error InvalidToken();
/// @notice Thrown when printing is attempted outside the allowed time window
error PrintNotAvailable();
/// @notice Thrown when an invalid contract address is provided
error InvalidContractAddress();
/// @notice Thrown when an invalid date range is provided
error InvalidDateRange();
/// @notice Thrown when an invalid number of sizes is provided
error InvalidNumberOfSizes();
/// @notice Thrown when prices array length doesn't match expected length
error PricesLengthMismatch();
/// @notice Thrown when a price is zero or invalid
error InvalidPrice();
/// @notice Thrown when an empty array is provided
error EmptyArray();
/// @notice Thrown when an invalid address is provided
error InvalidAddress();
/// @notice Thrown when an invalid timestamp is provided
error InvalidTimestamp();
/// @notice Thrown when there's no ETH to withdraw
error NoETHToWithdraw();
/// @notice Thrown when there are no tokens to withdraw
error NoTokensToWithdraw();
/// @notice Thrown when number of sizes doesn't match prices array length
error SizesPricesMismatch();
// ============ State Variables ============
/// @notice The address of the contract owner who can perform administrative functions
address public owner;
/// @notice The address of the NFT contract that this ArtPrint contract is associated with
address public contractAddress;
/// @notice The timestamp when printing becomes available (inclusive)
uint256 public startDate;
/// @notice The timestamp when printing becomes unavailable (inclusive)
uint256 public endDate;
/// @notice The total supply of NFTs that can be printed (token IDs range from 1 to totalSupply)
uint256 public totalSupply;
/// @notice Array of prices for each print size (index corresponds to size)
uint256[] public prices;
/// @notice The number of different print sizes available (e.g., 2 means sizes 0 and 1 are available)
uint256 public numberOfSizes;
/// @notice Mapping from token ID to the address that printed it (address(0) means not printed)
mapping(uint256 => address) public printedBy;
/// @notice Mapping from token ID to the size that was printed
mapping(uint256 => uint256) public printSize;
// ============ Constructor ============
/**
* @notice Initializes the ArtPrint contract with configuration parameters
* @dev Sets up the initial state for the printing system
* @param _contractAddress The address of the NFT contract to associate with this printer
* @param _startDate The timestamp when printing becomes available (inclusive)
* @param _endDate The timestamp when printing becomes unavailable (inclusive)
* @param _totalSupply The total supply of NFTs that can be printed (token IDs range from 1 to _totalSupply)
* @param _prices Array of prices for each print size (index corresponds to size)
* @param _numberOfSizes The number of sizes (e.g., 2 means sizes 0 and 1 are available)
*
* @custom:requirements
* - _contractAddress must not be address(0)
* - _startDate must be less than _endDate
* - _prices array length must match _numberOfSizes (e.g., if _numberOfSizes = 2, then _prices must have 2 elements for sizes 0 and 1)
* - All prices must be greater than 0
*/
constructor(
address _contractAddress,
uint256 _startDate,
uint256 _endDate,
uint256 _totalSupply,
uint256[] memory _prices,
uint256 _numberOfSizes
) {
if (_contractAddress == address(0)) {
revert InvalidContractAddress();
}
if (_startDate >= _endDate) {
revert InvalidDateRange();
}
if (_numberOfSizes == 0) {
revert InvalidNumberOfSizes();
}
if (_prices.length != _numberOfSizes) {
revert PricesLengthMismatch();
}
// Validate all prices are greater than 0
for (uint256 i = 0; i < _prices.length; i++) {
if (_prices[i] == 0) {
revert InvalidPrice();
}
}
owner = msg.sender;
contractAddress = _contractAddress;
startDate = _startDate;
endDate = _endDate;
totalSupply = _totalSupply;
prices = _prices;
numberOfSizes = _numberOfSizes;
}
// ============ Core Functions ============
/**
* @notice Allows NFT owners to create a physical print of their digital asset
* @dev This is the main function that handles the printing process, including ownership verification,
* payment collection, and state updates. Each NFT can only be printed once.
*
* @param _id The token ID of the NFT to print (must be between 1 and totalSupply inclusive)
* @param _size The size of the print (0 to numberOfSizes-1, e.g., if numberOfSizes=2, valid sizes are 0 and 1)
*
* @custom:requirements
* - Caller must be the owner of the NFT
* - NFT must not have been printed before
* - Current time must be within the printing window (startDate <= now <= endDate)
* - Token ID must be valid (1 <= _id <= totalSupply, token IDs start at 1)
* - Size must be valid (0 <= _size < numberOfSizes, e.g., if numberOfSizes=2, valid sizes are 0 and 1)
* - Payment must be sufficient for the selected size
*
* @custom:effects
* - Records the print in all relevant mappings
* - Collects ETH payment
*
* @custom:reverts
* - InvalidToken: If token ID is 0 or greater than totalSupply
* - InvalidSize: If size is out of range or price is 0
* - PrintNotAvailable: If current time is outside printing window
* - AlreadyPrinted: If NFT has already been printed
* - NotOwner: If caller doesn't own the NFT
* - InsufficientPayment: If payment is less than required price
*/
function print(uint256 _id, uint256 _size) external payable {
// Validate token ID first
if (_id > totalSupply || _id == 0) {
revert InvalidToken();
}
// Validate size
if (_size >= numberOfSizes) {
revert InvalidSize();
}
// Check time window
if (block.timestamp < startDate || block.timestamp > endDate) {
revert PrintNotAvailable();
}
// Check if already printed (printedBy[_id] != address(0) means it's printed)
if (printedBy[_id] != address(0)) {
revert AlreadyPrinted();
}
// Get price and validate
uint256 price = prices[_size];
if (price == 0) {
revert InvalidSize();
}
// Check payment
if (msg.value < price) {
revert InsufficientPayment();
}
// Verify ownership (moved before state changes to prevent reentrancy)
if (INFT(contractAddress).ownerOf(_id) != msg.sender) {
revert NotOwner();
}
// Record the print
printedBy[_id] = msg.sender;
printSize[_id] = _size;
}
// ============ Bulk Administrative Functions ============
/**
* @notice Bulk update the printedBy address and print size for multiple token IDs
* @dev Allows the owner to set the printer address and size for multiple NFTs at once
* @param _ids Array of token IDs to update
* @param _printedBy Array of addresses to set as printer for each token (address(0) to undo printing)
* @param _sizes Array of print sizes corresponding to each token ID
*
* @custom:requirements
* - Caller must be the contract owner
* - _ids array must not be empty
* - _ids, _printedBy, and _sizes arrays must have the same length
* - All sizes must be valid (0 to numberOfSizes-1)
*
* @custom:effects
* - Updates printedBy and printSize mappings for all specified token IDs
* - If _printedBy[i] is address(0), effectively undoes the printing for that token
* - Useful for administrative corrections or bulk operations
*/
function bulkSetPrintedBy(
uint256[] memory _ids,
address[] memory _printedBy,
uint256[] memory _sizes
) external onlyOwner {
if (_ids.length == 0) {
revert EmptyArray();
}
if (_ids.length != _sizes.length || _ids.length != _printedBy.length) {
revert SizesPricesMismatch();
}
for (uint256 i = 0; i < _ids.length; i++) {
if (_ids[i] == 0 || _ids[i] > totalSupply) {
revert InvalidToken();
}
if (_sizes[i] >= numberOfSizes) {
revert InvalidSize();
}
printedBy[_ids[i]] = _printedBy[i];
printSize[_ids[i]] = _sizes[i];
}
}
// ============ Configuration Functions ============
/**
* @notice Updates the associated NFT contract address
* @dev Allows the owner to change which NFT contract this printer is associated with
* @param _contractAddress The new NFT contract address
*
* @custom:requirements
* - Caller must be the contract owner
* - _contractAddress must not be address(0)
*
* @custom:effects
* - Updates contractAddress state variable
*/
function setContractAddress(address _contractAddress) external onlyOwner {
if (_contractAddress == address(0)) {
revert InvalidContractAddress();
}
contractAddress = _contractAddress;
}
/**
* @notice Updates the start date for printing availability
* @dev Allows the owner to change when printing becomes available
* @param _startDate The new start timestamp (inclusive)
*
* @custom:requirements
* - Caller must be the contract owner
* - _startDate must be less than endDate
*
* @custom:effects
* - Updates startDate state variable
*/
function setStartDate(uint256 _startDate) external onlyOwner {
if (_startDate >= endDate) {
revert InvalidDateRange();
}
startDate = _startDate;
}
/**
* @notice Updates the end date for printing availability
* @dev Allows the owner to change when printing becomes unavailable
* @param _endDate The new end timestamp (inclusive)
*
* @custom:requirements
* - Caller must be the contract owner
* - _endDate must be greater than startDate
*
* @custom:effects
* - Updates endDate state variable
*/
function setEndDate(uint256 _endDate) external onlyOwner {
if (_endDate <= startDate) {
revert InvalidDateRange();
}
endDate = _endDate;
}
/**
* @notice Updates the total supply of NFTs that can be printed
* @dev Allows the owner to change the total supply (token IDs range from 1 to _totalSupply)
* @param _totalSupply The new total supply value
*
* @custom:requirements
* - Caller must be the contract owner
*
* @custom:effects
* - Updates totalSupply state variable
*/
function setTotalSupply(uint256 _totalSupply) external onlyOwner {
totalSupply = _totalSupply;
}
/**
* @notice Atomically updates both prices and number of sizes
* @dev Solves the circular dependency between prices and numberOfSizes
* @param _prices Array of new prices (index corresponds to size)
* @param _numberOfSizes The new number of sizes
*
* @custom:requirements
* - Caller must be the contract owner
* - _prices array length must match _numberOfSizes
* - All prices must be greater than 0
* - _numberOfSizes must be greater than 0
*
* @custom:effects
* - Updates both prices array and numberOfSizes atomically
* - Prevents configuration inconsistencies
*/
function setPricesAndSizes(uint256[] memory _prices, uint256 _numberOfSizes) external onlyOwner {
if (_numberOfSizes == 0) {
revert InvalidNumberOfSizes();
}
if (_prices.length != _numberOfSizes) {
revert PricesLengthMismatch();
}
for (uint256 i = 0; i < _prices.length; i++) {
if (_prices[i] == 0) {
revert InvalidPrice();
}
}
prices = _prices;
numberOfSizes = _numberOfSizes;
}
/**
* @notice Transfers ownership of the contract to a new address
* @dev Allows the current owner to transfer ownership to another address
* @param _owner The new owner address
*
* @custom:requirements
* - Caller must be the current contract owner
* - _owner must not be address(0)
*
* @custom:effects
* - Updates owner state variable
* - New owner gains access to all owner-only functions
*/
function setOwner(address _owner) external onlyOwner {
if (_owner == address(0)) {
revert InvalidAddress();
}
owner = _owner;
}
// ============ View Functions ============
// ============ Modifiers ============
/**
* @notice Restricts function access to the contract owner only
* @dev Reverts with NotOwner error if caller is not the contract owner
*
* @custom:requirements
* - msg.sender must equal the contract owner address
*
* @custom:reverts
* - NotOwner: If caller is not the contract owner
*/
modifier onlyOwner() {
if (msg.sender != owner) {
revert NotOwner();
}
_;
}
// ============ Withdrawal Functions ============
/**
* @notice Allows the contract owner to withdraw all ETH from the contract
* @dev Transfers the entire ETH balance to the specified recipient address
* @param recipient The address to send the ETH to (must be payable)
*
* @custom:requirements
* - Caller must be the contract owner
* - recipient must not be address(0)
* - Contract must have a positive ETH balance
*
* @custom:effects
* - Transfers entire ETH balance to recipient
* - Contract balance becomes 0
*
* @custom:reverts
* - NotOwner: If caller is not the contract owner
* - InvalidAddress: If recipient is address(0)
* - NoETHToWithdraw: If contract balance is 0
*/
function withdrawAllETH(address payable recipient) external onlyOwner {
if (recipient == address(0)) {
revert InvalidAddress();
}
uint256 balance = address(this).balance;
if (balance == 0) {
revert NoETHToWithdraw();
}
recipient.transfer(balance);
}
/**
* @notice Allows the contract owner to withdraw all ERC20 tokens from the contract
* @dev Transfers the entire balance of a specific ERC20 token to the specified recipient
* @param token The ERC20 token contract address to withdraw
* @param recipient The address to send the tokens to
*
* @custom:requirements
* - Caller must be the contract owner
* - token must not be address(0)
* - recipient must not be address(0)
* - Contract must have a positive token balance
*
* @custom:effects
* - Transfers entire token balance to recipient
* - Contract token balance becomes 0
*
* @custom:reverts
* - NotOwner: If caller is not the contract owner
* - InvalidAddress: If recipient is address(0)
* - InvalidContractAddress: If token is address(0)
* - NoTokensToWithdraw: If contract token balance is 0
*/
function withdrawAllERC20(address token, address recipient) external onlyOwner {
if (recipient == address(0)) {
revert InvalidAddress();
}
if (token == address(0)) {
revert InvalidContractAddress();
}
uint256 balance = IERC20(token).balanceOf(address(this));
if (balance == 0) {
revert NoTokensToWithdraw();
}
IERC20(token).transfer(recipient, balance);
}
// ============ Receive Function ============
/**
* @notice Allows the contract to receive ETH
* @dev This function is required for the contract to accept ETH payments
* sent directly to the contract address (e.g., from print() function calls)
*
* @custom:effects
* - Increases contract ETH balance by the received amount
* - No additional logic is performed
*/
receive() external payable {}
}
"
}
},
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": false,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"devdoc",
"userdoc",
"metadata",
"abi"
]
}
}
}
}}
Submitted on: 2025-09-19 20:51:16
Comments
Log in to comment.
No comments yet.