1256 lines
50 KiB
Solidity
1256 lines
50 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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 paybackPercent; // Payback percentage used for this epoch (scaled by 10000)
|
|
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 liabilities by token address (all tokens including BSC_TOKEN)
|
|
mapping(address => uint256) public withdrawLiabilities;
|
|
|
|
// Epoch-based staking variables
|
|
mapping(uint256 => Epoch) public epochs;
|
|
mapping(address => uint256) public userBigStake; // User's main stake amount
|
|
mapping(address => uint256) public userOriginalStake; // User's original stake amount (never changes)
|
|
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%)
|
|
uint256 public maxUnlockPercentage; // Maximum unlock percentage per epoch (e.g., 100 = 1%)
|
|
|
|
// 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
|
|
mapping(address => uint256) public pendingSellStakes; // Track total value of pending sell stakes 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 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;
|
|
maxUnlockPercentage = 100; // 1% maximum unlock per epoch
|
|
}
|
|
|
|
// 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 updateMaxUnlockPercentage(uint256 _maxPercentage) external onlyOwner {
|
|
require(_maxPercentage > 0, "Max percentage must be greater than 0");
|
|
maxUnlockPercentage = _maxPercentage;
|
|
}
|
|
|
|
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
|
|
);
|
|
}
|
|
|
|
// Check that unlock percentage doesn't exceed maximum
|
|
require(unlockPercentage <= maxUnlockPercentage, "Unlock percentage high");
|
|
|
|
// Create new epoch entry
|
|
epochs[currentEpochId] = Epoch({
|
|
estDaysRemaining: estDaysRemaining,
|
|
currentTreasuryTvl: currentTreasuryTvl,
|
|
totalLiability: totalBigStakes,
|
|
paybackPercent: _paybackPercent,
|
|
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 = getNetStake(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 minus pending sell stakes)
|
|
function getNetStake(address user) public view returns (uint256) {
|
|
uint256 bigStake = userBigStake[user];
|
|
uint256 committed = pendingSellStakes[user];
|
|
return bigStake > committed ? bigStake - committed : 0;
|
|
}
|
|
|
|
/// @notice Get comprehensive user stake information
|
|
function getUserStakeInfo(address user) external view returns (
|
|
uint256 bigStake, // Current "active" stake amount
|
|
uint256 unclaimedFunds, // Available to claim
|
|
uint256 netStake, // Available for new actions (minus pending sells)
|
|
uint256 originalStake // Original stake amount (never changes)
|
|
) {
|
|
uint256 unclaimed = calculateUnclaimedFunds(user);
|
|
uint256 net = getNetStake(user);
|
|
return (
|
|
userBigStake[user],
|
|
unclaimed,
|
|
net,
|
|
userOriginalStake[user]
|
|
);
|
|
}
|
|
|
|
/// @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
|
|
}));
|
|
|
|
// Increment withdraw liabilities for BSC_TOKEN
|
|
withdrawLiabilities[BSC_TOKEN] += unclaimedAmount;
|
|
|
|
emit FundsClaimed(msg.sender, unclaimedAmount);
|
|
}
|
|
|
|
/// @notice Withdraw claimed funds after unlock period
|
|
function withdrawStake(uint256[] calldata stakeIds) external nonReentrant {
|
|
WithdrawStake[] storage userStakes = withdrawStakes[msg.sender];
|
|
require(userStakes.length > 0, "No stakes available");
|
|
require(stakeIds.length > 0, "No stake IDs provided");
|
|
|
|
for (uint256 j = 0; j < stakeIds.length; j++) {
|
|
uint256 stakeId = stakeIds[j];
|
|
bool found = false;
|
|
|
|
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 liabilities for all tokens
|
|
withdrawLiabilities[token] -= amount;
|
|
|
|
// Transfer tokens to user based on the specified token
|
|
IERC20(token).safeTransfer(msg.sender, amount);
|
|
|
|
emit StakeWithdrawn(msg.sender, amount, stakeId);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
require(found, "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");
|
|
|
|
// Require minimum 25% of user's net stake
|
|
require(amount >= (netStake * 2500) / 10000, "Amount too low");
|
|
|
|
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
|
|
}));
|
|
|
|
// Increment withdraw liabilities for BSC_TOKEN
|
|
withdrawLiabilities[BSC_TOKEN] += payoutAmount;
|
|
|
|
emit FundsClaimed(msg.sender, payoutAmount);
|
|
}
|
|
|
|
// Bot Functions for Staking Management
|
|
|
|
|
|
/// @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 original stake only if this is the first time (never changes after)
|
|
if (userOriginalStake[users[i]] == 0) {
|
|
userOriginalStake[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 = getNetStake(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;
|
|
|
|
// Add to pending sell stakes tracking (don't touch actual bigStake until sale/cancel)
|
|
pendingSellStakes[msg.sender] += 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 and apply directly to userBigStake
|
|
uint256 fee = (value * cancellationFee) / 10000;
|
|
|
|
// Remove from pending and apply fee directly to bigStake
|
|
pendingSellStakes[msg.sender] -= value;
|
|
if (fee > 0) {
|
|
userBigStake[msg.sender] -= fee;
|
|
totalBigStakes -= fee;
|
|
}
|
|
// Note: fee reduces total liability permanently
|
|
|
|
// 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);
|
|
|
|
// Transfer stakes: remove from seller, add to buyer (minus protocol share)
|
|
userBigStake[seller] -= value;
|
|
userBigStake[msg.sender] += buyerStake;
|
|
pendingSellStakes[seller] -= value;
|
|
// Note: totalBigStakes decreases by protocolShare
|
|
totalBigStakes -= protocolShare;
|
|
|
|
// 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 liabilities for this token
|
|
withdrawLiabilities[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 liabilities for this token
|
|
withdrawLiabilities[_token] += totalReward;
|
|
|
|
emit VestingClaimed(msg.sender, totalReward, 0);
|
|
}
|
|
|
|
/// @notice Claim unlocked bonus tokens from multiple vestings
|
|
/// @param _vestingIndices Array of vesting indices to claim bonus from
|
|
function claimBonus(uint256[] calldata _vestingIndices) external nonReentrant {
|
|
uint256 totalBonusClaimed = 0;
|
|
|
|
for (uint256 i = 0; i < _vestingIndices.length; i++) {
|
|
uint256 _vestingIndex = _vestingIndices[i];
|
|
|
|
// Skip invalid vesting indices
|
|
if (_vestingIndex >= vestings[msg.sender].length) {
|
|
continue;
|
|
}
|
|
|
|
Vesting storage vesting = vestings[msg.sender][_vestingIndex];
|
|
uint256 maxBonus = getUnlockedVestingBonus(msg.sender, _vestingIndex);
|
|
|
|
// Skip if invalid claim amount or nothing to claim
|
|
if (maxBonus < vesting.claimedBonus) {
|
|
continue;
|
|
}
|
|
|
|
uint256 bonusToClaim = maxBonus - vesting.claimedBonus;
|
|
if (bonusToClaim == 0) {
|
|
continue;
|
|
}
|
|
|
|
vesting.claimedBonus += bonusToClaim;
|
|
totalBonusClaimed += 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
|
|
}));
|
|
}
|
|
|
|
// Only update liabilities and emit event if something was actually claimed
|
|
if (totalBonusClaimed > 0) {
|
|
// Increment withdraw liabilities for BSC_TOKEN
|
|
withdrawLiabilities[BSC_TOKEN] += totalBonusClaimed;
|
|
emit BonusClaimed(msg.sender, totalBonusClaimed);
|
|
}
|
|
}
|
|
|
|
|
|
// 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
|
|
}
|
|
|
|
} |