// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
/**
* @title RoyaltyShareToken
* @dev Telif haklarını temsil eden ERC-20 token
*/
contract RoyaltyShareToken is ERC20 {
uint256 public immutable contentTokenId;
address public immutable contentContract;
constructor(
string memory name,
string memory symbol,
uint256 _contentTokenId,
address _contentContract,
uint256 initialSupply
) ERC20(name, symbol) {
contentTokenId = _contentTokenId;
contentContract = _contentContract;
_mint(msg.sender, initialSupply);
}
}
/**
* @title TransferableRoyaltyNFT
* @dev ERC-2981 ile telif ödemesi alan ve teliflerini tokenize edebilen NFT
*/
contract TransferableRoyaltyNFT is ERC721, ERC2981, Ownable, ReentrancyGuard {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
struct RoyaltyInfo {
address paymentSplitter;
address shareToken;
uint256 totalRoyaltiesEarned;
uint96 royaltyPercentage; // Basis points (500 = 5%)
bool isTokenized;
}
// Token ID => Royalty bilgileri
mapping(uint256 => RoyaltyInfo) public royaltyInfos;
// Token ID => Pending royalty payments
mapping(uint256 => uint256) public pendingRoyalties;
// Share token holders => Token ID => Claimable amount
mapping(address => mapping(uint256 => uint256)) public claimableRoyalties;
event RoyaltyPaid(uint256 indexed tokenId, address payer, uint256 amount);
event RoyaltyTokenized(uint256 indexed tokenId, address shareToken, uint256 totalShares);
event RoyaltyClaimed(uint256 indexed tokenId, address claimer, uint256 amount);
event RoyaltySharesTraded(uint256 indexed tokenId, address from, address to, uint256 amount);
constructor() ERC721("ContentRoyaltyNFT", "CRNFT") {}
/**
* @dev Yeni NFT mint et ve telif ayarlarını yap
* @param to Alıcı adres
* @param uri Metadata URI
* @param royaltyPercentage Telif yüzdesi (basis points)
*/
function mintWithRoyalty(
address to,
string memory uri,
uint96 royaltyPercentage
) public returns (uint256) {
require(royaltyPercentage <= 10000, "Royalty too high");
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(to, newTokenId);
_setTokenRoyalty(newTokenId, to, royaltyPercentage);
royaltyInfos[newTokenId] = RoyaltyInfo({
paymentSplitter: address(0),
shareToken: address(0),
totalRoyaltiesEarned: 0,
royaltyPercentage: royaltyPercentage,
isTokenized: false
});
return newTokenId;
}
/**
* @dev Telif haklarını tokenize et (hisse tokenları oluştur)
* @param tokenId NFT token ID
* @param totalShares Toplam hisse sayısı
*/
function tokenizeRoyalties(
uint256 tokenId,
uint256 totalShares
) public returns (address) {
require(ownerOf(tokenId) == msg.sender, "Not owner");
require(!royaltyInfos[tokenId].isTokenized, "Already tokenized");
require(totalShares > 0, "Invalid share amount");
// Payment splitter oluştur
RoyaltyPaymentSplitter splitter = new RoyaltyPaymentSplitter(
tokenId,
address(this)
);
// Hisse tokenları oluştur
string memory name = string(abi.encodePacked("Royalty Share #", toString(tokenId)));
string memory symbol = string(abi.encodePacked("RS", toString(tokenId)));
RoyaltyShareToken shareToken = new RoyaltyShareToken(
name,
symbol,
tokenId,
address(this),
totalShares
);
// Tüm hisseleri mint eden'e ver
// (shareToken constructor'da zaten mint ediyor)
// Royalty info güncelle
royaltyInfos[tokenId].paymentSplitter = address(splitter);
royaltyInfos[tokenId].shareToken = address(shareToken);
royaltyInfos[tokenId].isTokenized = true;
// ERC-2981 royalty receiver'ı splitter'a ayarla
_setTokenRoyalty(tokenId, address(splitter), royaltyInfos[tokenId].royaltyPercentage);
emit RoyaltyTokenized(tokenId, address(shareToken), totalShares);
return address(shareToken);
}
/**
* @dev NFT satışından telif ödemesi al
* @param tokenId NFT token ID
* @param salePrice Satış fiyatı
*/
function payRoyalty(uint256 tokenId, uint256 salePrice) external payable nonReentrant {
(address receiver, uint256 royaltyAmount) = royaltyInfo(tokenId, salePrice);
require(msg.value >= royaltyAmount, "Insufficient royalty payment");
require(receiver != address(0), "No royalty set");
RoyaltyInfo storage info = royaltyInfos[tokenId];
info.totalRoyaltiesEarned += royaltyAmount;
if (info.isTokenized) {
// Tokenize edilmiş - payment splitter'a gönder
pendingRoyalties[tokenId] += royaltyAmount;
payable(receiver).transfer(royaltyAmount);
} else {
// Normal - doğrudan sahibine
payable(receiver).transfer(royaltyAmount);
}
// Fazla ödemeyi iade et
if (msg.value > royaltyAmount) {
payable(msg.sender).transfer(msg.value - royaltyAmount);
}
emit RoyaltyPaid(tokenId, msg.sender, royaltyAmount);
}
/**
* @dev Hisse sahiplerinin telif gelirini claim etmesi
* @param tokenId NFT token ID
*/
function claimRoyaltyShare(uint256 tokenId) external nonReentrant {
RoyaltyInfo memory info = royaltyInfos[tokenId];
require(info.isTokenized, "Royalties not tokenized");
RoyaltyShareToken shareToken = RoyaltyShareToken(info.shareToken);
uint256 userShares = shareToken.balanceOf(msg.sender);
require(userShares > 0, "No shares owned");
RoyaltyPaymentSplitter splitter = RoyaltyPaymentSplitter(
payable(info.paymentSplitter)
);
uint256 claimable = splitter.getClaimableAmount(msg.sender);
require(claimable > 0, "No royalties to claim");
splitter.claim(msg.sender);
emit RoyaltyClaimed(tokenId, msg.sender, claimable);
}
/**
* @dev Hisse transferinde telif haklarını güncelle
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId,
uint256 batchSize
) internal virtual override {
super._beforeTokenTransfer(from, to, tokenId, batchSize);
// Eğer tokenize edilmişse, transfer yapılamaz
// Sadece hisse tokenları transfer edilebilir
if (royaltyInfos[tokenId].isTokenized && from != address(0)) {
revert("Cannot transfer tokenized NFT. Trade royalty shares instead.");
}
}
/**
* @dev Token URI (placeholder)
*/
function tokenURI(uint256 tokenId) public view override returns (string memory) {
require(_exists(tokenId), "Token does not exist");
return string(abi.encodePacked("ipfs://", toString(tokenId)));
}
/**
* @dev Utility function: uint256 to string
*/
function toString(uint256 value) internal pure returns (string memory) {
if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
// ERC-2981 ve ERC-721 interface desteği
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC2981)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
/**
* @title RoyaltyPaymentSplitter
* @dev Telif ödemelerini hisse sahiplerine dağıt
*/
contract RoyaltyPaymentSplitter is ReentrancyGuard {
uint256 public immutable tokenId;
address public immutable nftContract;
uint256 public totalReceived;
uint256 public totalReleased;
mapping(address => uint256) public released;
event PaymentReceived(address from, uint256 amount);
event PaymentReleased(address to, uint256 amount);
constructor(uint256 _tokenId, address _nftContract) {
tokenId = _tokenId;
nftContract = _nftContract;
}
receive() external payable {
totalReceived += msg.value;
emit PaymentReceived(msg.sender, msg.value);
}
/**
* @dev Kullanıcının claim edebileceği miktarı hesapla
*/
function getClaimableAmount(address account) public view returns (uint256) {
TransferableRoyaltyNFT nft = TransferableRoyaltyNFT(nftContract);
(,, address shareTokenAddr,,) = nft.royaltyInfos(tokenId);
RoyaltyShareToken shareToken = RoyaltyShareToken(shareTokenAddr);
uint256 totalShares = shareToken.totalSupply();
uint256 userShares = shareToken.balanceOf(account);
if (totalShares == 0 || userShares == 0) return 0;
uint256 totalPayment = (totalReceived * userShares) / totalShares;
uint256 alreadyReleased = released[account];
return totalPayment > alreadyReleased ? totalPayment - alreadyReleased : 0;
}
/**
* @dev Telif ödemesini claim et
*/
function claim(address account) external nonReentrant {
uint256 payment = getClaimableAmount(account);
require(payment > 0, "No payment due");
released[account] += payment;
totalReleased += payment;
payable(account).transfer(payment);
emit PaymentReleased(account, payment);
}
}