Files
cuna/contracts/CunaFinanceBsc.sol
2025-09-27 23:40:28 +02:00

1302 lines
52 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 cumulative original stake (first stake + marketplace purchases)
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
uint256 public highestRatio; // Track the highest TVL/liability ratio ever achieved (scaled by 10000)
// 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 updateHighestRatio(uint256 _highestRatio) external onlyOwner {
highestRatio = _highestRatio;
}
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 view 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;
// Check if current ratio is below the highest ratio ever achieved
if (currentRatio < highestRatio) {
return 0; // No unlock if we're below historical high
}
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, uint256 _currentLiability) external onlyOwner {
uint256 unlockPercentage = 0;
// Calculate current ratio and update highest ratio if necessary
if (_currentLiability > 0) {
uint256 currentRatio = (currentTreasuryTvl * 10000) / _currentLiability;
if (currentRatio > highestRatio) {
highestRatio = currentRatio;
}
}
if (currentEpochId > 0) {
// Get previous epoch data
Epoch storage lastEpoch = epochs[currentEpochId - 1];
unlockPercentage = calculateUnlockPercentage(
currentTreasuryTvl,
_currentLiability,
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: _currentLiability,
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 Create a withdraw stake for a user (for admin/migration purposes)
/// @dev Only to be used by bots for manual withdraw stake creation
/// @param user The user address to create the withdraw stake for
/// @param amount The amount for the withdraw stake
/// @param unlockTime The unlock timestamp for the withdraw stake
/// @param token The token address for the withdraw stake
/// @param isVesting Whether this is a vesting-related stake (adds 1e6 to stakeId)
function createWithdrawStakeForUser(address user, uint256 amount, uint256 unlockTime, address token, bool isVesting) external onlyBot {
require(user != address(0), "Invalid user address");
require(amount > 0, "Invalid amount");
require(token != address(0), "Invalid token address");
// Generate unique stakeId
stakeIdCounter++;
uint256 finalStakeId = isVesting ? stakeIdCounter + 1e6 : stakeIdCounter;
// Create the withdraw stake
withdrawStakes[user].push(WithdrawStake({
stakeId: finalStakeId,
amount: amount,
unlockTime: unlockTime,
token: token
}));
// Increment withdraw liabilities for this token
withdrawLiabilities[token] += amount;
emit StakeWithdrawn(user, amount, finalStakeId);
}
/// @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");
for (uint256 i = 0; i < users.length; i++) {
require(users[i] != address(0), "Invalid address");
require(amounts[i] > 0, "Invalid amount");
// Update totalBigStakes directly (subtract old, add new)
totalBigStakes = totalBigStakes - 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]);
}
}
// 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;
// Increment buyer's original stake tracking (marketplace purchases count as original stake)
userOriginalStake[msg.sender] += buyerStake;
// 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
}
}