PermanStorageOne

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/EternalStorage.sol": {
      "content": "// SPDX-License-Identifier: MIT\r
pragma solidity 0.8.24;\r
\r
import "@openzeppelin/contracts/access/Ownable.sol";\r
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";\r
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";\r
\r
contract PermanStorageOne is Ownable, ReentrancyGuard {\r
    // ============= Storage Variables ============\r
    string public name;\r
    string public symbol;\r
    uint256 public feeQty;\r
	uint256 public limitMediaId;\r
    \r
    struct Media {\r
        address owner;\r
        string description;\r
        uint256 mediaType; // 0 = video, 1 = image\r
        uint256 timestamp;\r
        bytes[] chunks;\r
        bool finalized;\r
        uint256 totalChunks;\r
        uint256 chunksUploaded;\r
    }\r
    \r
    IERC20 public chainGalleryToken;\r
    \r
    // ============= Sale Variables ============\r
    uint256 public askingPrice;\r
    address public payoutAddress;\r
    uint256 public constant MAX_BUYERS = 7;\r
    \r
    bool public saleActive;\r
    bool public allowBidWithdrawal;\r
    uint256 public emergencyWithdrawTimestamp;\r
    uint256 public constant EMERGENCY_DELAY = 7 days;\r
    \r
    struct Buyer {\r
        address buyerAddress;\r
        uint256 bidAmount;\r
        bool exists;\r
    }\r
    \r
    Buyer[] public buyers;\r
    mapping(address => uint256) public buyerIndex;\r
    \r
    struct FailedRefund {\r
        address buyer;\r
        uint256 amount;\r
        uint256 timestamp;\r
        bool claimed;\r
    }\r
    \r
    mapping(address => FailedRefund) public failedRefunds;\r
    address[] public failedRefundAddresses;\r
    \r
    // ============= Fee Variables ============\r
    address public feeReceiver;\r
    uint256 public withdrawalFee = 0.0001 ether;\r
    \r
    // ============= Subscription Variables ============\r
    struct Subscription {\r
        address subscriber;\r
        uint256 startTime;\r
        uint256 endTime;\r
        uint256 mediaId;\r
        bool active;\r
    }\r
    \r
    struct SubscriptionPlan {\r
        uint256 duration;\r
        uint256 price;\r
        bool active;\r
    }\r
    \r
    mapping(address => mapping(uint256 => Subscription)) public subscriptions;\r
    mapping(uint256 => SubscriptionPlan) public subscriptionPlans;\r
    uint256 public nextPlanId;\r
    uint256 public subFee;\r
    mapping(address => bool) public authorizedReaders;\r
\r
    // ============= Media Storage ============\r
    mapping(uint256 => Media) public media;\r
    mapping(address => uint256[]) public userMedia;\r
    uint256[] public allMedia;\r
    uint256 public nextMediaId;\r
\r
    // ============= Events ============\r
    // Media Events\r
    event MediaCreated(uint256 indexed mediaId, address indexed owner, uint256 mediaType, string description);\r
    event ChunkAdded(uint256 indexed mediaId, uint256 chunkIndex, uint256 chunkSize);\r
    event MediaFinalized(uint256 indexed mediaId);\r
    event MediaDefinalized(uint256 indexed mediaId);\r
    event MediaDescriptionUpdated(uint256 indexed mediaId, string newDescription);\r
    event sendTokensToContract_(address indexed to, uint256 amount);\r
    \r
    // Sale Events\r
    event SaleStarted(uint256 price, address payoutTo);\r
    event SaleStopped();\r
    event BidPlaced(address indexed buyer, uint256 amount);\r
    event SaleCompleted(address indexed buyer, uint256 price);\r
    event BidAccepted(address indexed buyer, uint256 acceptedPrice);\r
    event WithdrawalPermissionChanged(bool allowed);\r
    event BidWithdrawn(address indexed buyer, uint256 refundAmount);\r
    event EmergencyWithdrawInitiated(uint256 timestamp, uint256 unlockTime);\r
    event EmergencyWithdrawExecuted(uint256 amount, uint256 timestamp);\r
    \r
    // Fee Events\r
    event WithdrawalFeeUpdated(uint256 newFee);\r
    event TokensWithdrawn(address indexed to, uint256 amount, uint256 ethFee);\r
    \r
    // Refund Events\r
    event RefundFailed(address indexed buyer, uint256 amount, uint256 timestamp);\r
    event RefundClaimed(address indexed buyer, uint256 amount);\r
    event ManualRefundProcessed(address indexed buyer, uint256 amount);\r
    \r
    // Subscription Events\r
    event SubscriptionPlanCreated(uint256 indexed planId, uint256 duration, uint256 price);\r
    event SubscriptionPlanUpdated(uint256 indexed planId, uint256 duration, uint256 price, bool active);\r
    event Subscribed(address indexed subscriber, uint256 indexed mediaId, uint256 planId, uint256 endTime);\r
    event AuthorizedReaderUpdated(address indexed reader, bool authorized);\r
\r
    // ============= Constructor ============\r
    constructor(uint256 id, uint256 _feeQty, address feeReceiver_, address owner_,address chainGalleryToken_ ) Ownable(owner_) {\r
        require(id > 0, "Invalid media ID");\r
        subFee = 0;\r
		limitMediaId=5;\r
        \r
        // Updated to ChainGallery token\r
        chainGalleryToken = IERC20(address(chainGalleryToken_));\r
        \r
        string memory paddedId = _padId(id, 13);\r
        name = string(abi.encodePacked("EternalStorage_", paddedId));\r
        symbol = string(abi.encodePacked("EStoOne_", paddedId));\r
        \r
        saleActive = false;\r
        feeQty = _feeQty;\r
        feeReceiver = feeReceiver_;\r
        allowBidWithdrawal = false;\r
        nextGroupId = 0;\r
        emergencyWithdrawTimestamp = 0;\r
        _transferOwnership(owner_);\r
    }\r
\r
    // ============= Modifiers ============\r
    modifier hasRequiredChainGalleryBalance(uint256 monthNumber) {\r
        require(monthNumber >= 1 && monthNumber <= 12, "Invalid month");\r
        uint256 requiredBalance = monthNumber * feeQty * (10 ** 18);\r
        require(chainGalleryToken.balanceOf(address(this)) >= requiredBalance, "Contract insufficient ChainGallery");\r
        _;\r
    }\r
\r
\r
    // ============= Internal Helper Functions ============\r
    function _uintToString(uint256 value) internal pure returns (string memory) {\r
        if (value == 0) return "0";\r
        uint256 temp = value;\r
        uint256 digits;\r
        while (temp != 0) {\r
            digits++;\r
            temp /= 10;\r
        }\r
        bytes memory buffer = new bytes(digits);\r
        while (value != 0) {\r
            digits -= 1;\r
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));\r
            value /= 10;\r
        }\r
        return string(buffer);\r
    }\r
\r
    function _padId(uint256 id, uint256 length) internal pure returns (string memory) {\r
        string memory idStr = _uintToString(id);\r
        uint256 idLen = bytes(idStr).length;\r
\r
        if (idLen >= length) {\r
            return idStr;\r
        }\r
\r
        bytes memory padded = new bytes(length);\r
        uint256 padLen = length - idLen;\r
\r
        for (uint256 i = 0; i < padLen; i++) {\r
            padded[i] = bytes1("0");\r
        }\r
\r
        for (uint256 i = 0; i < idLen; i++) {\r
            padded[padLen + i] = bytes(idStr)[i];\r
        }\r
\r
        return string(padded);\r
    }\r
\r
    function _findEmptySlot() internal view returns (uint256) {\r
        for (uint256 i = 0; i < buyers.length; i++) {\r
            if (!buyers[i].exists) {\r
                return i;\r
            }\r
        }\r
        return type(uint256).max;\r
    }\r
\r
    function _getActiveBuyersCount() internal view returns (uint256) {\r
        uint256 count = 0;\r
        for (uint256 i = 0; i < buyers.length; i++) {\r
            if (buyers[i].exists) {\r
                count++;\r
            }\r
        }\r
        return count;\r
    }\r
\r
    function _clearBuyersData() internal {\r
        for (uint256 i = 0; i < buyers.length; i++) {\r
            if (buyers[i].exists) {\r
                delete buyerIndex[buyers[i].buyerAddress];\r
            }\r
        }\r
        delete buyers;\r
    }\r
\r
    function _recordFailedRefund(address buyer, uint256 amount) internal {\r
        if (failedRefunds[buyer].amount == 0) {\r
            failedRefundAddresses.push(buyer);\r
        }\r
        \r
        failedRefunds[buyer] = FailedRefund({\r
            buyer: buyer,\r
            amount: amount,\r
            timestamp: block.timestamp,\r
            claimed: false\r
        });\r
    }\r
\r
    function _hasActiveSubscription(address subscriber, uint256 mediaId) internal view returns (bool) {\r
        Subscription storage sub = subscriptions[subscriber][mediaId];\r
        return sub.active && sub.endTime > block.timestamp;\r
    }\r
\r
    function _refundAllBuyersExcept(address exceptBuyer) internal {\r
        Buyer[] memory buyersToRefund = buyers;\r
        \r
        for (uint256 i = 0; i < buyersToRefund.length; i++) {\r
            if (buyersToRefund[i].exists && buyersToRefund[i].buyerAddress != exceptBuyer) {\r
                address buyerAddr = buyersToRefund[i].buyerAddress;\r
                uint256 refundAmount = buyersToRefund[i].bidAmount;\r
                \r
                if (refundAmount > 0) {\r
                    (bool sent, ) = payable(buyerAddr).call{value: refundAmount}("");\r
                    if (!sent) {\r
                        _recordFailedRefund(buyerAddr, refundAmount);\r
                        emit RefundFailed(buyerAddr, refundAmount, block.timestamp);\r
                    }\r
                }\r
            }\r
        }\r
    }\r
\r
    function _transferOwnershipWithSale(address newOwner, uint256 salePrice) internal nonReentrant {\r
        require(newOwner != address(0), "New owner is the zero address");\r
        require(saleActive, "Sale is not active");\r
        \r
        saleActive = false;\r
        _transferOwnership(newOwner);\r
        \r
        // Process payments\r
        uint256 feeAmount = (salePrice * 2) / 100;\r
        uint256 payoutAmount = salePrice - feeAmount;\r
        \r
        // Transfer funds\r
        (bool success, ) = payable(payoutAddress).call{value: payoutAmount}("");\r
        require(success, "Payout failed");\r
        \r
        (bool feeSuccess, ) = payable(feeReceiver).call{value: feeAmount}("");\r
        require(feeSuccess, "Fee transfer failed");\r
        \r
        // Refund other buyers\r
        _refundAllBuyersExcept(newOwner);\r
        _clearBuyersData();\r
        \r
        emit SaleCompleted(newOwner, salePrice);\r
    }\r
     function hasPlacedBid(address user) external view returns (bool) {\r
        uint256 index = buyerIndex[user];\r
        return index > 0 && index <= buyers.length && buyers[index - 1].exists;\r
    }\r
    // ============= Media Functions ============\r
    function createMedia(\r
        string memory description,\r
        uint256 mediaType,\r
        uint256 totalChunks,\r
        uint256 monthNumber\r
    ) public onlyOwner hasRequiredChainGalleryBalance(monthNumber) returns (uint256) {\r
        require(mediaType == 0 || mediaType == 1, "Invalid media type");\r
		require(nextMediaId <=limitMediaId, "limitMediaId");\r
        require(totalChunks > 0, "Total chunks must be greater than 0");\r
        limitMediaId=monthNumber*5;\r
        uint256 mediaId = nextMediaId++;\r
        Media storage newMedia = media[mediaId];\r
        newMedia.owner = msg.sender;\r
        newMedia.description = description;\r
        newMedia.mediaType = mediaType;\r
        newMedia.timestamp = block.timestamp;\r
        newMedia.totalChunks = totalChunks;\r
        newMedia.chunksUploaded = 0;\r
\r
        for (uint256 i = 0; i < totalChunks; i++) {\r
            newMedia.chunks.push(bytes(""));\r
        }\r
\r
        userMedia[msg.sender].push(mediaId);\r
        allMedia.push(mediaId);\r
\r
        emit MediaCreated(mediaId, msg.sender, mediaType, description);\r
        return mediaId;\r
    }\r
\r
    function addChunk(\r
        uint256 mediaId,\r
        uint256 chunkIndex,\r
        bytes memory chunkData\r
    ) public onlyOwner {\r
        Media storage mediaItem = media[mediaId];\r
        require(mediaItem.owner == msg.sender, "Not the owner");\r
        require(!mediaItem.finalized, "Media is finalized");\r
        require(chunkIndex < mediaItem.totalChunks, "Invalid chunk index");\r
        require(mediaItem.chunks[chunkIndex].length == 0, "Chunk already uploaded");\r
\r
        mediaItem.chunks[chunkIndex] = chunkData;\r
        mediaItem.chunksUploaded++;\r
\r
        emit ChunkAdded(mediaId, chunkIndex, chunkData.length);\r
    }\r
\r
    function addChunksBatch(\r
        uint256 mediaId,\r
        uint256[] memory chunkIndices,\r
        bytes[] memory chunksData\r
    ) public onlyOwner {\r
        require(chunkIndices.length == chunksData.length, "Arrays length mismatch");\r
		\r
\r
        for (uint256 i = 0; i < chunkIndices.length; i++) {\r
            addChunk(mediaId, chunkIndices[i], chunksData[i]);\r
        }\r
    }\r
\r
    function storeMedia(\r
        string memory description,\r
        uint256 mediaType,\r
        bytes[] memory chunks,\r
        uint256 monthNumber\r
    ) public onlyOwner hasRequiredChainGalleryBalance(monthNumber) returns (uint256) {\r
        require(chunks.length > 0, "No chunks provided");\r
\r
        uint256 mediaId = createMedia(description, mediaType, chunks.length, monthNumber);\r
\r
        for (uint256 i = 0; i < chunks.length; i++) {\r
            media[mediaId].chunks[i] = chunks[i];\r
        }\r
        media[mediaId].chunksUploaded = chunks.length;\r
\r
        return mediaId;\r
    }\r
\r
    function finalizeMedia(uint256 mediaId) public onlyOwner {\r
        Media storage mediaItem = media[mediaId];\r
        require(mediaItem.owner == msg.sender, "Not the owner");\r
        require(!mediaItem.finalized, "Already finalized");\r
        require(mediaItem.chunksUploaded == mediaItem.totalChunks, "Not all chunks uploaded");\r
\r
        mediaItem.finalized = true;\r
        emit MediaFinalized(mediaId);\r
    }\r
\r
\r
    // ============= Media Access Functions (Fixed Access Control) ============\r
    function getMedia(uint256 mediaId) public view canAccessMedia(mediaId) returns (\r
        address owner,\r
        string memory description,\r
        uint256 mediaType,\r
        uint256 timestamp,\r
        bytes[] memory chunks,\r
        uint256 totalChunks,\r
        uint256 chunksUploaded,\r
        bool finalized\r
    ) {\r
        Media storage mediaItem = media[mediaId];\r
        return (\r
            mediaItem.owner,\r
            mediaItem.description,\r
            mediaItem.mediaType,\r
            mediaItem.timestamp,\r
            mediaItem.chunks,\r
            mediaItem.totalChunks,\r
            mediaItem.chunksUploaded,\r
            mediaItem.finalized\r
        );\r
    }\r
\r
    function getMediaChunk(uint256 mediaId, uint256 chunkIndex) public view canAccessMedia(mediaId) returns (bytes memory) {\r
        Media storage mediaItem = media[mediaId];\r
        require(chunkIndex < mediaItem.chunks.length, "Invalid chunk index");\r
        return mediaItem.chunks[chunkIndex];\r
    }\r
\r
    function getMediaProgress(uint256 mediaId) public view canAccessMedia(mediaId) returns (uint256 uploaded, uint256 total) {\r
        Media storage mediaItem = media[mediaId];\r
        return (mediaItem.chunksUploaded, mediaItem.totalChunks);\r
    }\r
\r
    function getUserMedia(address user) public view returns (uint256[] memory) {\r
        // Allow anyone to see which media a user owns (public information)\r
        return userMedia[user];\r
    }\r
\r
    function getAllMedia() public view returns (uint256[] memory) {\r
        // Allow anyone to see all media IDs (public information)\r
        return allMedia;\r
    }\r
\r
    function getMediaCount() public view returns (uint256) {\r
        return allMedia.length;\r
    }\r
\r
    // ============= Sale Functions ============\r
    function startSale(uint256 _askingPrice, address _payoutAddress) external onlyOwner {\r
        require(_askingPrice > 0, "Price must be greater than 0");\r
        require(_payoutAddress != address(0), "Invalid payout address");\r
        require(!saleActive, "Sale already active");\r
\r
        _clearBuyersData();\r
\r
        askingPrice = _askingPrice;\r
        payoutAddress = _payoutAddress;\r
        saleActive = true;\r
        allowBidWithdrawal = false;\r
\r
        emit SaleStarted(_askingPrice, _payoutAddress);\r
    }\r
\r
    function stopSale() external onlyOwner nonReentrant {\r
        require(saleActive, "Sale is not active");\r
        \r
        saleActive = false;\r
        Buyer[] memory buyersToRefund = buyers;\r
        _clearBuyersData();\r
\r
        for (uint256 i = 0; i < buyersToRefund.length; i++) {\r
            if (buyersToRefund[i].exists && buyersToRefund[i].bidAmount > 0) {\r
                (bool sent, ) = payable(buyersToRefund[i].buyerAddress).call{value: buyersToRefund[i].bidAmount}("");\r
                if (!sent) {\r
                    _recordFailedRefund(buyersToRefund[i].buyerAddress, buyersToRefund[i].bidAmount);\r
                    emit RefundFailed(buyersToRefund[i].buyerAddress, buyersToRefund[i].bidAmount, block.timestamp);\r
                }\r
            }\r
        }\r
\r
        emit SaleStopped();\r
    }\r
\r
    function acceptBid(address buyer) external onlyOwner {\r
        require(saleActive, "Sale is not active");\r
        require(buyerIndex[buyer] > 0, "Buyer not found in the list");\r
        \r
        uint256 index = buyerIndex[buyer] - 1; // Convert to 0-based index\r
        require(buyers[index].exists, "Buyer does not exist");\r
        \r
        uint256 bidAmount = buyers[index].bidAmount;\r
        _transferOwnershipWithSale(buyer, bidAmount);\r
        emit BidAccepted(buyer, bidAmount);\r
    }\r
\r
    function acceptHighestBid() external onlyOwner {\r
        require(saleActive, "Sale is not active");\r
        \r
        address highestBidder;\r
        uint256 highestBid = 0;\r
\r
        for (uint256 i = 0; i < buyers.length; i++) {\r
            if (buyers[i].exists && buyers[i].bidAmount > highestBid) {\r
                highestBid = buyers[i].bidAmount;\r
                highestBidder = buyers[i].buyerAddress;\r
            }\r
        }\r
\r
        require(highestBidder != address(0), "No valid bids found");\r
        require(highestBid > 0, "Highest bid must be greater than zero");\r
\r
        _transferOwnershipWithSale(highestBidder, highestBid);\r
        emit BidAccepted(highestBidder, highestBid);\r
    }\r
\r
    function placeBid() external payable {\r
        require(saleActive, "Sale is not active");\r
        require(msg.value > 0, "Bid amount must be greater than 0");\r
        require(msg.value >= askingPrice / 2, "Must deposit at least half of asking price");\r
        require(buyerIndex[msg.sender] == 0, "Already placed a bid");\r
\r
        uint256 activeCount = _getActiveBuyersCount();\r
        require(activeCount < MAX_BUYERS, "Maximum buyers reached");\r
\r
        uint256 emptySlot = _findEmptySlot();\r
        if (emptySlot != type(uint256).max) {\r
            buyers[emptySlot] = Buyer({\r
                buyerAddress: msg.sender,\r
                bidAmount: msg.value,\r
                exists: true\r
            });\r
            buyerIndex[msg.sender] = emptySlot + 1;\r
        } else {\r
            buyers.push(Buyer({\r
                buyerAddress: msg.sender,\r
                bidAmount: msg.value,\r
                exists: true\r
            }));\r
            buyerIndex[msg.sender] = buyers.length;\r
        }\r
\r
        emit BidPlaced(msg.sender, msg.value);\r
\r
        if (msg.value >= askingPrice) {\r
            _transferOwnershipWithSale(msg.sender, msg.value);\r
        }\r
    }\r
    function setBidWithdrawalAllowed(bool allowed) external onlyOwner {\r
        require(saleActive, "Sale is not active");\r
        allowBidWithdrawal = allowed;\r
        emit WithdrawalPermissionChanged(allowed);\r
    }\r
      function getBuyersCount() external view returns (uint256) {\r
        return _getActiveBuyersCount();\r
    }\r
    // ============= Refund Functions ============\r
    function claimFailedRefund() external nonReentrant {\r
        FailedRefund storage refund = failedRefunds[msg.sender];\r
        require(refund.amount > 0, "No failed refund found");\r
        require(!refund.claimed, "Refund already claimed");\r
        \r
        uint256 refundAmount = refund.amount;\r
        refund.claimed = true;\r
        refund.amount = 0;\r
        \r
        (bool success, ) = payable(msg.sender).call{value: refundAmount}("");\r
        require(success, "Refund claim failed");\r
        \r
        emit RefundClaimed(msg.sender, refundAmount);\r
    }\r
\r
    function processManualRefund(address buyer) external onlyOwner nonReentrant {\r
        FailedRefund storage refund = failedRefunds[buyer];\r
        require(refund.amount > 0, "No failed refund found");\r
        require(!refund.claimed, "Refund already claimed");\r
        \r
        uint256 refundAmount = refund.amount;\r
        refund.claimed = true;\r
        refund.amount = 0;\r
        \r
        (bool success, ) = payable(buyer).call{value: refundAmount}("");\r
        require(success, "Manual refund failed");\r
        \r
        emit ManualRefundProcessed(buyer, refundAmount);\r
    }\r
\r
    function getPendingRefundsTotal() external view returns (uint256) {\r
        uint256 total = 0;\r
        for (uint256 i = 0; i < failedRefundAddresses.length; i++) {\r
            FailedRefund storage refund = failedRefunds[failedRefundAddresses[i]];\r
            if (!refund.claimed && refund.amount > 0) {\r
                total += refund.amount;\r
            }\r
        }\r
        return total;\r
    }\r
\r
    // ============= Subscription Functions ============\r
    function createSubscriptionPlan(uint256 duration, uint256 price) external onlyOwner returns (uint256) {\r
        uint256 planId = nextPlanId++;\r
        subscriptionPlans[planId] = SubscriptionPlan({\r
            duration: duration,\r
            price: price,\r
            active: true\r
        });\r
        \r
        emit SubscriptionPlanCreated(planId, duration, price);\r
        return planId;\r
    }\r
\r
    function updateSubscriptionPlan(uint256 planId, uint256 duration, uint256 price, bool active) external onlyOwner {\r
        require(subscriptionPlans[planId].duration > 0, "Plan does not exist");\r
        \r
        subscriptionPlans[planId] = SubscriptionPlan({\r
            duration: duration,\r
            price: price,\r
            active: active\r
        });\r
        \r
        emit SubscriptionPlanUpdated(planId, duration, price, active);\r
    }\r
\r
    function subscribeToMedia(uint256 mediaId, uint256 planId) external payable nonReentrant {\r
        require(media[mediaId].owner != address(0), "Media does not exist");\r
        SubscriptionPlan storage plan = subscriptionPlans[planId];\r
        require(plan.active, "Subscription plan is not active");\r
        require(msg.value >= plan.price, "Insufficient payment for subscription");\r
        \r
        subscriptions[msg.sender][mediaId] = Subscription({\r
            subscriber: msg.sender,\r
            startTime: block.timestamp,\r
            endTime: block.timestamp + plan.duration,\r
            mediaId: mediaId,\r
            active: true\r
        });\r
        \r
        emit Subscribed(msg.sender, mediaId, planId, block.timestamp + plan.duration);\r
        \r
        // Refund excess payment\r
        if (msg.value > plan.price) {\r
            uint256 refundAmount = msg.value - plan.price;\r
            (bool success, ) = payable(msg.sender).call{value: refundAmount}("");\r
            require(success, "Refund failed");\r
        }\r
    }\r
\r
    function setAuthorizedReader(address reader, bool authorized) external onlyOwner {\r
        authorizedReaders[reader] = authorized;\r
        emit AuthorizedReaderUpdated(reader, authorized);\r
    }\r
\r
    // ============= Emergency & Admin Functions ============\r
    function initiateEmergencyWithdraw() external onlyOwner {\r
        emergencyWithdrawTimestamp = block.timestamp;\r
        emit EmergencyWithdrawInitiated(block.timestamp, block.timestamp + EMERGENCY_DELAY);\r
    }\r
\r
    function emergencyWithdraw() external onlyOwner nonReentrant {\r
        require(emergencyWithdrawTimestamp > 0, "Emergency withdraw not initiated");\r
        require(block.timestamp >= emergencyWithdrawTimestamp + EMERGENCY_DELAY, "Emergency delay not passed");\r
        \r
        uint256 balance = address(this).balance;\r
        emergencyWithdrawTimestamp = 0;\r
        \r
        (bool success, ) = payable(owner()).call{value: balance}("");\r
        require(success, "Emergency withdraw failed");\r
        \r
        emit EmergencyWithdrawExecuted(balance, block.timestamp);\r
    }\r
\r
    function withdrawTokens(address to, uint256 amount) external onlyOwner {\r
        require(to != address(0), "Invalid address");\r
        require(amount > 0, "Amount must be greater than 0");\r
        require(address(this).balance >= withdrawalFee, "Insufficient ETH for withdrawal fee");\r
        \r
        // Pay withdrawal fee\r
        (bool feeSuccess, ) = payable(feeReceiver).call{value: withdrawalFee}("");\r
        require(feeSuccess, "Fee payment failed");\r
        \r
        // Transfer ChainGallery tokens\r
        bool tokenSuccess = chainGalleryToken.transfer(to, amount);\r
        require(tokenSuccess, "Token transfer failed");\r
        \r
        emit TokensWithdrawn(to, amount, withdrawalFee);\r
    }\r
\r
    // ============= Utility Functions ============\r
    function getBuyers() external view returns (Buyer[] memory) {\r
        return buyers;\r
    }\r
\r
    function getFailedRefundAddresses() external view returns (address[] memory) {\r
        return failedRefundAddresses;\r
    }\r
\r
    function getFailedRefund(address buyer) external view returns (FailedRefund memory) {\r
        return failedRefunds[buyer];\r
    }\r
    //======================\r
    function getActiveBuyers() external view returns (Buyer[] memory) {\r
        uint256 activeCount = _getActiveBuyersCount();\r
        Buyer[] memory activeBuyers = new Buyer[](activeCount);\r
        uint256 index = 0;\r
\r
        for (uint256 i = 0; i < buyers.length; i++) {\r
            if (buyers[i].exists) {\r
                activeBuyers[index] = buyers[i];\r
                index++;\r
            }\r
        }\r
\r
        return activeBuyers;\r
    }\r
    function getMinimumDeposit() external view returns (uint256) {\r
        return askingPrice / 2;\r
    }\r
\r
    function getBuyersStats() external view returns (uint256 active, uint256 totalSlots) {\r
        uint256 activeCount = _getActiveBuyersCount();\r
        return (activeCount, buyers.length);\r
    }\r
    //=========================================\r
    function _hasValidSubscription(address subscriber, uint256 mediaId) internal view returns (bool) {\r
        Subscription storage sub = subscriptions[subscriber][mediaId];\r
        return sub.active && sub.endTime > block.timestamp;\r
    }\r
\r
    \r
    function hasAccess(address subscriber, uint256 mediaId) external view returns (bool) {\r
        // 1️⃣ Media must exist\r
        if (media[mediaId].owner == address(0)) {\r
            return false;\r
        }\r
\r
        // 2️⃣ Check all access methods\r
        if (\r
            subscriber == owner() ||\r
            subscriber == media[mediaId].owner ||\r
            _hasValidSubscription(subscriber, mediaId) || \r
            authorizedReaders[subscriber] ||\r
            hasGroupAccess(subscriber, mediaId)\r
        ) {\r
            return true;\r
        }\r
\r
        return false;\r
    }\r
// Add this function to your MediaOnChainStorage3 contract\r
function getSubscriptionInfo(address subscriber, uint256 mediaId) \r
    public \r
    view \r
    returns (\r
        address subscriber_,\r
        uint256 startTime_,\r
        uint256 endTime_,\r
        uint256 mediaId_,\r
        bool active_\r
    ) \r
{\r
    Subscription memory sub = subscriptions[subscriber][mediaId];\r
    return (\r
        sub.subscriber,\r
        sub.startTime,\r
        sub.endTime,\r
        sub.mediaId,\r
        sub.active\r
    );\r
}\r
\r
// Optional: Add a function to get all active subscriptions for a user\r
function getUserActiveSubscriptions(address subscriber) \r
    public \r
    view \r
    returns (uint256[] memory activeMediaIds) \r
{\r
    // This would require tracking user subscriptions, but for now we can return media IDs where user has active subscriptions\r
    // You might need to implement proper tracking for this\r
    uint256 mediaCount = getMediaCount();\r
    uint256 activeCount = 0;\r
    \r
    // First pass: count active subscriptions\r
    for (uint256 i = 0; i < mediaCount; i++) {\r
        if (subscriptions[subscriber][i].active && subscriptions[subscriber][i].endTime > block.timestamp) {\r
            activeCount++;\r
        }\r
    }\r
    \r
    // Second pass: populate array\r
    uint256[] memory mediaIds = new uint256[](activeCount);\r
    uint256 index = 0;\r
    for (uint256 i = 0; i < mediaCount; i++) {\r
        if (subscriptions[subscriber][i].active && subscriptions[subscriber][i].endTime > block.timestamp) {\r
            mediaIds[index] = i;\r
            index++;\r
        }\r
    }\r
    \r
    return mediaIds;\r
}\r
     function grantFreeSubscription(address subscriber, uint256 mediaId, uint256 duration) external onlyOwner {\r
    (address mediaOwner,,,,,,,) = getMedia(mediaId);\r
    require(mediaOwner != address(0), "Media does not exist");\r
    \r
    Subscription storage subscription = subscriptions[subscriber][mediaId];\r
    uint256 currentEndTime = subscription.endTime > block.timestamp ? subscription.endTime : block.timestamp;\r
    \r
    subscription.subscriber = subscriber;\r
    subscription.mediaId = mediaId;\r
    subscription.startTime = block.timestamp;\r
    subscription.endTime = currentEndTime + duration;\r
    subscription.active = true;\r
    \r
    emit Subscribed(subscriber, mediaId, 0, subscription.endTime);\r
}\r
//============================EXTRA============================\r
// Add these to your contract\r
struct GroupSubscription {\r
    uint256 groupId;\r
    string groupName;\r
    uint256[] mediaIds;\r
    uint256 price;\r
    uint256 duration;\r
    bool active;\r
    address owner;\r
}\r
\r
mapping(uint256 => GroupSubscription) public groupSubscriptions;\r
mapping(address => mapping(uint256 => uint256)) public groupSubscriptionExpiry; // user => groupId => expiryTime\r
uint256 public nextGroupId;\r
\r
// Events\r
event GroupSubscriptionCreated(uint256 groupId, string groupName, uint256 price, uint256 duration, address owner);\r
event GroupSubscribed(address subscriber, uint256 groupId, uint256 expiryTime);\r
\r
// Create a new group subscription (owner only)\r
function createGroupSubscription(\r
    string memory groupName,\r
    uint256[] memory mediaIds,\r
    uint256 price,\r
    uint256 duration\r
) public onlyOwner returns (uint256) {\r
    uint256 groupId = nextGroupId++;\r
    \r
    groupSubscriptions[groupId] = GroupSubscription({\r
        groupId: groupId,\r
        groupName: groupName,\r
        mediaIds: mediaIds,\r
        price: price,\r
        duration: duration,\r
        active: true,\r
        owner: msg.sender\r
    });\r
    \r
    emit GroupSubscriptionCreated(groupId, groupName, price, duration, msg.sender);\r
    return groupId;\r
}\r
\r
// Subscribe to a group\r
function subscribeToGroup(uint256 groupId) public payable {\r
    GroupSubscription storage group = groupSubscriptions[groupId];\r
    require(group.active, "Group subscription not active");\r
    require(msg.value >= group.price, "Insufficient payment");\r
    \r
    uint256 expiryTime = block.timestamp + group.duration;\r
    groupSubscriptionExpiry[msg.sender][groupId] = expiryTime;\r
    \r
    emit GroupSubscribed(msg.sender, groupId, expiryTime);\r
    \r
    // Refund excess payment\r
    if (msg.value > group.price) {\r
        payable(msg.sender).transfer(msg.value - group.price);\r
    }\r
}\r
\r
modifier canAccessMedia(uint256 mediaId) {\r
    require(media[mediaId].owner != address(0), "Media does not exist");\r
    \r
    address mediaOwner = media[mediaId].owner;\r
    bool hasAccess_ = (\r
        msg.sender == owner() || \r
        msg.sender == mediaOwner || \r
        _hasActiveSubscription(msg.sender, mediaId) ||\r
        authorizedReaders[msg.sender] ||\r
        _hasGroupAccess(msg.sender, mediaId)  // Use internal version for gas efficiency\r
    );\r
    \r
    require(hasAccess_, "No access to this media");\r
    _;\r
}\r
\r
// Internal version for modifier use (more gas efficient)\r
function _hasGroupAccess(address subscriber, uint256 mediaId) internal view returns (bool) {\r
    for (uint256 i = 0; i < nextGroupId; i++) {\r
        if (groupSubscriptions[i].active && groupSubscriptionExpiry[subscriber][i] > block.timestamp) {\r
            GroupSubscription storage group = groupSubscriptions[i];\r
            for (uint256 j = 0; j < group.mediaIds.length; j++) {\r
                if (group.mediaIds[j] == mediaId) {\r
                    return true;\r
                }\r
            }\r
        }\r
    }\r
    return false;\r
}\r
\r
// Keep the public version for external calls\r
function hasGroupAccess(address subscriber, uint256 mediaId) public view returns (bool) {\r
    return _hasGroupAccess(subscriber, mediaId);\r
}\r
\r
// Get user's active group subscriptions\r
function getUserGroupSubscriptions(address subscriber) public view returns (uint256[] memory activeGroups) {\r
    uint256 count = 0;\r
    \r
    // Count active groups\r
    for (uint256 i = 0; i < nextGroupId; i++) {\r
        if (groupSubscriptionExpiry[subscriber][i] > block.timestamp) {\r
            count++;\r
        }\r
    }\r
    \r
    // Populate array\r
    uint256[] memory groupIds = new uint256[](count);\r
    uint256 index = 0;\r
    for (uint256 i = 0; i < nextGroupId; i++) {\r
        if (groupSubscriptionExpiry[subscriber][i] > block.timestamp) {\r
            groupIds[index] = i;\r
            index++;\r
        }\r
    }\r
    \r
    return groupIds;\r
}\r
\r
// Get group details\r
function getGroupSubscription(uint256 groupId) public view returns (\r
    string memory groupName,\r
    uint256[] memory mediaIds,\r
    uint256 price,\r
    uint256 duration,\r
    bool active,\r
    uint256 mediaCount\r
) {\r
    GroupSubscription storage group = groupSubscriptions[groupId];\r
    return (\r
        group.groupName,\r
        group.mediaIds,\r
        group.price,\r
        group.duration,\r
        group.active,\r
        group.mediaIds.length\r
    );\r
}\r
\r
// Update group subscription (owner only)\r
function updateGroupSubscription(\r
    uint256 groupId,\r
    string memory groupName,\r
    uint256[] memory mediaIds,\r
    uint256 price,\r
    uint256 duration,\r
    bool active\r
) public onlyOwner {\r
    GroupSubscription storage group = groupSubscriptions[groupId];\r
    group.groupName = groupName;\r
    group.mediaIds = mediaIds;\r
    group.price = price;\r
    group.duration = duration;\r
    group.active = active;\r
}\r
//============================================================\r
\r
    // Accept ETH transfers\r
    receive() external payable {}\r
}"
    },
    "@openzeppelin/contracts/security/ReentrancyGuard.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be _NOT_ENTERED
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == _ENTERED;
    }
}
"
    },
    "@openzeppelin/contracts/token/ERC20/IERC20.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)

pragma solidity >=0.4.16;

/**
 * @dev Interface of the ERC-20 standard as defined in the ERC.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the value of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the value of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 value) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the
     * allowance mechanism. `value` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}
"
    },
    "@openzeppelin/contracts/access/Ownable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)

pragma solidity ^0.8.20;

import {Context} from "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * The initial owner is set to the address provided by the deployer. This can
 * later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    /**
     * @dev The caller account is not authorized to perform an operation.
     */
    error OwnableUnauthorizedAccount(address account);

    /**
     * @dev The owner is not a valid owner account. (eg. `address(0)`)
     */
    error OwnableInvalidOwner(address owner);

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
     */
    constructor(address initialOwner) {
        if (initialOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(initialOwner);
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        if (owner() != _msgSender()) {
            revert OwnableUnauthorizedAccount(_msgSender());
        }
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        if (newOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}
"
    },
    "@openzeppelin/contracts/utils/Context.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

pragma solidity ^0.8.20;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}
"
    }
  },
  "settings": {
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "remappings": []
  }
}}

Tags:
ERC20, Multisig, Multi-Signature, Factory|addr:0x17d171b1cd2dcec0e30d1aea4afc1e320cf8f7fa|verified:true|block:23686715|tx:0xba6158eb10280c0ca6e1fb871eebf48fb62aa45f3f91ba69961493aa27d83b53|first_check:1761823034

Submitted on: 2025-10-30 12:17:17

Comments

Log in to comment.

No comments yet.