// 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 PacaFinanceWithBoostAndScheduleBsc 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; 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; 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 = 0x55d398326f99059fF775485246999027B3197955; // 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 onlyBot { 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 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 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 Creates a withdraw stake for a given user /// @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 _stakeIndex The stake index to reference function createWithdrawStake(address user, uint256 amount, uint256 unlockTime, uint256 _stakeIndex) external onlyBot { withdrawStake[user].push(WithdrawStake({ stakeId: _stakeIndex, amount: amount, unlockTime: unlockTime })); withdrawLiabilities += amount; } /// @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 { 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 { // if (_amount <= 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. // * // * 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]; // if (stake.stakeId == _stakeIndex && stake.amount != 0) { // if (block.timestamp < stake.unlockTime) revert StakeLocked(); // // uint256 _amount = stake.amount; // uint256 poolBalance = IERC20(pool.tokenAddress).balanceOf(address(this)); // if (poolBalance < _amount) revert InsufficientRewards(); // // // Update state before external calls // withdrawLiabilities -= _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 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 Function that lets you look up an address’s 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 address’s 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 Test function for upgrade verification /// @return Returns a constant value to verify upgrade worked function testUpgradeFunction() external pure returns (uint256) { return 788; } /// @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 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); } }