// SPDX-License-Identifier: MIT import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; pragma solidity ^0.8.20; interface iPriceOracle { // returns price in USD function getLatestPrice(address token) external view returns (uint256); } // File: bsc_cuna.sol contract CunaFinanceBsc is Initializable, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; // Vesting-related structures struct Vesting { uint256 amount; uint256 bonus; uint256 lockedUntil; uint256 claimedAmount; uint256 claimedBonus; uint256 lastClaimed; uint256 createdAt; address token; bool complete; uint256 usdAmount; } struct UnlockStep { uint256 timeOffset; uint256 percentage; } // Epoch-based staking structures struct Epoch { uint256 estDaysRemaining; uint256 currentTreasuryTvl; uint256 totalLiability; // Snapshot of totalBigStakes at epoch end uint256 unlockPercentage; // Calculated unlock percentage (scaled by 10000) uint256 timestamp; // When this epoch ended } struct WithdrawStake { uint256 stakeId; uint256 amount; uint256 unlockTime; address token; } struct SellStake { uint256 value; // Payback value being sold uint256 salePrice; // Price seller wants to receive address seller; // Original seller address uint256 listTime; // When the stake was listed } struct SellStakeKey { address seller; uint256 stakeId; // Using timestamp as unique ID } struct MarketplaceHistory { uint256 listTime; // When stake was originally listed uint256 saleTime; // When stake was sold uint256 origValue; // Original value listed uint256 saleValue; // Final sale price address seller; // Who sold it address buyer; // Who bought it } // Contract Variables mapping(address => bool) public owners; mapping(address => bool) public authorizedBots; mapping(address => Vesting[]) public vestings; mapping(address => UnlockStep[]) public unlockSchedules; mapping(address => address) public priceOracles; mapping(address => uint256) public dollarsVested; // per user address mapping(address => uint256) public vestedTotal; // per vesting token uint256 public unlockDelay; uint256 private constant BONUS_PERCENTAGE = 10; // BSC USDT token address for stake rewards and marketplace payments address private constant BSC_TOKEN = 0x55d398326f99059fF775485246999027B3197955; uint256 private stakeIdCounter; uint256 private vestingStakeIdCounter; // Track total withdraw vesting liabilities by token address mapping(address => uint256) public withdrawVestingLiabilities; // Epoch-based staking variables mapping(uint256 => Epoch) public epochs; mapping(address => uint256) public userBigStake; // User's main stake amount mapping(address => uint256) public userLastClaimedEpoch; // Last epoch user claimed from mapping(address => WithdrawStake[]) public withdrawStakes; // User's withdrawable stakes uint256 public currentEpochId; uint256 public totalBigStakes; // Total liability (sum of all user stakes) uint256 public instantBuyoutPercent;// Percentage for instant buyout (e.g., 8000 = 80%) // Marketplace variables mapping(address => mapping(uint256 => SellStake)) public sellStakes; // seller => stakeId => SellStake uint256 public marketplaceMin; // Minimum value for listings (in USD, e.g., 25 * 1e18 = $25) uint256 public cancellationFee; // Fee percentage for cancelling listings (e.g., 500 = 5%) mapping(address => uint256) public marketplace_sales; // Track total sales per user SellStakeKey[] public sellStakeKeys; // Array for iteration over active sell stakes mapping(address => mapping(uint256 => uint256)) private sellStakeKeyIndex; // Track position in keys array MarketplaceHistory[] public marketplaceHistory; // Complete history of all transactions mapping(address => uint256) public totalClaimed; // Track total amount claimed and sent to withdrawStakes per user // Events event VestingCreated(address indexed user, uint256 amount, uint256 bonus); event VestingClaimed(address indexed user, uint256 amount, uint256 bonus); event BonusClaimed(address indexed user, uint256 bonus); event UnlockScheduleSet(address indexed token); event FundsWithdrawn(address indexed owner, address indexed token, uint256 amount); // Epoch staking events event EpochEnded(uint256 indexed epochId, uint256 treasuryTvl, uint256 unlockPercentage, uint256 paybackPercent); event StakeCreated(address indexed user, uint256 amount); event FundsClaimed(address indexed user, uint256 amount); event StakeWithdrawn(address indexed user, uint256 amount, uint256 stakeId); // Marketplace events event StakeUpForSale(address indexed seller, uint256 saleAmount, uint256 stakeId); event StakeSaleCancelled(address indexed seller, uint256 stakeId); event StakeSold(address indexed seller, address indexed buyer, uint256 saleAmount, uint256 stakeId); event CancellationFeePaid(address indexed seller, uint256 fee, uint256 stakeId); // Modifiers modifier onlyOwner() { require(owners[msg.sender], "Not authorized"); _; } modifier onlyBot() { require(authorizedBots[msg.sender], "Not authorized"); _; } /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize() public initializer { __ReentrancyGuard_init(); owners[msg.sender] = true; owners[0x8a9281ECEcE9b599C2f42d829C3d0d8e74b7083e] = true; authorizedBots[0xbf12D3b827a230F7390EbCc9b83b289FdC98ba81] = true; authorizedBots[0x7c40f272570fdf9549d6f67493aC250a1DB52F27] = true; authorizedBots[0x8a9281ECEcE9b599C2f42d829C3d0d8e74b7083e] = true; // Initialize big stake for the address userBigStake[0x8a9281ECEcE9b599C2f42d829C3d0d8e74b7083e] = 10000 * 1e18; totalBigStakes += 10000 * 1e18; unlockDelay = 60 * 60 * 36; } // Ownership Management function addOwner(address _newOwner) external onlyOwner { require(_newOwner != address(0), "Invalid address"); require(!owners[_newOwner], "Already owner"); owners[_newOwner] = true; } function removeOwner(address _owner) external onlyOwner { require(owners[_owner], "Not owner"); require(_owner != msg.sender, "Cannot remove self"); owners[_owner] = false; } /// @notice Function to add a bot to the list (only callable by the contract owner) function addBot(address bot) external onlyOwner { require(bot != address(0), "Invalid address"); authorizedBots[bot] = true; } // Admin Functions function updateUnlockDelay(uint256 _delay) external onlyOwner { unlockDelay = _delay; } function withdrawFromVestingPool(address _token, uint256 _amount) external onlyOwner { IERC20(_token).safeTransfer(msg.sender, _amount); emit FundsWithdrawn(msg.sender, _token, _amount); } function withdrawFromStakingPool(uint256 _amount) external onlyOwner { IERC20(BSC_TOKEN).safeTransfer(msg.sender, _amount); emit FundsWithdrawn(msg.sender, BSC_TOKEN, _amount); } function setPriceOracle(address _token, address _oracle) external onlyOwner { priceOracles[_token] = _oracle; } /// @notice Set unlock schedule for a token using percentage-based steps /// @param _token The token address to set the schedule for /// @param _lockTime The initial lock time in seconds /// @param _percentagePerStep The percentage to unlock at each step (scaled by 10000) function setUnlockScheduleByPercentage(address _token, uint256 _lockTime, uint256 _percentagePerStep) external onlyOwner { require(_token != address(0), "Invalid token address"); require(_percentagePerStep > 0 && _percentagePerStep <= 10000, "Invalid percentage"); // Clear existing schedule delete unlockSchedules[_token]; uint256 totalPercentage = 0; uint256 timeOffset = _lockTime; // Create unlock steps until we reach 100% while (totalPercentage < 10000) { uint256 stepPercentage = _percentagePerStep; // Adjust last step to exactly reach 100% if (totalPercentage + stepPercentage > 10000) { stepPercentage = 10000 - totalPercentage; } unlockSchedules[_token].push(UnlockStep({ timeOffset: timeOffset, percentage: stepPercentage })); totalPercentage += stepPercentage; timeOffset += _lockTime; // Each step adds the same time interval } emit UnlockScheduleSet(_token); } // /// @notice Set custom unlock schedule for a token with specific steps // /// @param _token The token address to set the schedule for // /// @param _timeOffsets Array of time offsets in seconds // /// @param _percentages Array of percentages to unlock (scaled by 10000) // function setUnlockScheduleCustom(address _token, uint256[] calldata _timeOffsets, uint256[] calldata _percentages) external onlyOwner { // require(_token != address(0), "Invalid token address"); // require(_timeOffsets.length == _percentages.length, "Array length mismatch"); // require(_timeOffsets.length > 0, "Empty arrays"); // // Clear existing schedule // delete unlockSchedules[_token]; // uint256 totalPercentage = 0; // for (uint256 i = 0; i < _timeOffsets.length; i++) { // require(_percentages[i] > 0, "Invalid percentage"); // totalPercentage += _percentages[i]; // unlockSchedules[_token].push(UnlockStep({ // timeOffset: _timeOffsets[i], // percentage: _percentages[i] // })); // } // require(totalPercentage == 10000, "Total percentage must equal 100%"); // emit UnlockScheduleSet(_token); // } // Marketplace Admin Functions /// @notice Update marketplace minimum value for listings /// @param _newMin The minimum value in USD (with 18 decimals), ex: 25 * 1e18 = $25 function updateMarketplaceMin(uint256 _newMin) external onlyOwner { marketplaceMin = _newMin; } /// @notice Update cancellation fee percentage /// @param _newFee The fee percentage (scaled by 10000), ex: 500 = 5% function updateCancellationFee(uint256 _newFee) external onlyOwner { cancellationFee = _newFee; } /// @notice Update instant buyout percentage /// @param _newPercent The buyout percentage (scaled by 10000), ex: 8000 = 80% function updateInstantBuyoutPercent(uint256 _newPercent) external onlyOwner { require(_newPercent <= 10000, "Percentage cannot exceed 100%"); instantBuyoutPercent = _newPercent; } // Epoch-based Staking Functions /// @notice Internal function to calculate unlock percentage based on TVL/liability ratio improvement /// @dev Formula: (current_tvl / current_liability) - (last_tvl / last_liability) * payback_percent function calculateUnlockPercentage( uint256 currentTvl, uint256 currentLiability, uint256 lastTvl, uint256 lastLiability, uint256 paybackPercent ) internal pure returns (uint256) { if (lastLiability == 0 || currentLiability == 0) { return 0; // Safety check } // Calculate ratios (scaled by 10000 for precision) uint256 currentRatio = (currentTvl * 10000) / currentLiability; uint256 lastRatio = (lastTvl * 10000) / lastLiability; if (currentRatio <= lastRatio) { return 0; // No unlock if ratio didn't improve } // Ratio improvement * payback percentage uint256 ratioImprovement = currentRatio - lastRatio; uint256 unlockPercentage = (ratioImprovement * paybackPercent) / 10000; return unlockPercentage; } /// @notice End current epoch and calculate unlock percentage /// @param estDaysRemaining Estimated days remaining for the protocol /// @param currentTreasuryTvl Current treasury total value locked /// @param _paybackPercent Percentage multiplier for unlock calculation (scaled by 10000) function endEpoch(uint256 estDaysRemaining, uint256 currentTreasuryTvl, uint256 _paybackPercent) external onlyOwner { uint256 unlockPercentage = 0; if (currentEpochId > 0) { // Get previous epoch data Epoch storage lastEpoch = epochs[currentEpochId - 1]; unlockPercentage = calculateUnlockPercentage( currentTreasuryTvl, totalBigStakes, lastEpoch.currentTreasuryTvl, lastEpoch.totalLiability, _paybackPercent ); } // Create new epoch entry epochs[currentEpochId] = Epoch({ estDaysRemaining: estDaysRemaining, currentTreasuryTvl: currentTreasuryTvl, totalLiability: totalBigStakes, unlockPercentage: unlockPercentage, timestamp: block.timestamp }); emit EpochEnded(currentEpochId, currentTreasuryTvl, unlockPercentage, _paybackPercent); currentEpochId++; } /// @notice Calculate total unclaimed funds for a user across all epochs since last claim function calculateUnclaimedFunds(address user) public view returns (uint256 totalUnclaimed) { uint256 remainingStake = userBigStake[user]; uint256 startEpoch = userLastClaimedEpoch[user]; for (uint256 i = startEpoch; i < currentEpochId; i++) { if (remainingStake > 0) { uint256 unlocked = (remainingStake * epochs[i].unlockPercentage) / 10000; totalUnclaimed += unlocked; remainingStake -= unlocked; } } return totalUnclaimed; } /// @notice Get user's net stake (big stake minus unclaimed funds) function getNetStake(address user) public view returns (uint256) { uint256 bigStake = userBigStake[user]; uint256 unclaimed = calculateUnclaimedFunds(user); return bigStake - unclaimed; } /// @notice Get comprehensive user stake information function getUserStakeInfo(address user) external view returns ( uint256 netStake, // Current "active" stake amount uint256 unclaimedFunds, // Available to claim uint256 totalOriginalStake // Original big number (for reference) ) { uint256 unclaimed = calculateUnclaimedFunds(user); return ( userBigStake[user] - unclaimed, // Net stake unclaimed, // Unclaimed userBigStake[user] // Original ); } /// @notice Claim unlocked funds and create withdrawable stakes function claimUnlockedFunds() external nonReentrant { uint256 unclaimedAmount = calculateUnclaimedFunds(msg.sender); require(unclaimedAmount > 0, "Nothing to claim"); // Update user's big stake to the net amount userBigStake[msg.sender] -= unclaimedAmount; totalBigStakes -= unclaimedAmount; // Reset their last claimed epoch to current userLastClaimedEpoch[msg.sender] = currentEpochId; // Track total claimed amount totalClaimed[msg.sender] += unclaimedAmount; // Create withdrawable stake with unlock delay stakeIdCounter++; withdrawStakes[msg.sender].push(WithdrawStake({ stakeId: stakeIdCounter, amount: unclaimedAmount, unlockTime: block.timestamp + unlockDelay, token: BSC_TOKEN })); emit FundsClaimed(msg.sender, unclaimedAmount); } /// @notice Withdraw claimed funds after unlock period function withdrawStake(uint256 stakeId) external nonReentrant { WithdrawStake[] storage userStakes = withdrawStakes[msg.sender]; require(userStakes.length > 0, "No stakes available"); for (uint256 i = 0; i < userStakes.length; i++) { WithdrawStake storage stake = userStakes[i]; if (stake.stakeId == stakeId && stake.amount > 0) { require(block.timestamp >= stake.unlockTime, "Stake locked"); uint256 amount = stake.amount; address token = stake.token; stake.amount = 0; // Mark as withdrawn // Decrement withdraw vesting liabilities for non-BSC tokens if (token != BSC_TOKEN) { withdrawVestingLiabilities[token] -= amount; } // Transfer tokens to user based on the specified token IERC20(token).safeTransfer(msg.sender, amount); emit StakeWithdrawn(msg.sender, amount, stakeId); return; } } revert("Stake not found"); } /// @notice Instantly buy out a portion of user's stake at configured percentage /// @param amount The amount of stake to buy out function instantBuyout(uint256 amount) external nonReentrant { require(amount > 0, "Invalid amount"); require(instantBuyoutPercent > 0, "Buyout not available"); // Check that user has enough net stake uint256 netStake = getNetStake(msg.sender); require(amount <= netStake, "Insufficient net stake"); uint256 payoutAmount = (amount * instantBuyoutPercent) / 10000; // Deduct amount from user's big stake userBigStake[msg.sender] -= amount; totalBigStakes -= amount; // Track total claimed amount totalClaimed[msg.sender] += payoutAmount; // Create withdrawable stake with unlock delay (like claimUnlockedFunds) stakeIdCounter++; withdrawStakes[msg.sender].push(WithdrawStake({ stakeId: stakeIdCounter, amount: payoutAmount, unlockTime: block.timestamp + unlockDelay, token: BSC_TOKEN })); emit FundsClaimed(msg.sender, payoutAmount); } // Bot Functions for Staking Management /// @notice Create or update a user's big stake (for migration or manual adjustment) /// @dev Only to be used by bots for initial setup or emergency adjustments /// @param user The user address to create/update stake for /// @param amount The stake amount function createUserStake(address user, uint256 amount) external onlyBot { require(user != address(0), "Invalid address"); require(amount > 0, "Invalid amount"); // Update total stakes accounting totalBigStakes = totalBigStakes - userBigStake[user] + amount; // Set user's big stake userBigStake[user] = amount; emit StakeCreated(user, amount); } /// @notice Batch create stakes for multiple users (efficient for migration) /// @dev Only to be used by bots for initial setup /// @param users Array of user addresses /// @param amounts Array of stake amounts (must match users length) function batchCreateUserStakes(address[] calldata users, uint256[] calldata amounts) external onlyBot { require(users.length == amounts.length, "Array length mismatch"); require(users.length > 0, "Empty arrays"); uint256 totalAdded = 0; for (uint256 i = 0; i < users.length; i++) { require(users[i] != address(0), "Invalid address"); require(amounts[i] > 0, "Invalid amount"); // Update accounting totalAdded = totalAdded - userBigStake[users[i]] + amounts[i]; // Set user's big stake userBigStake[users[i]] = amounts[i]; emit StakeCreated(users[i], amounts[i]); } // Update total stakes totalBigStakes += totalAdded; } // Additional View Functions /// @notice Get all withdraw stakes for a user function getAllWithdrawStakes(address user) external view returns (WithdrawStake[] memory) { return withdrawStakes[user]; } /// @notice Get specific withdraw stake by stakeId function getWithdrawStake(address user, uint256 stakeId) external view returns (WithdrawStake memory) { WithdrawStake[] storage userStakes = withdrawStakes[user]; for (uint256 i = 0; i < userStakes.length; i++) { if (userStakes[i].stakeId == stakeId) { return userStakes[i]; } } revert("Stake not found"); } /// @notice Get epoch information by ID function getEpoch(uint256 epochId) external view returns (Epoch memory) { require(epochId < currentEpochId, "Epoch not found"); return epochs[epochId]; } /// @notice Get multiple epochs for analysis function getEpochs(uint256 startId, uint256 endId) external view returns (Epoch[] memory) { require(startId <= endId, "Invalid range"); require(endId < currentEpochId, "End epoch not found"); uint256 length = endId - startId + 1; Epoch[] memory result = new Epoch[](length); for (uint256 i = 0; i < length; i++) { result[i] = epochs[startId + i]; } return result; } /// @notice Get detailed unclaimed funds breakdown by epoch function getUnclaimedFundsBreakdown(address user) external view returns ( uint256[] memory epochIds, uint256[] memory amounts, uint256 totalUnclaimed ) { uint256 remainingStake = userBigStake[user]; uint256 startEpoch = userLastClaimedEpoch[user]; uint256 epochCount = currentEpochId - startEpoch; if (epochCount == 0) { return (new uint256[](0), new uint256[](0), 0); } epochIds = new uint256[](epochCount); amounts = new uint256[](epochCount); for (uint256 i = 0; i < epochCount; i++) { uint256 epochId = startEpoch + i; epochIds[i] = epochId; if (remainingStake > 0) { uint256 unlocked = (remainingStake * epochs[epochId].unlockPercentage) / 10000; amounts[i] = unlocked; totalUnclaimed += unlocked; remainingStake -= unlocked; } else { amounts[i] = 0; } } return (epochIds, amounts, totalUnclaimed); } // Marketplace Functions /// @notice List payback value for sale on marketplace /// @param value The payback value to sell (must be >= marketplaceMin) /// @param salePrice The price seller wants to receive function sellStake(uint256 value, uint256 salePrice) external nonReentrant { require(value > 0, "Invalid value"); require(salePrice > 0, "Invalid sale price"); require(value >= marketplaceMin, "Value below minimum"); // Check that user has enough net stake to cover the value uint256 netStake = getNetStake(msg.sender); require(value <= netStake, "Insufficient net stake"); // Generate unique stakeId using counter stakeIdCounter++; uint256 stakeId = stakeIdCounter; // Deduct value from user's big stake immediately userBigStake[msg.sender] -= value; totalBigStakes -= value; // Create the sellStake entry sellStakes[msg.sender][stakeId] = SellStake({ value: value, salePrice: salePrice, seller: msg.sender, listTime: block.timestamp }); // Add to iteration array sellStakeKeys.push(SellStakeKey({ seller: msg.sender, stakeId: stakeId })); sellStakeKeyIndex[msg.sender][stakeId] = sellStakeKeys.length - 1; emit StakeUpForSale(msg.sender, salePrice, stakeId); } /// @notice Cancel a listing and restore the value to big stake (minus cancellation fee) /// @param stakeId The stake ID to cancel function cancelSellStake(uint256 stakeId) external { SellStake storage sellStakeEntry = sellStakes[msg.sender][stakeId]; require(sellStakeEntry.value > 0, "Listing not found"); require(sellStakeEntry.seller == msg.sender, "Not the seller"); uint256 value = sellStakeEntry.value; // Calculate cancellation fee uint256 fee = (value * cancellationFee) / 10000; uint256 valueAfterFee = value - fee; // Restore value minus fee to user's big stake userBigStake[msg.sender] += valueAfterFee; totalBigStakes += valueAfterFee; // Note: fee reduces total liability (not added back to totalBigStakes) // Emit fee event if (fee > 0) { emit CancellationFeePaid(msg.sender, fee, stakeId); } // Remove sellStake entry delete sellStakes[msg.sender][stakeId]; // Remove from iteration array using swap-and-pop uint256 index = sellStakeKeyIndex[msg.sender][stakeId]; uint256 lastIndex = sellStakeKeys.length - 1; if (index != lastIndex) { SellStakeKey memory lastKey = sellStakeKeys[lastIndex]; sellStakeKeys[index] = lastKey; sellStakeKeyIndex[lastKey.seller][lastKey.stakeId] = index; } sellStakeKeys.pop(); delete sellStakeKeyIndex[msg.sender][stakeId]; emit StakeSaleCancelled(msg.sender, stakeId); } /// @notice Update the sale price of a listing /// @param stakeId The stake ID to update /// @param newSalePrice The new sale price function updateSellStake(uint256 stakeId, uint256 newSalePrice) external { SellStake storage sellStakeEntry = sellStakes[msg.sender][stakeId]; require(sellStakeEntry.value > 0, "Listing not found"); require(sellStakeEntry.seller == msg.sender, "Not the seller"); require(newSalePrice > 0, "Invalid sale price"); sellStakeEntry.salePrice = newSalePrice; emit StakeUpForSale(msg.sender, newSalePrice, stakeId); } /// @notice Buy a listed stake from marketplace /// @param seller The address of the seller /// @param stakeId The stake ID to buy function buySellStake(address seller, uint256 stakeId) external nonReentrant { SellStake storage sellStakeEntry = sellStakes[seller][stakeId]; require(sellStakeEntry.value > 0, "Listing not found"); require(seller != msg.sender, "Cannot buy own listing"); uint256 value = sellStakeEntry.value; uint256 salePrice = sellStakeEntry.salePrice; uint256 listTime = sellStakeEntry.listTime; // Calculate discount and protocol share using discount squared formula uint256 discount = value > salePrice ? value - salePrice : 0; uint256 discountPercent = discount * 10000 / value; // Calculate discount percentage (scaled by 10000) uint256 protocolSharePercent = (discountPercent * discountPercent) / 10000; // Square the discount percentage uint256 protocolShare = (value * protocolSharePercent) / 10000; // Apply squared discount to stake value uint256 buyerStake = value - protocolShare; // Buyer gets value minus protocol share // Transfer payment from buyer to seller (direct transfer) IERC20(BSC_TOKEN).safeTransferFrom(msg.sender, seller, salePrice); // Add buyerStake to buyer's big stake (value - protocol share) userBigStake[msg.sender] += buyerStake; totalBigStakes += buyerStake; // Note: protocolShare reduces total liability (not added back to totalBigStakes) // Track marketplace sales for seller marketplace_sales[seller] += salePrice; // Create marketplace history entry marketplaceHistory.push(MarketplaceHistory({ listTime: listTime, saleTime: block.timestamp, origValue: value, saleValue: salePrice, seller: seller, buyer: msg.sender })); // Remove sellStake entry delete sellStakes[seller][stakeId]; // Remove from iteration array using swap-and-pop uint256 index = sellStakeKeyIndex[seller][stakeId]; uint256 lastIndex = sellStakeKeys.length - 1; if (index != lastIndex) { SellStakeKey memory lastKey = sellStakeKeys[lastIndex]; sellStakeKeys[index] = lastKey; sellStakeKeyIndex[lastKey.seller][lastKey.stakeId] = index; } sellStakeKeys.pop(); delete sellStakeKeyIndex[seller][stakeId]; emit StakeSold(seller, msg.sender, salePrice, stakeId); } // Bot Functions for Emergency Management /// @notice This function will end and clear a user's vestings. /// @dev Only to be used by bots in emergencies /// @param user The user whose vestings will be ended and 0'd function clearVesting(address user) external onlyBot { for (uint256 i = 0; i < vestings[user].length; ++i) { Vesting storage vesting = vestings[user][i]; // Decrement accounting variables before clearing if (!vesting.complete) { if (dollarsVested[user] >= vesting.usdAmount) { dollarsVested[user] -= vesting.usdAmount; } if (vestedTotal[vesting.token] >= vesting.amount) { vestedTotal[vesting.token] -= vesting.amount; } } vesting.amount = 0; vesting.bonus = 0; vesting.claimedAmount = 0; vesting.claimedBonus = 0; vesting.complete = true; } } /// @notice Creates a vesting for a given user /// @dev Only to be used by bots for manual vesting creation /// @param user The user address to create the vesting for /// @param amount The amount for the vesting /// @param bonus The bonus amount for the vesting /// @param lockedUntil The unlock timestamp for the vesting /// @param token The token address for the vesting /// @param usdAmount The USD value of the vesting function createVesting(address user, uint256 amount, uint256 bonus, uint256 lockedUntil, address token, uint256 usdAmount) external onlyBot { createVesting(user, amount, bonus, lockedUntil, token, usdAmount, block.timestamp, block.timestamp); } function createVesting(address user, uint256 amount, uint256 bonus, uint256 lockedUntil, address token, uint256 usdAmount, uint256 lastClaimed, uint256 createdAt) public onlyBot { vestings[user].push(Vesting({ amount: amount, bonus: bonus, lockedUntil: lockedUntil, claimedAmount: 0, claimedBonus: 0, lastClaimed: lastClaimed, createdAt: createdAt, token: token, complete: false, usdAmount: usdAmount })); dollarsVested[user] += usdAmount; vestedTotal[token] += amount; } // /// @notice Migrates all vestings from an old address to a new address // /// @dev Only to be used by bots for account migrations // /// @param oldAddress The address with existing vestings to migrate from // /// @param newAddress The address to migrate vestings to // function migrateVestings(address oldAddress, address newAddress) external onlyBot { // require(oldAddress != address(0) && newAddress != address(0) && oldAddress != newAddress, "Invalid address"); // Vesting[] storage oldVestings = vestings[oldAddress]; // uint256 vestingCount = oldVestings.length; // require(vestingCount > 0, "No vestings available"); // Vesting[] storage newVestings = vestings[newAddress]; // for (uint256 i = 0; i < vestingCount; i++) { // Vesting storage oldVesting = oldVestings[i]; // // Copy vesting to new address // newVestings.push(oldVesting); // // Clear old vesting // oldVesting.amount = 0; // oldVesting.bonus = 0; // oldVesting.lockedUntil = 0; // oldVesting.claimedAmount = 0; // oldVesting.claimedBonus = 0; // oldVesting.lastClaimed = 0; // oldVesting.createdAt = 0; // oldVesting.usdAmount = 0; // oldVesting.complete = true; // } // // Migrate dollars vested // dollarsVested[newAddress] += dollarsVested[oldAddress]; // dollarsVested[oldAddress] = 0; // // Migrate pending vesting withdrawals // WithdrawVesting[] storage oldWithdrawVestings = withdrawVestingActual[oldAddress]; // uint256 withdrawVestingCount = oldWithdrawVestings.length; // if (withdrawVestingCount > 0) { // WithdrawVesting[] storage newWithdrawVestings = withdrawVestingActual[newAddress]; // for (uint256 i = 0; i < withdrawVestingCount; i++) { // newWithdrawVestings.push(oldWithdrawVestings[i]); // } // delete withdrawVestingActual[oldAddress]; // } // } // Vesting View Functions function getUnlockedVesting(address _user, uint256 _vestingIndex) public view returns (uint256) { Vesting storage vesting = vestings[_user][_vestingIndex]; uint256 timeElapsed = block.timestamp - vesting.createdAt; address token = vesting.token; uint256 unlockedAmount = 0; for (uint256 i = 0; i < unlockSchedules[token].length; ++i) { UnlockStep storage step = unlockSchedules[token][i]; uint256 timeTier = step.timeOffset; uint256 percentage = step.percentage; if (timeElapsed >= timeTier) { unlockedAmount = unlockedAmount + ((vesting.amount * percentage) / 10000); } } return unlockedAmount; } function getVestingSchedule(address _user, uint256 _vestingIndex) public view returns (uint256[] memory, uint256[] memory) { Vesting storage vesting = vestings[_user][_vestingIndex]; address token = vesting.token; uint256 scheduleLength = unlockSchedules[token].length; uint256[] memory unlockTimestamps = new uint256[](scheduleLength); uint256[] memory unlockPercentages = new uint256[](scheduleLength); for (uint256 i = 0; i < scheduleLength; ++i) { UnlockStep storage step = unlockSchedules[token][i]; // Calculate the absolute unlock timestamp unlockTimestamps[i] = vesting.createdAt + step.timeOffset; unlockPercentages[i] = step.percentage; // Percentage is stored as scaled by 10000 (e.g., 2500 = 25%) } return (unlockTimestamps, unlockPercentages); } function getUnlockedVestingBonus(address _user, uint256 _vestingIndex) public view returns (uint256) { Vesting storage vesting = vestings[_user][_vestingIndex]; uint256 timeElapsed = block.timestamp - vesting.createdAt; address token = vesting.token; uint256 unlockedAmount = 0; for (uint256 i = 0; i < unlockSchedules[token].length; ++i) { UnlockStep storage step = unlockSchedules[token][i]; uint256 timeTier = step.timeOffset; uint256 percentage = step.percentage; uint256 maxBonusAmount = (vesting.usdAmount * BONUS_PERCENTAGE) / 100; if (timeElapsed >= timeTier) { unlockedAmount = unlockedAmount + ((maxBonusAmount * percentage) / 10000); } } return unlockedAmount; } /// @notice View function to get all vestings for a specific address function getVestings(address user) external view returns (Vesting[] memory) { return vestings[user]; } /** * @notice Returns the vested amounts and USD values for an array of tokens. * @param _tokens The array of token addresses to evaluate. * @return amounts The array of vested amounts for each token. * @return usdValues The array of USD values for each token's vested amount. * @return totalUsd The total USD value of all vested tokens in the array. */ function getVestedTotals(address[] calldata _tokens) external view returns ( uint256[] memory amounts, uint256[] memory usdValues, uint256 totalUsd ) { uint256 length = _tokens.length; amounts = new uint256[](length); usdValues = new uint256[](length); for (uint256 i = 0; i < length; i++) { address token = _tokens[i]; // 1. Get the total amount vested for this token. uint256 tokenAmount = vestedTotal[token]; amounts[i] = tokenAmount; // 2. Query the oracle for this token's USD price. // Assumes the oracle returns a price scaled by 1e18. uint256 price = iPriceOracle(priceOracles[token]).getLatestPrice(token); // 3. Calculate the vested USD value: (price * amount) / 1e18 uint256 valueInUsd = (price * tokenAmount) / 1e18; usdValues[i] = valueInUsd; // 4. Accumulate the total USD amount totalUsd += valueInUsd; } return (amounts, usdValues, totalUsd); } /// @notice Returns the total USD value of the user's unclaimed, uncomplete, stake amounts, based on current token prices from the oracle. /// @return totalUsd The total unclaimed stake value, in USD (1e18 precision). function getUserTotalUnclaimedUsdValue(address user) external view returns (uint256 totalUsd) { uint256 length = vestings[user].length; for (uint256 i = 0; i < length; i++) { Vesting memory v = vestings[user][i]; if (!v.complete) { uint256 tokenPrice = iPriceOracle(priceOracles[v.token]).getLatestPrice(v.token); // The unclaimed portion of the stake uint256 unclaimedAmount = v.amount - v.claimedAmount; // Convert unclaimed tokens to USD value uint256 stakeUsd = (tokenPrice * unclaimedAmount) / 1e18; totalUsd += stakeUsd; } } return totalUsd; } /// @notice Claim unlocked vesting tokens for a specific vesting /// @param _vestingIndex The index of the vesting to claim from function claimVesting(uint256 _vestingIndex) external nonReentrant { require(_vestingIndex < vestings[msg.sender].length, "Invalid vesting index"); Vesting storage vesting = vestings[msg.sender][_vestingIndex]; require(!vesting.complete, "Vesting complete"); uint256 maxClaim = getUnlockedVesting(msg.sender, _vestingIndex); require(maxClaim >= vesting.claimedAmount, "Invalid claim amount"); uint256 amountToClaim = maxClaim - vesting.claimedAmount; require(amountToClaim > 0, "Nothing to claim"); vesting.claimedAmount += amountToClaim; if (vesting.claimedAmount >= vesting.amount) { vesting.complete = true; } // Update user's dollarsVested if (dollarsVested[msg.sender] > 0) { uint256 usdPrice = (iPriceOracle(priceOracles[vesting.token]).getLatestPrice(vesting.token) * amountToClaim) / 1e18; if (usdPrice >= dollarsVested[msg.sender]) { dollarsVested[msg.sender] = 0; } else { dollarsVested[msg.sender] -= usdPrice; } } vestedTotal[vesting.token] -= amountToClaim; // Add vesting claims to unified withdrawal queue withdrawStakes[msg.sender].push(WithdrawStake({ stakeId: vestingStakeIdCounter++, amount: amountToClaim, unlockTime: block.timestamp + unlockDelay, token: vesting.token })); // Increment withdraw vesting liabilities for this token withdrawVestingLiabilities[vesting.token] += amountToClaim; emit VestingClaimed(msg.sender, amountToClaim, 0); } /// @notice Claim all unlocked vesting tokens for a specific token /// @param _token The token address to claim all vestings for function claimAllVestingByToken(address _token) external nonReentrant { uint256 totalReward = 0; for (uint256 i = 0; i < vestings[msg.sender].length; i++) { Vesting storage vesting = vestings[msg.sender][i]; if (vesting.token == _token && !vesting.complete) { uint256 maxClaim = getUnlockedVesting(msg.sender, i); if (maxClaim > vesting.claimedAmount) { uint256 amountToClaim = maxClaim - vesting.claimedAmount; totalReward += amountToClaim; vesting.claimedAmount += amountToClaim; if (vesting.claimedAmount >= vesting.amount) { vesting.complete = true; } } } } require(totalReward > 0, "Nothing to claim"); // Update user's dollarsVested if (dollarsVested[msg.sender] > 0) { uint256 usdPrice = (iPriceOracle(priceOracles[_token]).getLatestPrice(_token) * totalReward) / 1e18; if (usdPrice >= dollarsVested[msg.sender]) { dollarsVested[msg.sender] = 0; } else { dollarsVested[msg.sender] -= usdPrice; } } vestedTotal[_token] -= totalReward; // Add vesting claims to unified withdrawal queue withdrawStakes[msg.sender].push(WithdrawStake({ stakeId: vestingStakeIdCounter++, amount: totalReward, unlockTime: block.timestamp + unlockDelay, token: _token })); // Increment withdraw vesting liabilities for this token withdrawVestingLiabilities[_token] += totalReward; emit VestingClaimed(msg.sender, totalReward, 0); } /// @notice Claim unlocked bonus tokens from a specific vesting /// @param _vestingIndex The index of the vesting to claim bonus from function claimBonus(uint256 _vestingIndex) external nonReentrant { require(_vestingIndex < vestings[msg.sender].length, "Invalid vesting index"); Vesting storage vesting = vestings[msg.sender][_vestingIndex]; uint256 maxBonus = getUnlockedVestingBonus(msg.sender, _vestingIndex); require(maxBonus >= vesting.claimedBonus, "Invalid claim amount"); uint256 bonusToClaim = maxBonus - vesting.claimedBonus; require(bonusToClaim > 0, "Nothing to claim"); vesting.claimedBonus += bonusToClaim; // Create withdrawable stake with unlock delay (add 1e6 to distinguish from normal stakes) withdrawStakes[msg.sender].push(WithdrawStake({ stakeId: _vestingIndex + 1e6, amount: bonusToClaim, unlockTime: block.timestamp + unlockDelay, token: BSC_TOKEN })); emit BonusClaimed(msg.sender, bonusToClaim); } // Marketplace View Functions /// @notice Get all active marketplace listings /// @return sellers Array of seller addresses /// @return stakeIds Array of stake IDs /// @return sellStakeData Array of SellStake structs function getAllSellStakes() external view returns ( address[] memory sellers, uint256[] memory stakeIds, SellStake[] memory sellStakeData ) { uint256 length = sellStakeKeys.length; sellers = new address[](length); stakeIds = new uint256[](length); sellStakeData = new SellStake[](length); for (uint256 i = 0; i < length; i++) { SellStakeKey memory key = sellStakeKeys[i]; sellers[i] = key.seller; stakeIds[i] = key.stakeId; sellStakeData[i] = sellStakes[key.seller][key.stakeId]; } return (sellers, stakeIds, sellStakeData); } /// @notice Get a specific marketplace listing /// @param seller The seller address /// @param stakeId The stake ID /// @return The SellStake struct function getSellStake(address seller, uint256 stakeId) external view returns (SellStake memory) { return sellStakes[seller][stakeId]; } /// @notice Get marketplace history /// @param startIndex Starting index in history array /// @param length Number of entries to return /// @return Array of MarketplaceHistory structs function getMarketplaceHistory(uint256 startIndex, uint256 length) external view returns (MarketplaceHistory[] memory) { require(startIndex < marketplaceHistory.length, "Start index out of bounds"); uint256 endIndex = startIndex + length; if (endIndex > marketplaceHistory.length) { endIndex = marketplaceHistory.length; } MarketplaceHistory[] memory result = new MarketplaceHistory[](endIndex - startIndex); for (uint256 i = startIndex; i < endIndex; i++) { result[i - startIndex] = marketplaceHistory[i]; } return result; } /// @notice Get total marketplace history count /// @return Total number of marketplace transactions function getMarketplaceHistoryCount() external view returns (uint256) { return marketplaceHistory.length; } /// @notice Get user's total marketplace sales /// @param user The user address /// @return Total sales amount for the user function getUserMarketplaceSales(address user) external view returns (uint256) { return marketplace_sales[user]; } /// @notice Get user's total claimed amount sent to withdrawStakes /// @param user The user address /// @return Total amount claimed and sent to withdrawStakes for the user function getUserTotalClaimed(address user) external view returns (uint256) { return totalClaimed[user]; } /// @notice Search marketplace history for stakes where address was seller or buyer /// @param targetAddress The address to search for as seller or buyer /// @return Array of MarketplaceHistory structs where address was involved function searchMarketplaceHistory(address targetAddress) external view returns (MarketplaceHistory[] memory) { require(targetAddress != address(0), "Invalid address"); // Count matches first to size the result array properly uint256 matchCount = 0; for (uint256 i = 0; i < marketplaceHistory.length; i++) { if (marketplaceHistory[i].seller == targetAddress || marketplaceHistory[i].buyer == targetAddress) { matchCount++; } } // Return empty array if no matches if (matchCount == 0) { return new MarketplaceHistory[](0); } // Create result array with exact size needed MarketplaceHistory[] memory result = new MarketplaceHistory[](matchCount); uint256 resultIndex = 0; // Populate result array for (uint256 i = 0; i < marketplaceHistory.length; i++) { if (marketplaceHistory[i].seller == targetAddress || marketplaceHistory[i].buyer == targetAddress) { result[resultIndex] = marketplaceHistory[i]; resultIndex++; } } return result; } /// @notice Test function for upgrade verification /// @return Returns a constant value to verify upgrade worked function testUpgradeFunction() external pure returns (uint256) { return 999; // Different value from bsc_paca to distinguish contracts } }