// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import "@openzeppelin/contracts/interfaces/IERC5313.sol"; import "./interfaces/IVideoPayment.sol"; import "./libraries/VideoPaymentStorage.sol"; /** * @title VideoPayment * @dev Main contract for video content payment and access control */ contract VideoPayment is Initializable, UUPSUpgradeable, ReentrancyGuardUpgradeable, IERC5313, IVideoPayment { using VideoPaymentStorage for VideoPaymentStorage.VideoPaymentStorageStruct; // Storage VideoPaymentStorage.VideoPaymentStorageStruct private _storage; // Modifiers modifier onlyOwner() { if (msg.sender != _storage.owner) revert NotOwner(); _; } modifier onlyAdmin() { if (!_storage.isAdmin[msg.sender]) revert NotAdmin(); _; } modifier whenNotPaused() { if (_storage.paused) revert ContractPaused(); _; } modifier validContent(uint256 contentId) { if (!_storage.contentConfigs[contentId].isActive) revert InvalidContent(); _; } /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } /** * @dev Initialize the contract * @param initialOwner Initial owner address * @param initialAdmins Initial admin addresses array * @param supportedTokenAddresses Initial supported token addresses array */ function initialize( address initialOwner, address[] memory initialAdmins, address[] memory supportedTokenAddresses ) external initializer { if (initialOwner == address(0)) revert ZeroAddress(); __UUPSUpgradeable_init(); __ReentrancyGuard_init(); _storage.owner = initialOwner; _storage.version = 1; _storage.paused = false; // Add initial admins for (uint256 i = 0; i < initialAdmins.length; i++) { if (initialAdmins[i] != address(0)) { _storage.isAdmin[initialAdmins[i]] = true; emit AdminAdded(initialAdmins[i]); } } // Add initial supported tokens for (uint256 i = 0; i < supportedTokenAddresses.length; i++) { address token = supportedTokenAddresses[i]; if (token != address(0)) { // Add to enumeration structures if (!_storage.tokenExists[token]) { _storage.tokenExists[token] = true; _storage.tokenIndexes[token] = _storage .allSupportedTokens .length; _storage.allSupportedTokens.push(token); } _storage.supportedTokens[token] = true; emit SupportedTokenAdded(token); } } // Ensure native token (address(0)) is included in enumeration structures if (!_storage.tokenExists[address(0)]) { _storage.tokenExists[address(0)] = true; _storage.tokenIndexes[address(0)] = _storage .allSupportedTokens .length; _storage.allSupportedTokens.push(address(0)); } // Mark native token (address(0)) as always supported _storage.supportedTokens[address(0)] = true; } /** * @dev Get contract version */ function version() external view returns (uint256) { return _storage.version; } /** * @dev Migration function for future upgrades */ function migrate() external onlyOwner { // Initialize treasury if not set (for upgrade from version without treasury) if (_storage.treasury == address(0)) { _storage.treasury = payable(_storage.owner); emit TreasuryUpdated(address(0), _storage.owner); } } /** * @dev Authorize upgrade (required by UUPSUpgradeable) */ function _authorizeUpgrade( address newImplementation ) internal override onlyOwner {} // ============ Owner Management Functions ============ /** * @dev Get current owner address * @return Current owner address */ function owner() external view override(IERC5313, IVideoPayment) returns (address) { return _storage.owner; } /** * @dev Check if address is admin * @param account Address to check * @return Whether address is admin */ function isAdmin(address account) external view returns (bool) { return _storage.isAdmin[account]; } /** * @dev Add admin * @param admin Admin address to add */ function addAdmin(address admin) external onlyOwner { if (admin == address(0)) revert ZeroAddress(); _storage.isAdmin[admin] = true; emit AdminAdded(admin); } /** * @dev Remove admin * @param admin Admin address to remove */ function removeAdmin(address admin) external onlyOwner { if (admin == address(0)) revert ZeroAddress(); _storage.isAdmin[admin] = false; emit AdminRemoved(admin); } // ============ Treasury Management Functions ============ /** * @dev Set treasury address for receiving payments * @param newTreasury New treasury address */ function setTreasury(address payable newTreasury) external onlyOwner { if (newTreasury == address(0)) revert ZeroAddress(); address oldTreasury = _storage.treasury; _storage.treasury = newTreasury; emit TreasuryUpdated(oldTreasury, newTreasury); } /** * @dev Get current treasury address * @return Current treasury address */ function getTreasury() external view returns (address) { return _storage.treasury; } // ============ Content Management Functions ============ /** * @dev Set content configuration * @param contentId Content ID * @param token Token address (address(0) for native) * @param price Token price * @param viewCount View count * @param isActive Whether content is active */ function setContentConfig( uint256 contentId, address token, uint256 price, uint256 viewCount, bool isActive ) external onlyAdmin { if (!_storage.supportedTokens[token]) revert UnsupportedToken(); _storage.contentConfigs[contentId].tokenPrices[token] = price; _storage.contentConfigs[contentId].viewCount = viewCount; _storage.contentConfigs[contentId].isActive = isActive; // Sync with nativePrice for backward compatibility if (token == address(0)) { _storage.contentConfigs[contentId].nativePrice = price; } emit ContentConfigUpdated(contentId); } /** * @dev Batch set content configurations * @param contentIds Content ID array * @param token Token address (address(0) for native) * @param prices Price array * @param viewCounts View count array * @param isActiveArray Active status array */ function batchSetContentConfig( uint256[] memory contentIds, address token, uint256[] memory prices, uint256[] memory viewCounts, bool[] memory isActiveArray ) external onlyAdmin { if ( contentIds.length != prices.length || contentIds.length != viewCounts.length || contentIds.length != isActiveArray.length ) revert ArrayLengthMismatch(); if (!_storage.supportedTokens[token]) revert UnsupportedToken(); for (uint256 i = 0; i < contentIds.length; i++) { _storage.contentConfigs[contentIds[i]].tokenPrices[token] = prices[ i ]; _storage.contentConfigs[contentIds[i]].viewCount = viewCounts[i]; _storage.contentConfigs[contentIds[i]].isActive = isActiveArray[i]; // Sync with nativePrice for backward compatibility if (token == address(0)) { _storage.contentConfigs[contentIds[i]].nativePrice = prices[i]; } emit ContentConfigUpdated(contentIds[i]); } } /** * @dev Check if content is active * @param contentId Content ID * @return Whether content is active */ function isContentActive(uint256 contentId) external view returns (bool) { return _storage.contentConfigs[contentId].isActive; } // ============ Price Management Functions ============ /** * @dev Set content price for specific token (address(0) for native token) * @param contentId Content ID * @param token Token address (address(0) for native) * @param price New price */ function setContentPrice( uint256 contentId, address token, uint256 price ) external onlyAdmin { if (!_storage.supportedTokens[token]) revert UnsupportedToken(); _storage.contentConfigs[contentId].tokenPrices[token] = price; // Sync with nativePrice for backward compatibility if (token == address(0)) { _storage.contentConfigs[contentId].nativePrice = price; } emit TokenPriceUpdated(contentId, token, price); } /** * @dev Batch set content prices for multiple contents (address(0) for native token) * @param contentIds Content ID array * @param token Token address (address(0) for native) * @param prices Price array */ function batchSetContentPrice( uint256[] memory contentIds, address token, uint256[] memory prices ) external onlyAdmin { if (contentIds.length != prices.length) revert ArrayLengthMismatch(); if (!_storage.supportedTokens[token]) revert UnsupportedToken(); for (uint256 i = 0; i < contentIds.length; i++) { _storage.contentConfigs[contentIds[i]].tokenPrices[token] = prices[ i ]; // Sync with nativePrice for backward compatibility if (token == address(0)) { _storage.contentConfigs[contentIds[i]].nativePrice = prices[i]; } emit TokenPriceUpdated(contentIds[i], token, prices[i]); } } // ============ Payment Token Management Functions ============ /** * @dev Add supported token (address(0) for native token) * @param token Token address to add (address(0) for native) */ function addSupportedToken(address token) external onlyAdmin { if (!_storage.tokenExists[token]) { _storage.tokenExists[token] = true; _storage.tokenIndexes[token] = _storage.allSupportedTokens.length; _storage.allSupportedTokens.push(token); } _storage.supportedTokens[token] = true; emit SupportedTokenAdded(token); } /** * @dev Remove supported token (address(0) for native token) * @param token Token address to remove (address(0) for native) */ function removeSupportedToken(address token) external onlyAdmin { _storage.supportedTokens[token] = false; if (_storage.tokenExists[token]) { uint256 tokenIndex = _storage.tokenIndexes[token]; uint256 lastIndex = _storage.allSupportedTokens.length - 1; // Move the last token to the position of the token to remove if (tokenIndex != lastIndex) { address lastToken = _storage.allSupportedTokens[lastIndex]; _storage.allSupportedTokens[tokenIndex] = lastToken; _storage.tokenIndexes[lastToken] = tokenIndex; } // Remove the last element _storage.allSupportedTokens.pop(); delete _storage.tokenExists[token]; delete _storage.tokenIndexes[token]; } emit SupportedTokenRemoved(token); } /** * @dev Check if token is supported * @param token Token address to check * @return Whether token is supported */ function isSupportedToken(address token) external view returns (bool) { return _storage.supportedTokens[token]; } // ============ Token Enumeration Functions ============ /** * @dev Get all supported tokens * @return All supported token addresses */ function getAllSupportedTokens() external view returns (address[] memory) { return _storage.allSupportedTokens; } // ============ Purchase Functions ============ /** * @dev Purchase content with token (address(0) for native token) * @param contentId Content ID * @param paymentToken Payment token address (address(0) for native) * @param amount Payment amount */ function purchaseContent( uint256 contentId, address paymentToken, uint256 amount ) external payable nonReentrant whenNotPaused validContent(contentId) { if (!_storage.supportedTokens[paymentToken]) revert UnsupportedToken(); uint256 expectedPrice; if (paymentToken == address(0)) { // Native token payment expectedPrice = _storage.contentConfigs[contentId].nativePrice; if (msg.value != expectedPrice) revert InsufficientPayment(); if (amount != msg.value) revert InsufficientPayment(); // Transfer native token directly to treasury (bool success, ) = _storage.treasury.call{value: msg.value}(""); if (!success) revert TransferFailed(); } else { // ERC20 token payment expectedPrice = _storage.contentConfigs[contentId].tokenPrices[ paymentToken ]; if (expectedPrice != amount) revert InsufficientPayment(); // Transfer ERC20 token directly to treasury bool success = IERC20(paymentToken).transferFrom( msg.sender, _storage.treasury, amount ); if (!success) revert TransferFailed(); } // Create view permission _updateViewPermission(msg.sender, contentId); // Emit event - no more validUntil time emit ContentPurchased( msg.sender, contentId, paymentToken, amount, _storage.contentConfigs[contentId].viewCount ); } /** * @dev Batch purchase content with token (address(0) for native token) * @param contentIds Content ID array * @param paymentToken Payment token address (address(0) for native) * @param totalAmount Total payment amount */ function batchPurchase( uint256[] memory contentIds, address paymentToken, uint256 totalAmount ) external payable nonReentrant whenNotPaused { if (!_storage.supportedTokens[paymentToken]) revert UnsupportedToken(); uint256 expectedTotal = 0; for (uint256 i = 0; i < contentIds.length; i++) { if (!_storage.contentConfigs[contentIds[i]].isActive) revert InvalidContent(); if (paymentToken == address(0)) { expectedTotal += _storage .contentConfigs[contentIds[i]] .nativePrice; } else { expectedTotal += _storage .contentConfigs[contentIds[i]] .tokenPrices[paymentToken]; } } if (totalAmount != expectedTotal) revert InsufficientPayment(); if (paymentToken == address(0)) { // Native token payment if (msg.value != totalAmount) revert InsufficientPayment(); // Transfer native token directly to treasury (bool success, ) = _storage.treasury.call{value: msg.value}(""); if (!success) revert TransferFailed(); } else { // ERC20 token payment bool success = IERC20(paymentToken).transferFrom( msg.sender, _storage.treasury, totalAmount ); if (!success) revert TransferFailed(); } // Create view permissions for (uint256 i = 0; i < contentIds.length; i++) { _updateViewPermission(msg.sender, contentIds[i]); } // Emit event emit BatchPurchased(msg.sender, contentIds, paymentToken, totalAmount); } /** * @dev Purchase content with NFT * @param contentId Content ID * @param nftContract NFT contract address * @param tokenId NFT token ID */ function purchaseWithNFT( uint256 contentId, address nftContract, uint256 tokenId ) external nonReentrant whenNotPaused validContent(contentId) { // Check NFT ownership if (IERC721(nftContract).ownerOf(tokenId) != msg.sender) revert NoValidPermission(); // Transfer NFT (burn it by transferring to this contract) IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId); // Create view permission _updateViewPermission(msg.sender, contentId); // Emit event emit NFTPurchased(msg.sender, contentId, nftContract, tokenId); } /** * @dev Internal function to update view permission * @param user User address * @param contentId Content ID */ function _updateViewPermission(address user, uint256 contentId) internal { VideoPaymentStorage.ViewPermission storage existing = _storage .userPermissions[user][contentId]; uint256 viewCount = _storage.contentConfigs[contentId].viewCount; if (viewCount == 0) { // One-time purchase: Check if already purchased if (existing.isValid) { // User already has valid permission, no need to do anything // This allows the purchase to succeed without duplicate payment return; } // Create new permission for unlimited viewing existing.purchaseTime = block.timestamp; existing.remainingViews = 0; // 0 means unlimited existing.isValid = true; } else { // View-count based: Add to existing or create new if (existing.isValid) { // Existing permission is still valid, accumulate view count existing.remainingViews += viewCount; emit ViewCountAdded( user, contentId, viewCount, existing.remainingViews ); } else { // No existing permission, create new existing.purchaseTime = block.timestamp; existing.remainingViews = viewCount; existing.isValid = true; } } } // ============ Permission Verification Functions ============ /** * @dev Check if user has valid view permission * @param user User address * @param contentId Content ID * @return Whether user has valid permission */ function hasViewPermission( address user, uint256 contentId ) external view returns (bool) { VideoPaymentStorage.ViewPermission memory permission = _storage .userPermissions[user][contentId]; if (!permission.isValid) { return false; } // Get view count to determine content type uint256 viewCount = _storage.contentConfigs[contentId].viewCount; if (viewCount == 0) { // One-time purchase: always valid if permission exists return true; } else { // View-count based: valid if there are remaining views return permission.remainingViews > 0; } } /** * @dev Get user permissions for multiple contents * @param user User address * @param contentIds Content ID array * @return Permission array */ function getUserPermissions( address user, uint256[] memory contentIds ) external view returns (VideoPaymentStorage.ViewPermission[] memory) { VideoPaymentStorage.ViewPermission[] memory permissions = new VideoPaymentStorage.ViewPermission[]( contentIds.length ); for (uint256 i = 0; i < contentIds.length; i++) { permissions[i] = _storage.userPermissions[user][contentIds[i]]; } return permissions; } /** * @dev Get detailed permission info * @param user User address * @param contentId Content ID * @return Permission details */ function getPermissionDetails( address user, uint256 contentId ) external view returns (VideoPaymentStorage.ViewPermission memory) { return _storage.userPermissions[user][contentId]; } /** * @dev Consume one view for user * @param user User address * @param contentId Content ID * @return Success status */ function consumeView( address user, uint256 contentId ) external onlyAdmin returns (bool) { VideoPaymentStorage.ViewPermission storage permission = _storage .userPermissions[user][contentId]; if (!permission.isValid) { revert NoValidPermission(); } // Get view count to determine if this is unlimited or count-based uint256 viewCount = _storage.contentConfigs[contentId].viewCount; if (viewCount == 0) { // One-time purchase with unlimited viewing - don't decrement emit ViewConsumed(user, contentId, 0); // 0 indicates unlimited return true; } else { // View-count based purchase - check and decrement if (permission.remainingViews == 0) { revert NoValidPermission(); } // Consume one view permission.remainingViews--; emit ViewConsumed(user, contentId, permission.remainingViews); // Mark as invalid if no views remaining if (permission.remainingViews == 0) { permission.isValid = false; emit PermissionExpired(user, contentId); } return true; } } /** * @dev Batch consume views for multiple users * @param users User address array * @param contentIds Content ID array * @return Success status array */ function batchConsumeView( address[] memory users, uint256[] memory contentIds ) external onlyAdmin returns (bool[] memory) { if (users.length != contentIds.length) revert ArrayLengthMismatch(); bool[] memory results = new bool[](users.length); for (uint256 i = 0; i < users.length; i++) { VideoPaymentStorage.ViewPermission storage permission = _storage .userPermissions[users[i]][contentIds[i]]; if (!permission.isValid) { results[i] = false; continue; } // Get view count to determine if this is unlimited or count-based uint256 viewCount = _storage .contentConfigs[contentIds[i]] .viewCount; if (viewCount == 0) { // One-time purchase with unlimited viewing - don't decrement emit ViewConsumed(users[i], contentIds[i], 0); // 0 indicates unlimited results[i] = true; } else { // View-count based purchase - check and decrement if (permission.remainingViews == 0) { results[i] = false; continue; } // Consume one view permission.remainingViews--; emit ViewConsumed( users[i], contentIds[i], permission.remainingViews ); // Mark as invalid if no views remaining if (permission.remainingViews == 0) { permission.isValid = false; emit PermissionExpired(users[i], contentIds[i]); } results[i] = true; } } return results; } // ============ Content Query Functions ============ /** * @dev Get content purchase information * @param contentId Content ID * @return Purchase information including supported tokens and prices */ function getContentPurchaseInfo( uint256 contentId ) external view returns (IVideoPayment.ContentPurchaseInfo memory) { VideoPaymentStorage.ContentConfig storage config = _storage .contentConfigs[contentId]; // Get supported tokens for this content ( address[] memory supportedTokens, uint256[] memory prices ) = _getSupportedTokensForContent(contentId); return IVideoPayment.ContentPurchaseInfo({ viewCount: config.viewCount, isActive: config.isActive, supportedTokens: supportedTokens, tokenPrices: prices, isUnlimitedViewing: config.viewCount == 0 }); } /** * @dev Internal function to get supported tokens and prices for content * @param contentId Content ID * @return supportedTokens Array of supported token addresses * @return prices Array of corresponding prices */ function _getSupportedTokensForContent( uint256 contentId ) internal view returns (address[] memory supportedTokens, uint256[] memory prices) { // Count supported tokens with prices set for this content uint256 count = 0; uint256 totalTokens = _storage.allSupportedTokens.length; // Count tokens that have prices set and are supported for (uint256 i = 0; i < totalTokens; i++) { address token = _storage.allSupportedTokens[i]; if (_storage.supportedTokens[token]) { uint256 price; if (token == address(0)) { price = _storage.contentConfigs[contentId].nativePrice; } else { price = _storage.contentConfigs[contentId].tokenPrices[ token ]; } if (price > 0) { count++; } } } // Create arrays with exact size supportedTokens = new address[](count); prices = new uint256[](count); uint256 index = 0; // Fill arrays with tokens that have prices set for (uint256 i = 0; i < totalTokens; i++) { address token = _storage.allSupportedTokens[i]; if (_storage.supportedTokens[token]) { uint256 price; if (token == address(0)) { price = _storage.contentConfigs[contentId].nativePrice; } else { price = _storage.contentConfigs[contentId].tokenPrices[ token ]; } if (price > 0) { supportedTokens[index] = token; prices[index] = price; index++; } } } } // ============ Emergency Control Functions ============ /** * @dev Pause contract */ function pause() external onlyOwner { _storage.paused = true; emit Paused(); } /** * @dev Unpause contract */ function unpause() external onlyOwner { _storage.paused = false; emit Unpaused(); } }