ArtPrint

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"
        ]
      }
    }
  }
}}

Tags:
Multisig, Multi-Signature, Factory|addr:0x07410766724ae836fa3ceb7f22574f25c10dd6fc|verified:true|block:23398959|tx:0xa4b843107407d213229caada8540c622e331d9b35532f5eae4515c59b56d87f0|first_check:1758307874

Submitted on: 2025-09-19 20:51:16

Comments

Log in to comment.

No comments yet.