DreamNFTUpgradeSystem.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
interface IDreamNFT {
function ownerOf(uint256 tokenId) external view returns (address);
function levelUp(uint256 tokenId, string memory newImageURI) external;
function setCooldown(uint256 tokenId, uint256 newCooldown) external;
function dreamLevel(uint256 tokenId) external view returns (uint256);
function cooldownEnd(uint256 tokenId) external view returns (uint256);
}
interface IFragmentOfLucidity {
function ownerOf(uint256 tokenId) external view returns (address);
function isRedeemed(uint256 tokenId) external view returns (bool);
function markRedeemed(uint256 tokenId) external;
}
interface IBurnableERC20 {
function balanceOf(address account) external view returns (uint256);
function burn(address from, uint256 amount) external;
}
contract DreamNFTUpgradeSystem is
Initializable,
UUPSUpgradeable,
OwnableUpgradeable,
ReentrancyGuardUpgradeable
{
IDreamNFT public dreamNFT;
IFragmentOfLucidity public fragmentNFT;
IBurnableERC20 public timeToken; // for cooldown reductions
// Base cooldown (set in initialize)
uint256 public upgradeBaseCooldown;
uint256 public minCooldown;
// Challenge bonus: user => bonus seconds
mapping(address => uint256) public challengeBonuses;
// Winners pool: prize for the first to reach Level 5
uint256 public winnersPool;
address public firstOneiricWinner;
bool public hasFirstOneiricWinner;
uint256 public specialTokensAwarded;
uint256 public constant SPECIAL_TOKENS_COUNT = 9;
mapping(address => bool) public specialTokenHolders;
// Overridden cooldown if set
mapping(uint256 => uint256) public nextUpgradeTimestamp;
// Fusion fee in Wei. 5% of the fragment price can be set externally or a fixed amount.
uint256 public fusionFee;
// -------------- Events --------------
event NFTUpgraded(uint256 indexed tokenId, address indexed owner, uint256 newLevel, string newImageURI);
event UpgradeCooldownUpdated(uint256 indexed tokenId, uint256 newCooldownTimestamp);
event OneiricSovereignDeclared(address indexed winner, uint256 prizeAmount);
event OracleAcolyteAwarded(address indexed acolyte, uint256 acolyteNumber);
event ChallengeBonusUpdated(address indexed holder, uint256 bonus);
event CooldownReduced(uint256 indexed tokenId, uint256 reductionInSeconds);
event WinnersPoolDeposited(address indexed depositor, uint256 amount);
event ETHWithdrawn(address indexed recipient, uint256 amount);
event FusionFeeUpdated(uint256 newFee);
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(
address _dreamNFT,
address _fragmentNFT,
address _timeToken,
uint256 _upgradeBaseCooldown,
uint256 _minCooldown
) public initializer {
__Ownable_init(msg.sender);
__UUPSUpgradeable_init();
__ReentrancyGuard_init();
require(
_dreamNFT != address(0) &&
_fragmentNFT != address(0) &&
_timeToken != address(0),
"UpgradeSystem: Invalid addresses"
);
dreamNFT = IDreamNFT(_dreamNFT);
fragmentNFT = IFragmentOfLucidity(_fragmentNFT);
timeToken = IBurnableERC20(_timeToken);
upgradeBaseCooldown = _upgradeBaseCooldown; // e.g. 25 days
minCooldown = _minCooldown; // e.g. 1 day or so
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
// ---------------- Fusion Fee ----------------
/**
* @notice Set the required 5% fusion fee in Wei.
* Example: If a fragment is notionally 1 ETH, you'd set fusionFee = 0.05 ETH in Wei.
*/
function setFusionFee(uint256 newFee) external onlyOwner {
fusionFee = newFee;
emit FusionFeeUpdated(newFee);
}
// ---------------- Upgrade Logic ----------------
/**
* @notice Upgrade a DreamNFT by fusing a Fragment of Lucidity, paying a 5% ETH fee.
* @param tokenId The DreamNFT to upgrade.
* @param fragmentTokenId The Fragment to fuse.
* @param newImageURI Updated image or metadata URI after the upgrade.
*/
function upgradeNFT(
uint256 tokenId,
uint256 fragmentTokenId,
string memory newImageURI
) external payable nonReentrant {
require(dreamNFT.ownerOf(tokenId) == msg.sender, "UpgradeSystem: Not the NFT owner");
// Enforce fusion fee
require(fusionFee > 0, "UpgradeSystem: Fusion fee not set");
require(msg.value >= fusionFee, "UpgradeSystem: Insufficient ETH for fusion fee");
// Check if fragment is valid
require(fragmentNFT.ownerOf(fragmentTokenId) == msg.sender, "UpgradeSystem: Not the Fragment owner");
require(!fragmentNFT.isRedeemed(fragmentTokenId), "UpgradeSystem: Fragment already redeemed");
// Check cooldown
uint256 nftCooldown = nextUpgradeTimestamp[tokenId];
if (nftCooldown == 0) {
nftCooldown = dreamNFT.cooldownEnd(tokenId);
}
require(block.timestamp >= nftCooldown, "UpgradeSystem: Cooldown not over");
// Mark fragment as redeemed
fragmentNFT.markRedeemed(fragmentTokenId);
// Level up the NFT
dreamNFT.levelUp(tokenId, newImageURI);
// Send the fee to the contract owner (or you could direct it to winnersPool if desired)
(bool sent, ) = owner().call{value: msg.value}("");
require(sent, "UpgradeSystem: Fee transfer failed");
// Calculate new cooldown
uint256 newCooldown = block.timestamp + calculateCooldown(msg.sender);
nextUpgradeTimestamp[tokenId] = newCooldown;
dreamNFT.setCooldown(tokenId, newCooldown);
emit NFTUpgraded(tokenId, msg.sender, dreamNFT.dreamLevel(tokenId), newImageURI);
emit UpgradeCooldownUpdated(tokenId, newCooldown);
// If NFT is now level 5, handle special awarding
if (dreamNFT.dreamLevel(tokenId) == 5) {
if (!hasFirstOneiricWinner) {
hasFirstOneiricWinner = true;
firstOneiricWinner = msg.sender;
// Award entire winners pool
uint256 prizeAmount = winnersPool;
winnersPool = 0;
(bool success, ) = msg.sender.call{value: prizeAmount}("");
require(success, "UpgradeSystem: Prize transfer failed");
emit OneiricSovereignDeclared(msg.sender, prizeAmount);
}
else if (specialTokensAwarded < SPECIAL_TOKENS_COUNT && !specialTokenHolders[msg.sender]) {
specialTokensAwarded++;
specialTokenHolders[msg.sender] = true;
emit OracleAcolyteAwarded(msg.sender, specialTokensAwarded);
}
}
}
function declareWinner(address winner) private {
require(!hasFirstOneiricWinner, "Winner already declared");
require(winner != address(0), "Invalid winner address");
hasFirstOneiricWinner = true;
firstOneiricWinner = winner;
uint256 prizeAmount = winnersPool;
winnersPool = 0;
// Transfer prize with safety checks
require(prizeAmount > 0, "No prize to transfer");
(bool success, ) = payable(winner).call{value: prizeAmount}("");
require(success, "Prize transfer failed");
emit OneiricSovereignDeclared(winner, prizeAmount);
}
/**
* @notice Reduce the cooldown by burning Time Tokens.
* @param tokenId The DreamNFT token ID.
* @param timeTokenAmount The number of TimeTokens to burn (1 token = 1 hour = 3600 seconds).
*/
function reduceCooldown(uint256 tokenId, uint256 timeTokenAmount) external nonReentrant {
require(dreamNFT.ownerOf(tokenId) == msg.sender, "UpgradeSystem: Not NFT owner");
require(timeTokenAmount > 0, "UpgradeSystem: Zero time token amount");
uint256 reduction = timeTokenAmount * 3600;
uint256 currentCooldown = getRemainingCooldown(tokenId);
require(currentCooldown > 0, "UpgradeSystem: No active cooldown");
// Burn the tokens
timeToken.burn(msg.sender, timeTokenAmount);
// Calculate new cooldown
uint256 nftCooldown = nextUpgradeTimestamp[tokenId];
if (nftCooldown == 0) {
nftCooldown = dreamNFT.cooldownEnd(tokenId);
}
uint256 effectiveReduction = reduction > currentCooldown ? currentCooldown : reduction;
uint256 newCooldownTimestamp = nftCooldown - effectiveReduction;
// Ensure minCooldown is respected
if (block.timestamp < newCooldownTimestamp) {
uint256 remainAfterReduction = newCooldownTimestamp - block.timestamp;
if (remainAfterReduction < minCooldown) {
newCooldownTimestamp = block.timestamp + minCooldown;
}
} else {
// If we've overshot, set it to minCooldown from now
newCooldownTimestamp = block.timestamp + minCooldown;
}
nextUpgradeTimestamp[tokenId] = newCooldownTimestamp;
dreamNFT.setCooldown(tokenId, newCooldownTimestamp);
emit CooldownReduced(tokenId, effectiveReduction);
emit UpgradeCooldownUpdated(tokenId, newCooldownTimestamp);
}
// ---------------- Utility / Calculations ----------------
function getRemainingCooldown(uint256 tokenId) public view returns (uint256) {
uint256 nftCooldown = nextUpgradeTimestamp[tokenId];
if (nftCooldown == 0) {
nftCooldown = dreamNFT.cooldownEnd(tokenId);
}
return block.timestamp >= nftCooldown ? 0 : nftCooldown - block.timestamp;
}
/**
* @notice Calculate new cooldown for a user after upgrade, factoring in challenge bonuses.
*/
function calculateCooldown(address user) public view returns (uint256) {
uint256 bonus = challengeBonuses[user];
if (upgradeBaseCooldown > bonus) {
uint256 cd = upgradeBaseCooldown - bonus;
return cd < minCooldown ? minCooldown : cd;
}
return minCooldown;
}
// ---------------- Challenge Bonus ----------------
function setChallengeBonus(address user, uint256 bonus) external onlyOwner {
require(bonus <= upgradeBaseCooldown, "UpgradeSystem: Bonus exceeds base cooldown");
challengeBonuses[user] = bonus;
emit ChallengeBonusUpdated(user, bonus);
}
function setBatchChallengeBonuses(address[] calldata users, uint256[] calldata bonuses) external onlyOwner {
require(users.length == bonuses.length, "UpgradeSystem: Mismatched array lengths");
for (uint256 i = 0; i < users.length; i++) {
require(bonuses[i] <= upgradeBaseCooldown, "UpgradeSystem: Bonus exceeds base cooldown");
challengeBonuses[users[i]] = bonuses[i];
emit ChallengeBonusUpdated(users[i], bonuses[i]);
}
}
// ---------------- Winners Pool / Admin ----------------
function depositWinnersPool() external payable onlyOwner {
require(msg.value > 0, "UpgradeSystem: No ETH sent");
winnersPool += msg.value;
emit WinnersPoolDeposited(msg.sender, msg.value);
}
function withdrawETH(address recipient, uint256 amount) external onlyOwner {
require(amount <= address(this).balance, "UpgradeSystem: Insufficient balance");
(bool success, ) = recipient.call{value: amount}("");
require(success, "UpgradeSystem: Withdraw failed");
emit ETHWithdrawn(recipient, amount);
}
function withdrawETH() external onlyOwner nonReentrant {
uint256 balance = address(this).balance;
require(balance > 0, "No ETH to withdraw");
(bool success, ) = payable(owner()).call{value: balance}("");
require(success, "ETH withdrawal failed");
}
receive() external payable {}
}