Files
pacahh/contracts/sonic_paca.sol

1484 lines
58 KiB
Solidity
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: MIT
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
pragma solidity ^0.8.20;
interface iPriceOracle {
// returns price in USD
function getLatestPrice(address token) external view returns (uint256);
}
// Custom errors for gas optimization
error NotAuthorized();
error AlreadyOwner();
error NotOwner();
error CannotRemoveSelf();
error InvalidAddress();
error InvalidAmount();
error AmountBelowMinimum();
error InvalidRestakePercentage();
error StakeLocked();
error StakeComplete();
error NothingToClaim();
error InvalidClaimAmount();
error InsufficientRewards();
error InvalidStakeIndex();
error NoStakesAvailable();
error StakeNotFound();
error NotEnoughToCompound();
error PriceOracleNotSet();
error StakeAlreadyOnSale();
error PriceTooLow();
error StakeNotInSellState();
// File: paca.sol
contract PacaFinanceWithBoostAndScheduleSonic is Initializable, ReentrancyGuardUpgradeable {
using SafeERC20 for IERC20;
// Struct Definitions
struct Pool {
uint256 lockupPeriod;
uint256 dailyRewardRate;
uint256 totalStaked;
uint256 totalRewards;
address tokenAddress;
}
struct Stake {
uint256 amount;
uint256 lastClaimed;
uint256 dailyRewardRate;
uint256 unlockTime;
bool complete;
}
struct StakeInput {
address user;
uint256 amount;
uint256 lastClaimed;
uint256 unlockTime;
uint256 dailyRewardRate;
}
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;
}
struct BoostRange {
uint256 minTokens;
uint256 maxTokens;
uint256 boostPercentage;
}
struct WithdrawStake {
uint256 stakeId;
uint256 amount;
uint256 unlockTime;
}
struct WithdrawVesting {
uint256 vestingId;
uint256 amount;
uint256 unlockTime;
address token;
}
struct SellStake {
uint256 price;
uint256 bonusAmount;
uint256 amount;
uint256 lastClaimed;
uint256 dailyRewardRate;
uint256 origUnlockTime;
}
struct SellStakeKey {
address seller;
uint256 stakeId;
}
BoostRange[] public boosttiers;
// Contract Variables
Pool public pool;
address public owner;
mapping(address => bool) public owners;
mapping(address => Stake[]) public stakes;
mapping(address => Vesting[]) public vestings;
mapping(address => UnlockStep[]) public unlockSchedules;
mapping(address => address) public priceOracles;
mapping(address => uint256) public dollarsVested; // per user address
uint256 public lockupDuration;
uint256 public minStakeLock;
uint256 private constant BONUS_PERCENTAGE = 10;
mapping(address => bool) public authorizedBots;
mapping(address => uint256) public vestedTotal; // per vesting token
uint256 public unlockDelay;
uint256 public withdrawLiabilities;
mapping(address => WithdrawStake[]) public withdrawStake;
mapping(address => WithdrawVesting[]) private withdrawVesting; // Keep for storage layout compatibility - DO NOT USE
uint256 private withdrawVestingCounter; // Keep for storage layout compatibility - DO NOT USE
uint256 public restakeBonus;
mapping(address => uint256) public addressFixedRate;
mapping(address => mapping(uint256 => SellStake)) public sellStakes;
uint256 public sellTax;
uint256 public sellKickBack;
SellStakeKey[] public sellStakeKeys;
mapping(address => mapping(uint256 => uint256)) private sellStakeKeyIndex;
uint256 public sellMin;
// Moved to end to fix storage layout corruption - vesting withdrawal functionality
mapping(address => WithdrawVesting[]) private withdrawVestingActual;
uint256 private withdrawVestingCounterActual;
// Track total withdraw vesting liabilities by token address
mapping(address => uint256) public withdrawVestingLiabilities;
// Events
event Staked(address indexed user, uint256 amount);
event RewardClaimed(address indexed user, uint256 reward);
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 PoolUpdated(uint256 lockupPeriod, uint256 dailyRewardRate);
event UnlockScheduleSet(address indexed token);
event FundsWithdrawn(address indexed owner, address indexed token, uint256 amount);
event RewardsDeposited(uint256 amount);
event CompoundRewards(address indexed user, uint256 amount);
event MinStakeLockUpdated(uint256 amount);
event StakeWithdrawn(address indexed user, uint256 amount, uint256 stakeId);
event StakeUpForSale(address indexed user, uint256 saleAmount, uint256 stakeId);
event StakeSaleCancelled(address indexed user, uint256 stakeId);
event StakeSold(address indexed seller, address indexed buyer,uint256 saleAmount, uint256 stakeId);
// Modifiers
modifier onlyOwner() {
if (!owners[msg.sender]) revert NotAuthorized();
_;
}
modifier onlyBot() {
if (!authorizedBots[msg.sender]) revert NotAuthorized();
_;
}
function initialize() public initializer {
__ReentrancyGuard_init(); // Initialize ReentrancyGuardUpgradeable
owner = 0x41970Ce76b656030A79E7C1FA76FC4EB93980255;
owners[0x41970Ce76b656030A79E7C1FA76FC4EB93980255] = true;
// lockupDuration = 250 days;
// minStakeLock = 16 ether;
// pool.tokenAddress = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913;
// pool.lockupPeriod = 250 * 1 days;
// pool.dailyRewardRate = 33;
// // Price oracle for a specific tokens
// // priceOracles[0x940181a94A35A4569E4529A3CDfB74e38FD98631] = 0x0Dde1b42F7B3891C9731280A74081501729A73c5;
// authorizedBots[0xbf12D3b827a230F7390EbCc9b83b289FdC98ba81] = true;
// authorizedBots[0x7c40f272570fdf9549d6f67493aC250a1DB52F27] = true;
// unlockDelay = 60 * 60 * 36;
// restakeBonus = 3;
}
// Ownership Management
function addOwner(address _newOwner) external onlyOwner {
if (owners[_newOwner]) revert AlreadyOwner();
owners[_newOwner] = true;
}
function removeOwner(address _owner) external onlyOwner {
if (!owners[_owner]) revert NotOwner();
if (_owner == msg.sender) revert CannotRemoveSelf();
owners[_owner] = false;
}
/// @notice Function to add a bot to the list (only callable by the contract owner)
function addBot(address bot) external onlyOwner {
if (bot == address(0)) revert InvalidAddress();
authorizedBots[bot] = true;
}
// /// @notice Function to remove a bot from the list (only callable by the contract owner)
// function removeBot(address bot) external onlyOwner {
// require(bot != address(0), "Invalid address");
// authorizedBots[bot] = false;
// }
// Admin Functions
function updatePool(uint256 _lockupPeriod, uint256 _dailyRewardRate) external onlyOwner {
pool.lockupPeriod = _lockupPeriod * 1 days;
pool.dailyRewardRate = _dailyRewardRate;
emit PoolUpdated(_lockupPeriod, _dailyRewardRate);
}
function depositRewards(uint256 _amount) external onlyOwner {
IERC20(pool.tokenAddress).safeTransferFrom(msg.sender, address(this), _amount);
pool.totalRewards = pool.totalRewards + _amount;
emit RewardsDeposited(_amount);
}
function updateStakeMin(uint256 _amount) external onlyOwner {
minStakeLock = _amount;
emit MinStakeLockUpdated(_amount);
}
function updateUnlockDelay(uint256 _delay) external onlyOwner {
unlockDelay = _delay;
}
function updatePoolRewards(uint256 _amount) external onlyOwner {
pool.totalRewards = _amount;
}
function updateRestakeBonus(uint256 _newBonus) external onlyOwner {
restakeBonus = _newBonus;
}
/// @notice New Stake Sell Tax
/// @param _newTax The rate expressed in 2 digits, ex: 20
function updateSellTax(uint256 _newTax) external onlyOwner {
sellTax = _newTax;
}
/// @notice New Stake Sell Minimum
/// @param _newMin The rate expressed in 2 digits, ex: 30
function updateSellMin(uint256 _newMin) external onlyOwner {
sellMin = _newMin;
}
/// @notice New Stake Sell kickback to the buyer
/// @param _newKickback The rate expressed in 2 digits, ex: 5
function updatesellKickBack(uint256 _newKickback) external onlyOwner {
sellKickBack = _newKickback;
}
/// @notice Function to add an address to have a fixed daily reward (only callable by the contract owner)
/// @param _addr The address to give a fixed rate
/// @param _rate The fixed rate expressed in 2 digits, ex: 40
function addFixedRate(address _addr, uint _rate) external onlyOwner {
if (_addr == address(0)) revert InvalidAddress();
addressFixedRate[_addr] = _rate;
}
/// @notice Function to remove an address' fixed daily reward (only callable by the contract owner)
/// @param _addr The address to 0 out
function removeFixedRate(address _addr) external onlyOwner {
if (_addr == address(0)) revert InvalidAddress();
addressFixedRate[_addr] = 0;
}
// /// @notice Add or edit a tier range *commented out for size constraints*
// function addOrEditTier(uint256 minTokens, uint256 maxTokens, uint256 boostPercentage) public onlyOwner {
// require(minTokens < maxTokens, "Invalid range: minTokens must be < maxTokens");
// require(!rangesOverlap(minTokens, maxTokens), "Range overlaps with existing tiers");
// // Check if editing an existing range
// for (uint256 i = 0; i < boosttiers.length; ++i) {
// if (boosttiers[i].minTokens == minTokens && boosttiers[i].maxTokens == maxTokens) {
// // Edit the existing range
// boosttiers[i].boostPercentage = boostPercentage;
// return;
// }
// }
// // Add new range
// boosttiers.push(BoostRange(minTokens, maxTokens, boostPercentage));
// // Sort the ranges after adding
// sortRanges();
// }
// // Check for range overlap
// function rangesOverlap(uint256 minTokens, uint256 maxTokens) internal view returns (bool) {
// for (uint256 i = 0; i < boosttiers.length; ++i) {
// if (minTokens <= boosttiers[i].maxTokens && maxTokens >= boosttiers[i].minTokens) {
// return true;
// }
// }
// return false;
// }
// /// @notice Sort ranges by minTokens
// function sortRanges() internal {
// for (uint256 i = 0; i < boosttiers.length; ++i) {
// for (uint256 j = i + 1; j < boosttiers.length; j++) {
// if (boosttiers[i].minTokens > boosttiers[j].minTokens) {
// // Swap ranges
// BoostRange memory temp = boosttiers[i];
// boosttiers[i] = boosttiers[j];
// boosttiers[j] = temp;
// }
// }
// }
// }
// /// @notice Remove a range by index
// function removeTier(uint256 index) external onlyOwner {
// require(index < boosttiers.length, "Index out of bounds");
// for (uint256 i = index; i < boosttiers.length - 1; ++i) {
// boosttiers[i] = boosttiers[i + 1];
// }
// boosttiers.pop();
// }
function withdrawFromStakingPool(uint256 _amount) external onlyOwner {
IERC20(pool.tokenAddress).safeTransfer(msg.sender, _amount);
emit FundsWithdrawn(msg.sender, pool.tokenAddress, _amount);
}
function withdrawFromVestingPool(address _token, uint256 _amount) external onlyOwner {
IERC20(_token).safeTransfer(msg.sender, _amount);
emit FundsWithdrawn(msg.sender, _token, _amount);
}
// function setUnlockScheduleByPercentage(
// address _token,
// uint256 _lockTime, // Total lock time in seconds
// uint256 _percentagePerStep // Percentage unlocked per step (in basis points, e.g., 100 = 1%)
// ) external onlyOwner {
// require(_lockTime != 0, "Lock time must be greater than zero");
// require(_percentagePerStep != 0, "Percentage per step must be greater than zero");
// uint256 totalPercentage = 10000; // 100% in basis points
// require(totalPercentage % _percentagePerStep == 0, "Percentage must divide 100% evenly");
// uint256 steps = totalPercentage / _percentagePerStep; // Number of steps
// uint256 stepTime = _lockTime / steps; // Time interval per step
// delete unlockSchedules[_token]; // Clear existing schedule for this token
// for (uint256 i = 1; i <= steps; ++i) {
// unlockSchedules[_token].push(UnlockStep({
// timeOffset: stepTime * i, // Time offset for this step
// percentage: _percentagePerStep
// }));
// }
// emit UnlockScheduleSet(_token);
// }
/// @notice Get the boost percentage for a given token amount
function getBoost(uint256 depositedTokens) public view returns (uint256) {
for (uint256 i = 0; i < boosttiers.length; ++i) {
if (depositedTokens >= boosttiers[i].minTokens && depositedTokens <= boosttiers[i].maxTokens) {
return boosttiers[i].boostPercentage;
}
}
return 0; // Default boost if no range matches
}
/// @notice This function will end and clear a user's stakes.
/// @dev Only to be used by bots in emergencies
/// @param user The user whose stakes will be ended and 0'd
function clearStakes(address user) external onlyBot {
uint256 clearedStakes = 0;
for (uint256 i = 0; i < stakes[user].length; ++i) {
Stake storage stake = stakes[user][i];
clearedStakes = clearedStakes + stake.amount;
stake.amount = 0;
stake.complete = true;
}
pool.totalStaked = pool.totalStaked - clearedStakes;
}
/// @notice This function will end and clear a user's withdraw stakes.
/// @dev Only to be used by bots in emergencies
/// @param user The user whose withdraw stakes will be 0'd
function clearWithdrawStakes(address user) external onlyBot {
uint256 clearedStakes = 0;
for (uint256 i = 0; i < withdrawStake[user].length; ++i) {
WithdrawStake storage stake = withdrawStake[user][i];
clearedStakes = clearedStakes + stake.amount;
stake.amount = 0;
}
withdrawLiabilities -= clearedStakes;
}
/// @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 {
if (oldAddress == address(0) || newAddress == address(0) || oldAddress == newAddress) revert InvalidAddress();
Vesting[] storage oldVestings = vestings[oldAddress];
uint256 vestingCount = oldVestings.length;
if (vestingCount == 0) revert NoStakesAvailable();
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];
}
}
// /**
// * @dev Extends the lastClaimed and unlockTime for all stakes of a given address
// * @param _address The address whose stakes to extend
// * @param _seconds The number of seconds to add to lastClaimed and unlockTime
// */
// function extendStakes(address _address, uint256 _seconds) external onlyBot {
// if (_seconds == 0) return; // Early exit for zero seconds
// Stake[] storage userStakes = stakes[_address];
// uint256 length = userStakes.length;
// if (length == 0) return; // Early exit for no stakes
// // Cache the stake reference to avoid repeated array access
// for (uint256 i; i < length;) {
// Stake storage stake = userStakes[i];
// // Only extend active stakes with non-zero amounts
// if (!stake.complete && stake.amount > 0) {
// unchecked {
// stake.lastClaimed += _seconds;
// stake.unlockTime += _seconds;
// }
// }
// unchecked { ++i; }
// }
// }
// /**
// * @dev Extends the lockedUntil, lastClaimed, and createdAt for all vestings of a given address
// * @param _address The address whose vestings to extend
// * @param _seconds The number of seconds to add to the timestamps
// */
// function extendVestings(address _address, uint256 _seconds) external onlyBot {
// if (_seconds == 0) return; // Early exit for zero seconds
// Vesting[] storage userVestings = vestings[_address];
// uint256 length = userVestings.length;
// if (length == 0) return; // Early exit for no vestings
// // Cache the vesting reference to avoid repeated array access
// for (uint256 i; i < length;) {
// Vesting storage vesting = userVestings[i];
// // Only extend active vestings with non-zero amounts
// if (!vesting.complete && vesting.amount > 0) {
// unchecked {
// vesting.lockedUntil += _seconds;
// vesting.lastClaimed += _seconds;
// vesting.createdAt += _seconds;
// }
// }
// unchecked { ++i; }
// }
// }
// function adminClearSellStake(address _seller, uint256 _stakeId) external onlyOwner {
// SellStake storage sellStakeEntry = sellStakes[_seller][_stakeId];
// require(sellStakeEntry.amount != 0, "Sell stake not found");
// // Access the original stake.
// Stake storage stake = stakes[_seller][_stakeId];
// require(stake.amount == 0, "Stake not in sell state");
// // Restore the original stake's amount.
// // stake.amount = sellStakeEntry.amount;
// delete sellStakes[_seller][_stakeId];
// // Remove the key from the 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 StakeSaleCancelled(_seller, _stakeId);
// }
// function createStake(uint256 _amount) external {
// // Scale up for wei comparison, USDC is 1e6
// if (_amount * 1e12 <= minStakeLock) revert AmountBelowMinimum();
// // Transfer tokens from the user into the contract
// IERC20(pool.tokenAddress).safeTransferFrom(msg.sender, address(this), _amount);
// // Check if user has a fixed reward rate set
// uint256 finalRewardRate;
// if (addressFixedRate[msg.sender] > 0) {
// // Use the fixed rate
// finalRewardRate = addressFixedRate[msg.sender];
// } else {
// // Default logic, restake = false
// finalRewardRate = getUserRewardRate(msg.sender, false);
// }
// // Create the stake
// stakes[msg.sender].push(Stake({
// amount: _amount,
// lastClaimed: block.timestamp,
// dailyRewardRate: finalRewardRate,
// unlockTime: block.timestamp + pool.lockupPeriod,
// complete: false
// }));
// // Update total staked
// pool.totalStaked += _amount;
// emit Staked(msg.sender, _amount);
// }
// /// @notice Restake an expired stake with a bonus daily reward
// function restake(uint256 _stakeIndex, uint256 _restakePercentage) nonReentrant external {
// if (_restakePercentage > 100) revert InvalidRestakePercentage();
// Stake storage stake = stakes[msg.sender][_stakeIndex];
// // Ensure there is a stake to claim
// if (stake.amount == 0) revert NothingToClaim();
// if (block.timestamp < stake.unlockTime) revert StakeLocked();
// uint256 _amount = stake.amount;
// uint rewards = getPoolRewards(msg.sender, _stakeIndex);
// _amount = _amount + rewards;
// uint256 restake_amount = (_amount * _restakePercentage) / 100;
// uint256 withdraw_amount = _amount - restake_amount;
// // Update state before external calls
// stake.amount = 0;
// stake.complete = true;
// // Process withdraw
// if (withdraw_amount > 0) {
// withdrawLiabilities += withdraw_amount;
// if (pool.totalStaked >= withdraw_amount) {
// pool.totalStaked -= withdraw_amount;
// } else {
// pool.totalStaked = 0;
// }
// // Create temporary the stake for the user to delay withdraw
// withdrawStake[msg.sender].push(WithdrawStake({
// stakeId: _stakeIndex,
// amount: withdraw_amount,
// unlockTime: block.timestamp + unlockDelay
// }));
// // Emit a detailed event
// emit RewardClaimed(msg.sender, withdraw_amount);
// }
// // Process restake
// if (restake_amount > 0) {
// // Check if user has a fixed reward rate set
// uint256 finalRewardRate;
// if (addressFixedRate[msg.sender] > 0) {
// // Use the fixed rate
// finalRewardRate = addressFixedRate[msg.sender];
// } else {
// // restake = true
// finalRewardRate = getUserRewardRate(msg.sender, true);
// }
// stakes[msg.sender].push(Stake({
// amount: restake_amount,
// lastClaimed: block.timestamp,
// dailyRewardRate: finalRewardRate,
// unlockTime: block.timestamp + pool.lockupPeriod,
// complete: false
// }));
// emit Staked(msg.sender, restake_amount);
// }
// }
function createStakeForUser(address _user, uint256 _amount) external onlyOwner {
if (_amount == 0) revert InvalidAmount();
stakes[_user].push(Stake({
amount: _amount,
lastClaimed: block.timestamp,
dailyRewardRate: pool.dailyRewardRate,
unlockTime: block.timestamp + pool.lockupPeriod,
complete: false
}));
pool.totalStaked = pool.totalStaked + _amount;
emit Staked(_user, _amount);
}
function createStakes(StakeInput[] calldata stakesInput) external onlyBot payable {
uint256 totalLength = stakesInput.length;
for (uint256 i; i < totalLength;) {
StakeInput calldata stakeInput = stakesInput[i];
// Update pool total
pool.totalStaked = pool.totalStaked + stakeInput.amount;
// Create the stake for the user
stakes[stakeInput.user].push(Stake({
amount: stakeInput.amount,
lastClaimed: stakeInput.lastClaimed,
dailyRewardRate: stakeInput.dailyRewardRate,
unlockTime: stakeInput.unlockTime,
complete: false
}));
unchecked {
++i;
}
}
}
/// @notice Calculates pending rewards for a specific user's stake
/// @dev Rewards only accumulate until the stake's unlockTime is reached
/// @param _user Address of the stake owner
/// @param _stakeIndex Index of the stake in the user's stakes array
/// @return Accumulated rewards since last claim, stopping at unlockTime if applicable
function getPoolRewards(address _user, uint _stakeIndex) public view returns (uint256) {
Stake storage stake = stakes[_user][_stakeIndex];
uint256 endTime = block.timestamp < stake.unlockTime ? block.timestamp : stake.unlockTime;
uint256 elapsedTime = endTime > stake.lastClaimed ? endTime - stake.lastClaimed : 0;
uint256 rewards = (stake.amount * stake.dailyRewardRate * elapsedTime) / 1 days / 10000;
return rewards;
}
function getUserRewardRate(address _user, bool isRestake) public view returns (uint256) {
uint256 finalRewardRate = pool.dailyRewardRate + getBoost(dollarsVested[_user]);
if (isRestake) {
finalRewardRate += restakeBonus;
}
return finalRewardRate;
}
// function claimRewards() external nonReentrant {
// uint256 totalReward = 0;
// for (uint256 i = 0; i < stakes[msg.sender].length; ++i) {
// Stake storage stake = stakes[msg.sender][i];
// if (stake.amount > 0) {
// uint rewards = getPoolRewards(msg.sender, i);
// totalReward = totalReward + rewards;
// stake.lastClaimed = block.timestamp;
// }
// }
// if (totalReward == 0) revert NothingToClaim();
// if (pool.totalRewards < totalReward) revert InsufficientRewards();
// pool.totalRewards = pool.totalRewards - totalReward;
// IERC20(pool.tokenAddress).safeTransfer(msg.sender, totalReward);
// emit RewardClaimed(msg.sender, totalReward);
// }
// function claimStake(uint256 _stakeIndex) external nonReentrant {
// // Ensure the stake index is valid
// if (_stakeIndex >= stakes[msg.sender].length) revert InvalidStakeIndex();
// // Load the stake
// Stake storage stake = stakes[msg.sender][_stakeIndex];
// uint256 _amount = stake.amount;
// uint rewards = getPoolRewards(msg.sender, _stakeIndex);
// _amount = _amount + rewards;
// // Ensure there is a stake to claim
// if (_amount == 0) revert NothingToClaim();
// // Ensure the stake is unlocked (if using lockup periods)
// if (block.timestamp < stake.unlockTime) revert StakeLocked();
// // Update state before external calls
// stake.amount = 0;
// stake.complete = true;
// withdrawLiabilities += _amount;
// if (pool.totalStaked >= _amount) {
// pool.totalStaked -= _amount;
// } else {
// pool.totalStaked = 0;
// }
// // Create temporary the stake for the user to delay withdraw
// withdrawStake[msg.sender].push(WithdrawStake({
// stakeId: _stakeIndex,
// amount: _amount,
// unlockTime: block.timestamp + unlockDelay
// }));
// // Emit a detailed event
// emit RewardClaimed(msg.sender, _amount);
// }
// /**
// * @notice Withdraw a staked amount after its unlock time has passed.
// * @dev Locates the stake by `_stakeIndex`, checks that it's unlocked and non-zero,
// * and transfers tokens to the caller. For vesting stakes (where `_stakeIndex` >= 1e6),
// * the stored amount (in 1e18 decimals) is scaled to USDC's 1e6 decimals by dividing by 1e12.
// *
// * Requirements:
// * - Caller must have at least one stake.
// * - The stake must exist, be unlocked, and have a non-zero amount.
// * - The contract must have sufficient token balance.
// *
// * @param _stakeIndex The identifier of the stake to withdraw.
// */
// function withdraw(uint256 _stakeIndex) external nonReentrant {
// WithdrawStake[] storage userStakes = withdrawStake[msg.sender];
// if (userStakes.length == 0) revert NoStakesAvailable();
//
// for (uint256 i = 0; i < userStakes.length; ++i) {
// WithdrawStake storage stake = userStakes[i];
// // Skip already withdrawn stakes (amount == 0)
// if (stake.stakeId == _stakeIndex && stake.amount != 0) {
// if (block.timestamp < stake.unlockTime) revert StakeLocked();
//
// uint256 _amount = stake.amount;
//
// // Convert vesting stake amount to USDC decimals.
// if (_stakeIndex >= 1e6) {
// _amount = _amount / 1e12;
// }
//
// uint256 poolBalance = IERC20(pool.tokenAddress).balanceOf(address(this));
// if (poolBalance < _amount) revert InsufficientRewards();
//
// // Update state before external calls
// // withdrawLiabilities is in 1e18, deduct original amount
// withdrawLiabilities -= stake.amount;
// stake.amount = 0;
//
// // Transfer tokens
// IERC20(pool.tokenAddress).safeTransfer(msg.sender, _amount);
// emit StakeWithdrawn(msg.sender, _amount, _stakeIndex);
// return;
// }
// }
//
// // Revert if no matching stake with non-zero amount was found
// revert StakeNotFound();
// }
// /**
// * @notice Withdraws vesting tokens after cooldown period
// * @param _vestingId The vesting ID to withdraw
// */
// function withdrawVestingToken(uint256 _vestingId) external nonReentrant {
// WithdrawVesting[] storage userVestings = withdrawVestingActual[msg.sender];
// if (userVestings.length == 0) revert NoStakesAvailable();
//
// for (uint256 i = 0; i < userVestings.length; ++i) {
// WithdrawVesting storage vestingWithdraw = userVestings[i];
// if (vestingWithdraw.vestingId == _vestingId && vestingWithdraw.amount != 0) {
// if (block.timestamp < vestingWithdraw.unlockTime) revert StakeLocked();
//
// uint256 _amount = vestingWithdraw.amount;
// address _token = vestingWithdraw.token;
//
// // Check contract has sufficient balance
// uint256 tokenBalance = IERC20(_token).balanceOf(address(this));
// if (tokenBalance < _amount) revert InsufficientRewards();
//
// // Update state before external calls
// vestingWithdraw.amount = 0;
//
// // Decrement withdraw vesting liabilities for this token
// withdrawVestingLiabilities[_token] -= _amount;
//
// // Transfer tokens
// IERC20(_token).safeTransfer(msg.sender, _amount);
// emit StakeWithdrawn(msg.sender, _amount, _vestingId);
// return;
// }
// }
//
// revert StakeNotFound();
// }
// function compoundAllRewards() external {
// uint256 totalReward = 0;
// for (uint256 i = 0; i < stakes[msg.sender].length; ++i) {
// Stake storage stake = stakes[msg.sender][i];
// if (stake.amount > 0) {
// uint rewards = getPoolRewards(msg.sender, i);
// totalReward = totalReward + rewards;
// stake.lastClaimed = block.timestamp;
// }
// }
// if (totalReward <= minStakeLock) revert NotEnoughToCompound();
//
// // Check if user has a fixed reward rate set
// uint256 finalRewardRate;
// if (addressFixedRate[msg.sender] > 0) {
// // Use the fixed rate
// finalRewardRate = addressFixedRate[msg.sender];
// } else {
// // Default logic, restake = false
// finalRewardRate = getUserRewardRate(msg.sender, false);
// }
// stakes[msg.sender].push(Stake({
// amount: totalReward,
// lastClaimed: block.timestamp,
// dailyRewardRate: finalRewardRate,
// unlockTime: block.timestamp + pool.lockupPeriod,
// complete: false
// }));
// pool.totalStaked = pool.totalStaked + totalReward;
// emit CompoundRewards(msg.sender, totalReward);
// }
// function createVesting(address _token, uint256 _amount) external {
// if (_amount == 0) revert InvalidAmount();
// address oracle = priceOracles[_token];
// if (oracle == address(0)) revert PriceOracleNotSet();
// IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
// uint256 bonus = (_amount * BONUS_PERCENTAGE) / 100;
// uint256 usdPrice = (iPriceOracle(priceOracles[_token]).getLatestPrice(_token) * _amount) / 1e18;
// if (usdPrice <= minStakeLock) revert AmountBelowMinimum();
// // Update user's dollarsVested
// dollarsVested[msg.sender] += usdPrice;
// // Update token's vestedTotal
// vestedTotal[_token] += _amount;
// vestings[msg.sender].push(Vesting({
// amount: _amount,
// bonus: bonus,
// lockedUntil: block.timestamp + lockupDuration,
// claimedAmount: 0,
// claimedBonus: 0,
// lastClaimed: block.timestamp,
// createdAt: block.timestamp,
// token: _token,
// complete: false,
// usdAmount: usdPrice
// }));
// emit VestingCreated(msg.sender, _amount, bonus);
// }
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;
}
// function claimVesting(uint256 _vestingIndex) external nonReentrant {
// Vesting storage vesting = vestings[msg.sender][_vestingIndex];
// if (vesting.complete) revert StakeComplete();
// uint256 maxClaim = getUnlockedVesting(msg.sender, _vestingIndex);
// if (maxClaim < vesting.claimedAmount) revert InvalidClaimAmount();
// uint256 amountToClaim = maxClaim - vesting.claimedAmount;
// if (amountToClaim == 0) revert NothingToClaim();
// vesting.claimedAmount = 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 cooldown queue
// withdrawVestingActual[msg.sender].push(WithdrawVesting({
// vestingId: withdrawVestingCounterActual++,
// 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);
// }
// function claimAllVestingByToken(address _token) external nonReentrant {
// uint256 totalReward = 0;
// uint256 vestingsProcessed = 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) revert InvalidClaimAmount();
// uint256 amountToClaim = maxClaim - vesting.claimedAmount;
// if (amountToClaim > 0) {
// vesting.claimedAmount = vesting.claimedAmount + amountToClaim;
// totalReward = totalReward + amountToClaim;
// vesting.lastClaimed = block.timestamp;
// // Mark vesting as complete if fully claimed
// if (vesting.claimedAmount >= vesting.amount) {
// vesting.complete = true;
// }
// vestingsProcessed++;
// }
// }
// }
// if (totalReward == 0) revert NothingToClaim();
// // 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;
// }
// }
// // Update vesting total
// vestedTotal[_token] -= totalReward;
//
// // Add vesting claims to cooldown queue
// withdrawVestingActual[msg.sender].push(WithdrawVesting({
// vestingId: withdrawVestingCounterActual++,
// amount: totalReward,
// unlockTime: block.timestamp + unlockDelay,
// token: _token
// }));
//
// // Increment withdraw vesting liabilities for this token
// withdrawVestingLiabilities[_token] += totalReward;
// emit RewardClaimed(msg.sender, totalReward);
// }
// function claimBonus(uint256 _vestingIndex) external nonReentrant {
// Vesting storage vesting = vestings[msg.sender][_vestingIndex];
// uint256 maxBonus = getUnlockedVestingBonus(msg.sender, _vestingIndex);
// if (maxBonus < vesting.claimedBonus) revert InvalidClaimAmount();
// uint256 bonusToClaim = maxBonus - vesting.claimedBonus;
// if (bonusToClaim == 0) revert NothingToClaim();
// vesting.claimedBonus = vesting.claimedBonus + bonusToClaim;
// withdrawLiabilities += bonusToClaim;
// // IERC20(vesting.token).safeTransfer(msg.sender, bonusToClaim);
// // Create temporary the stake for the user to delay withdraw.
// // Add 1e6 to the vesting index to distinguish them from normal stakes.
// withdrawStake[msg.sender].push(WithdrawStake({
// stakeId: _vestingIndex + 1e6,
// amount: bonusToClaim,
// unlockTime: block.timestamp + unlockDelay
// }));
// emit BonusClaimed(msg.sender, bonusToClaim);
// }
function setPriceOracle(address _token, address _oracle) external onlyOwner {
priceOracles[_token] = _oracle;
}
function viewRewards(address _user) external view returns (uint256) {
uint256 totalReward = 0;
for (uint256 i = 0; i < stakes[_user].length; ++i) {
uint rewards = getPoolRewards(_user, i);
totalReward = totalReward + rewards;
}
return totalReward;
}
/// @notice View function to get all stakes for a specific address
function getStakes(address user) external view returns (Stake[] memory) {
return stakes[user];
}
/// @notice View function to get all vestings for a specific address
function getVestings(address user) external view returns (Vesting[] memory) {
return vestings[user];
}
/// @notice View to monitor contract pool deficits
function getPoolStatus() external view returns (uint256) {
uint256 poolBalance = IERC20(pool.tokenAddress).balanceOf(address(this));
// If the balance is greater than or equal to liabilities, return 0
if (poolBalance >= withdrawLiabilities) {
return 0;
}
// Otherwise, return the deficit (amount needed to cover liabilities)
return withdrawLiabilities - poolBalance;
}
/**
* @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 tokens 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 tokens 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 Function that lets you look up an addresss stake by stakeId.
/// @param user The address to evaluate.
/// @param _stakeId The stakeId of the ORIGINAL stake that is waiting to be unlocked
function getWithdrawStake(address user, uint256 _stakeId) external view returns (WithdrawStake memory) {
WithdrawStake[] storage userStakes = withdrawStake[user];
for (uint256 i = 0; i < userStakes.length; i++) {
if (userStakes[i].stakeId == _stakeId) {
return userStakes[i];
}
}
revert StakeNotFound();
}
/// @notice Function that lets you look up an addresss stake by vestingId.
/// @param user The address to evaluate.
/// @param _vestingId The vestingId of the ORIGINAL vest that is waiting to be unlocked
function getVestingWithdrawStake(address user, uint256 _vestingId) external view returns (WithdrawStake memory) {
WithdrawStake[] storage userStakes = withdrawStake[user];
uint256 boostedVestingId = _vestingId + 1e6;
for (uint256 i = 0; i < userStakes.length; i++) {
if (userStakes[i].stakeId == boostedVestingId) {
return userStakes[i];
}
}
revert StakeNotFound();
}
/// @notice Function that returns an array of all the user's withdrawStakes.
/// @param user The address to evaluate.
/// @return An array of WithdrawStake for the given user.
function getAllWithdrawStakes(address user) external view returns (WithdrawStake[] memory) {
return withdrawStake[user];
}
/// @notice Function that returns an array of all the user's withdrawVestings.
/// @param user The address to evaluate.
/// @return An array of WithdrawVesting for the given user.
function getAllWithdrawVestings(address user) external view returns (WithdrawVesting[] memory) {
return withdrawVestingActual[user];
}
/// @notice Returns the current withdraw vesting counter value
/// @return Current counter value for tracking unique withdrawal IDs
function getWithdrawVestingCounter() external view returns (uint256) {
return withdrawVestingCounterActual;
}
/// @notice Function to put a stake for sale.
/// Sets the original stake amount to 0 to prevent any alterations while for sale.
/// @param _stakeId The stake to sell.
/// @param price The price of the stake.
function sellStake(uint256 _stakeId, uint256 price) external {
Stake storage stake = stakes[msg.sender][_stakeId];
if (stake.complete) revert StakeComplete();
if (stake.amount == 0) revert InvalidAmount();
// Ensure the stake isn't already on sale.
if (sellStakes[msg.sender][_stakeId].amount != 0) revert StakeAlreadyOnSale();
if (price < (stake.amount * sellMin) / 100) revert PriceTooLow();
// Create a SellStake entry directly in the mapping.
sellStakes[msg.sender][_stakeId] = SellStake({
price: price,
bonusAmount: (price * sellKickBack) / 100,
amount: stake.amount,
lastClaimed: stake.lastClaimed,
dailyRewardRate: stake.dailyRewardRate,
origUnlockTime: stake.unlockTime
});
// Lock the original stake by setting its amount to 0.
stake.amount = 0;
// Add the key to the iteration array.
sellStakeKeys.push(SellStakeKey({ seller: msg.sender, stakeId: _stakeId }));
sellStakeKeyIndex[msg.sender][_stakeId] = sellStakeKeys.length - 1;
emit StakeUpForSale(msg.sender, price, _stakeId);
}
/// @notice Function to cancel a sell stake.
/// Restores the stake amount to the original stake and removes the sell stake.
/// @param _stakeId The stake ID to cancel the sale.
function cancelSellStake(uint256 _stakeId) external {
SellStake storage sellStakeEntry = sellStakes[msg.sender][_stakeId];
if (sellStakeEntry.amount == 0) revert StakeNotFound();
// Access the original stake.
Stake storage stake = stakes[msg.sender][_stakeId];
if (stake.amount != 0) revert StakeNotInSellState();
// Restore the original stake's amount.
stake.amount = sellStakeEntry.amount;
delete sellStakes[msg.sender][_stakeId];
// Remove the key from the 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 Function to update the price of a stake that is for sale.
/// @param _stakeId The stake ID to update.
/// @param newPrice The new price of the stake.
function updateSellStake(uint256 _stakeId, uint256 newPrice) external {
SellStake storage sellStakeEntry = sellStakes[msg.sender][_stakeId];
if (sellStakeEntry.amount == 0) revert StakeNotFound();
if (newPrice < (sellStakeEntry.amount * sellMin) / 100) revert PriceTooLow();
sellStakeEntry.bonusAmount = (newPrice * sellKickBack) / 100;
sellStakeEntry.price = newPrice;
emit StakeUpForSale(msg.sender, newPrice, _stakeId);
}
/// @notice Buys a sell stake.
/// Transfers the sale price from the buyer (using safeTransferFrom),
/// pays the seller (applying the sellTax),
/// creates a new stake for the buyer (amount = original amount + bonus),
/// marks the original stake as complete,
/// and sets the new stake's unlock time.
/// @param seller The address of the seller.
/// @param _stakeId The original stake id associated with the sell stake.
function buySellStake(address seller, uint256 _stakeId) external nonReentrant {
SellStake storage sellStakeEntry = sellStakes[seller][_stakeId];
if (sellStakeEntry.amount == 0) revert StakeNotFound();
// Transfer the sale price from the buyer to this contract.
IERC20(pool.tokenAddress).safeTransferFrom(msg.sender, address(this), sellStakeEntry.price);
// Calculate the seller's payment using the sell tax.
uint256 sellerPayment = (sellStakeEntry.price * (100 - sellTax)) / 100;
IERC20(pool.tokenAddress).safeTransfer(seller, sellerPayment);
// Mark the original stake as complete.
Stake storage originalStake = stakes[seller][_stakeId];
originalStake.complete = true;
// Create the new stake for the buyer using the inline push pattern.
stakes[msg.sender].push(Stake({
amount: sellStakeEntry.amount + sellStakeEntry.bonusAmount,
lastClaimed: sellStakeEntry.lastClaimed,
dailyRewardRate: sellStakeEntry.dailyRewardRate,
unlockTime: block.timestamp + pool.lockupPeriod,
complete: false
}));
// Remove the sell stake listing.
delete sellStakes[seller][_stakeId];
// Remove the key from the iteration array.
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, sellStakeEntry.price, _stakeId);
}
/// @notice Admin function to clear all sellStakes for a specific address
/// @dev Only callable by contract owners. Restores original stakes and removes all sell listings.
/// @param _address The address whose sellStakes should be cleared
function clearAllSellStakes(address _address) external onlyOwner {
// First, collect all stakeIds for this address from the sellStakeKeys array
uint256[] memory stakeIdsToRemove = new uint256[](sellStakeKeys.length);
uint256 stakeCount = 0;
// Find all stakeIds belonging to the target address
for (uint256 i = 0; i < sellStakeKeys.length; i++) {
if (sellStakeKeys[i].seller == _address) {
stakeIdsToRemove[stakeCount] = sellStakeKeys[i].stakeId;
stakeCount++;
}
}
// Process each stake found
for (uint256 i = 0; i < stakeCount; i++) {
uint256 stakeId = stakeIdsToRemove[i];
SellStake storage sellStakeEntry = sellStakes[_address][stakeId];
// Skip if already cleared
if (sellStakeEntry.amount == 0) continue;
// Restore the original stake
Stake storage originalStake = stakes[_address][stakeId];
originalStake.amount = sellStakeEntry.amount;
// Clear the sell stake entry
delete sellStakes[_address][stakeId];
// Remove from sellStakeKeys array using swap-and-pop
uint256 keyIndex = sellStakeKeyIndex[_address][stakeId];
uint256 lastIndex = sellStakeKeys.length - 1;
if (keyIndex != lastIndex) {
SellStakeKey memory lastKey = sellStakeKeys[lastIndex];
sellStakeKeys[keyIndex] = lastKey;
sellStakeKeyIndex[lastKey.seller][lastKey.stakeId] = keyIndex;
}
sellStakeKeys.pop();
delete sellStakeKeyIndex[_address][stakeId];
emit StakeSaleCancelled(_address, stakeId);
}
}
/// @notice Returns all active sell stakes with their keys and pending rewards.
/// @return sellers Array of seller addresses for each stake
/// @return stakeIds Array of stake IDs corresponding to each seller
/// @return sellStakeData Array of SellStake structs containing the sell stake data
/// @return pendingRewards Array of pending rewards for each stake
function getAllSellStakesWithKeys() external view returns (
address[] memory sellers,
uint256[] memory stakeIds,
SellStake[] memory sellStakeData,
uint256[] memory pendingRewards
) {
uint256 length = sellStakeKeys.length;
sellers = new address[](length);
stakeIds = new uint256[](length);
sellStakeData = new SellStake[](length);
pendingRewards = new uint256[](length);
for (uint256 i = 0; i < length; i++) {
SellStakeKey memory key = sellStakeKeys[i];
sellers[i] = key.seller;
stakeIds[i] = key.stakeId;
// Copy the SellStake struct from storage to memory
SellStake storage sourceStake = sellStakes[key.seller][key.stakeId];
sellStakeData[i] = SellStake({
price: sourceStake.price,
bonusAmount: sourceStake.bonusAmount,
amount: sourceStake.amount,
lastClaimed: sourceStake.lastClaimed,
dailyRewardRate: sourceStake.dailyRewardRate,
origUnlockTime: sourceStake.origUnlockTime
});
// Calculate pending rewards based on the provided logic
// Stop accumulating rewards past the unlockTime
uint256 endTime = block.timestamp < sourceStake.origUnlockTime ? block.timestamp : sourceStake.origUnlockTime;
uint256 elapsedTime = endTime > sourceStake.lastClaimed ? endTime - sourceStake.lastClaimed : 0;
pendingRewards[i] = (sourceStake.amount * sourceStake.dailyRewardRate * elapsedTime) / 1 days / 10000;
}
return (sellers, stakeIds, sellStakeData, pendingRewards);
}
}