1438 lines
55 KiB
Solidity
1438 lines
55 KiB
Solidity
// 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 PacaFinanceWithBoostAndScheduleBase 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 = 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 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 888;
|
||
}
|
||
|
||
|
||
|
||
/// @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);
|
||
}
|
||
|
||
} |