From 945b69dedab388a5c7ba64d7ba0373c2fed66872 Mon Sep 17 00:00:00 2001 From: sascha Date: Tue, 15 Jul 2025 23:42:01 +0200 Subject: [PATCH] Currently deployed, has vesting tracker --- contracts/base_paca.sol | 48 +- contracts/bsc_paca.sol | 49 +- contracts/sonic_paca.sol | 1435 ++++++++++++++++++++++++++++++++++++++ hardhat.config.js | 40 +- scripts/deployProxy.js | 43 +- scripts/manualUpgrade.js | 112 +++ 6 files changed, 1664 insertions(+), 63 deletions(-) create mode 100644 contracts/sonic_paca.sol create mode 100644 scripts/manualUpgrade.js diff --git a/contracts/base_paca.sol b/contracts/base_paca.sol index 37701ad..cb349eb 100644 --- a/contracts/base_paca.sol +++ b/contracts/base_paca.sol @@ -36,7 +36,7 @@ error StakeNotInSellState(); // File: paca.sol -contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUpgradeable { +contract PacaFinanceWithBoostAndScheduleBase is Initializable, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; @@ -136,8 +136,6 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp uint256 public unlockDelay; uint256 public withdrawLiabilities; mapping(address => WithdrawStake[]) public withdrawStake; - mapping(address => WithdrawVesting[]) public withdrawVesting; - uint256 private withdrawVestingCounter; uint256 public restakeBonus; mapping(address => uint256) public addressFixedRate; mapping(address => mapping(uint256 => SellStake)) public sellStakes; @@ -146,6 +144,12 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp 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); @@ -451,14 +455,14 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp dollarsVested[oldAddress] = 0; // Migrate pending vesting withdrawals - WithdrawVesting[] storage oldWithdrawVestings = withdrawVesting[oldAddress]; + WithdrawVesting[] storage oldWithdrawVestings = withdrawVestingActual[oldAddress]; uint256 withdrawVestingCount = oldWithdrawVestings.length; if (withdrawVestingCount > 0) { - WithdrawVesting[] storage newWithdrawVestings = withdrawVesting[newAddress]; + WithdrawVesting[] storage newWithdrawVestings = withdrawVestingActual[newAddress]; for (uint256 i = 0; i < withdrawVestingCount; i++) { newWithdrawVestings.push(oldWithdrawVestings[i]); } - delete withdrawVesting[oldAddress]; + delete withdrawVestingActual[oldAddress]; } } @@ -824,7 +828,7 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { * @param _vestingId The vesting ID to withdraw */ function withdrawVestingToken(uint256 _vestingId) external nonReentrant { - WithdrawVesting[] storage userVestings = withdrawVesting[msg.sender]; + WithdrawVesting[] storage userVestings = withdrawVestingActual[msg.sender]; if (userVestings.length == 0) revert NoStakesAvailable(); for (uint256 i = 0; i < userVestings.length; ++i) { @@ -842,6 +846,9 @@ function withdrawVestingToken(uint256 _vestingId) external nonReentrant { // 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); @@ -849,7 +856,6 @@ function withdrawVestingToken(uint256 _vestingId) external nonReentrant { } } - // Revert if no matching vesting with non-zero amount was found revert StakeNotFound(); } @@ -1009,12 +1015,15 @@ function withdrawVestingToken(uint256 _vestingId) external nonReentrant { vestedTotal[vesting.token] -= amountToClaim; // Add vesting claims to cooldown queue - withdrawVesting[msg.sender].push(WithdrawVesting({ - vestingId: withdrawVestingCounter++, + 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); } @@ -1062,12 +1071,15 @@ function withdrawVestingToken(uint256 _vestingId) external nonReentrant { vestedTotal[_token] -= totalReward; // Add vesting claims to cooldown queue - withdrawVesting[msg.sender].push(WithdrawVesting({ - vestingId: withdrawVestingCounter++, + 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); } @@ -1237,15 +1249,23 @@ function withdrawVestingToken(uint256 _vestingId) external nonReentrant { /// @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 withdrawVesting[user]; + 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 withdrawVestingCounter; + 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. diff --git a/contracts/bsc_paca.sol b/contracts/bsc_paca.sol index 4f2055b..f7abaef 100644 --- a/contracts/bsc_paca.sol +++ b/contracts/bsc_paca.sol @@ -36,7 +36,7 @@ error StakeNotInSellState(); // File: paca.sol -contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUpgradeable { +contract PacaFinanceWithBoostAndScheduleBsc is Initializable, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; @@ -136,8 +136,6 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp uint256 public unlockDelay; uint256 public withdrawLiabilities; mapping(address => WithdrawStake[]) public withdrawStake; - mapping(address => WithdrawVesting[]) public withdrawVesting; - uint256 private withdrawVestingCounter; uint256 public restakeBonus; mapping(address => uint256) public addressFixedRate; mapping(address => mapping(uint256 => SellStake)) public sellStakes; @@ -146,6 +144,12 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp 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); @@ -207,6 +211,7 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp 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(); @@ -226,6 +231,7 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp emit PoolUpdated(_lockupPeriod, _dailyRewardRate); } + function depositRewards(uint256 _amount) external onlyOwner { IERC20(pool.tokenAddress).safeTransferFrom(msg.sender, address(this), _amount); pool.totalRewards = pool.totalRewards + _amount; @@ -449,14 +455,14 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp dollarsVested[oldAddress] = 0; // Migrate pending vesting withdrawals - WithdrawVesting[] storage oldWithdrawVestings = withdrawVesting[oldAddress]; + WithdrawVesting[] storage oldWithdrawVestings = withdrawVestingActual[oldAddress]; uint256 withdrawVestingCount = oldWithdrawVestings.length; if (withdrawVestingCount > 0) { - WithdrawVesting[] storage newWithdrawVestings = withdrawVesting[newAddress]; + WithdrawVesting[] storage newWithdrawVestings = withdrawVestingActual[newAddress]; for (uint256 i = 0; i < withdrawVestingCount; i++) { newWithdrawVestings.push(oldWithdrawVestings[i]); } - delete withdrawVesting[oldAddress]; + delete withdrawVestingActual[oldAddress]; } } @@ -812,7 +818,7 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp * @param _vestingId The vesting ID to withdraw */ function withdrawVestingToken(uint256 _vestingId) external nonReentrant { - WithdrawVesting[] storage userVestings = withdrawVesting[msg.sender]; + WithdrawVesting[] storage userVestings = withdrawVestingActual[msg.sender]; if (userVestings.length == 0) revert NoStakesAvailable(); for (uint256 i = 0; i < userVestings.length; ++i) { @@ -830,6 +836,9 @@ function withdrawVestingToken(uint256 _vestingId) external nonReentrant { // 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); @@ -837,7 +846,6 @@ function withdrawVestingToken(uint256 _vestingId) external nonReentrant { } } - // Revert if no matching vesting with non-zero amount was found revert StakeNotFound(); } @@ -997,12 +1005,15 @@ function withdrawVestingToken(uint256 _vestingId) external nonReentrant { vestedTotal[vesting.token] -= amountToClaim; // Add vesting claims to cooldown queue - withdrawVesting[msg.sender].push(WithdrawVesting({ - vestingId: withdrawVestingCounter++, + 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); } @@ -1050,12 +1061,15 @@ function withdrawVestingToken(uint256 _vestingId) external nonReentrant { vestedTotal[_token] -= totalReward; // Add vesting claims to cooldown queue - withdrawVesting[msg.sender].push(WithdrawVesting({ - vestingId: withdrawVestingCounter++, + 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); } @@ -1225,15 +1239,22 @@ function withdrawVestingToken(uint256 _vestingId) external nonReentrant { /// @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 withdrawVesting[user]; + 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 withdrawVestingCounter; + return withdrawVestingCounterActual; } + /// @notice Test function for upgrade verification + /// @return Returns a constant value to verify upgrade worked + function testUpgradeFunction() external pure returns (uint256) { + return 777; + } + + /// @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. diff --git a/contracts/sonic_paca.sol b/contracts/sonic_paca.sol new file mode 100644 index 0000000..a663d35 --- /dev/null +++ b/contracts/sonic_paca.sol @@ -0,0 +1,1435 @@ +// SPDX-License-Identifier: MIT +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +pragma solidity ^0.8.20; + +interface iPriceOracle { + // returns price in USD + function getLatestPrice(address token) external view returns (uint256); +} + +// Custom errors for gas optimization +error NotAuthorized(); +error AlreadyOwner(); +error NotOwner(); +error CannotRemoveSelf(); +error InvalidAddress(); +error InvalidAmount(); +error AmountBelowMinimum(); +error InvalidRestakePercentage(); +error StakeLocked(); +error StakeComplete(); +error NothingToClaim(); +error InvalidClaimAmount(); +error InsufficientRewards(); +error InvalidStakeIndex(); +error NoStakesAvailable(); +error StakeNotFound(); +error NotEnoughToCompound(); +error PriceOracleNotSet(); +error StakeAlreadyOnSale(); +error PriceTooLow(); +error StakeNotInSellState(); + +// File: paca.sol + +contract PacaFinanceWithBoostAndScheduleSonic is Initializable, ReentrancyGuardUpgradeable { + using SafeERC20 for IERC20; + + + // Struct Definitions + struct Pool { + uint256 lockupPeriod; + uint256 dailyRewardRate; + uint256 totalStaked; + uint256 totalRewards; + address tokenAddress; + } + + struct Stake { + uint256 amount; + uint256 lastClaimed; + uint256 dailyRewardRate; + uint256 unlockTime; + bool complete; + } + + struct StakeInput { + address user; + uint256 amount; + uint256 lastClaimed; + uint256 unlockTime; + uint256 dailyRewardRate; + } + + struct Vesting { + uint256 amount; + uint256 bonus; + uint256 lockedUntil; + uint256 claimedAmount; + uint256 claimedBonus; + uint256 lastClaimed; + uint256 createdAt; + address token; + bool complete; + uint256 usdAmount; + } + + struct UnlockStep { + uint256 timeOffset; + uint256 percentage; + } + + struct BoostRange { + uint256 minTokens; + uint256 maxTokens; + uint256 boostPercentage; + } + + struct WithdrawStake { + uint256 stakeId; + uint256 amount; + uint256 unlockTime; + } + + struct WithdrawVesting { + uint256 vestingId; + uint256 amount; + uint256 unlockTime; + address token; + } + + struct SellStake { + uint256 price; + uint256 bonusAmount; + uint256 amount; + uint256 lastClaimed; + uint256 dailyRewardRate; + uint256 origUnlockTime; + } + + struct SellStakeKey { + address seller; + uint256 stakeId; + } + + BoostRange[] public boosttiers; + + // Contract Variables + Pool public pool; + address public owner; + mapping(address => bool) public owners; + mapping(address => Stake[]) public stakes; + mapping(address => Vesting[]) public vestings; + mapping(address => UnlockStep[]) public unlockSchedules; + mapping(address => address) public priceOracles; + mapping(address => uint256) public dollarsVested; // per user address + uint256 public lockupDuration; + uint256 public minStakeLock; + uint256 private constant BONUS_PERCENTAGE = 10; + + mapping(address => bool) public authorizedBots; + mapping(address => uint256) public vestedTotal; // per vesting token + uint256 public unlockDelay; + uint256 public withdrawLiabilities; + mapping(address => WithdrawStake[]) public withdrawStake; + mapping(address => WithdrawVesting[]) private withdrawVesting; // Keep for storage layout compatibility - DO NOT USE + uint256 private withdrawVestingCounter; // Keep for storage layout compatibility - DO NOT USE + uint256 public restakeBonus; + mapping(address => uint256) public addressFixedRate; + mapping(address => mapping(uint256 => SellStake)) public sellStakes; + uint256 public sellTax; + uint256 public sellKickBack; + SellStakeKey[] public sellStakeKeys; + mapping(address => mapping(uint256 => uint256)) private sellStakeKeyIndex; + uint256 public sellMin; + + // Moved to end to fix storage layout corruption - vesting withdrawal functionality + mapping(address => WithdrawVesting[]) private withdrawVestingActual; + uint256 private withdrawVestingCounterActual; + + // Track total withdraw vesting liabilities by token address + mapping(address => uint256) public withdrawVestingLiabilities; + + // Events + event Staked(address indexed user, uint256 amount); + event RewardClaimed(address indexed user, uint256 reward); + event VestingCreated(address indexed user, uint256 amount, uint256 bonus); + event VestingClaimed(address indexed user, uint256 amount, uint256 bonus); + event BonusClaimed(address indexed user, uint256 bonus); + event PoolUpdated(uint256 lockupPeriod, uint256 dailyRewardRate); + event UnlockScheduleSet(address indexed token); + event FundsWithdrawn(address indexed owner, address indexed token, uint256 amount); + event RewardsDeposited(uint256 amount); + event CompoundRewards(address indexed user, uint256 amount); + event MinStakeLockUpdated(uint256 amount); + event StakeWithdrawn(address indexed user, uint256 amount, uint256 stakeId); + event StakeUpForSale(address indexed user, uint256 saleAmount, uint256 stakeId); + event StakeSaleCancelled(address indexed user, uint256 stakeId); + event StakeSold(address indexed seller, address indexed buyer,uint256 saleAmount, uint256 stakeId); + + // Modifiers + modifier onlyOwner() { + if (!owners[msg.sender]) revert NotAuthorized(); + _; + } + modifier onlyBot() { + if (!authorizedBots[msg.sender]) revert NotAuthorized(); + _; + } + + function initialize() public initializer { + __ReentrancyGuard_init(); // Initialize ReentrancyGuardUpgradeable + owner = 0x41970Ce76b656030A79E7C1FA76FC4EB93980255; + owners[0x41970Ce76b656030A79E7C1FA76FC4EB93980255] = true; + + // lockupDuration = 250 days; + // minStakeLock = 16 ether; + + // pool.tokenAddress = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + // pool.lockupPeriod = 250 * 1 days; + // pool.dailyRewardRate = 33; + + // // Price oracle for a specific tokens + // // priceOracles[0x940181a94A35A4569E4529A3CDfB74e38FD98631] = 0x0Dde1b42F7B3891C9731280A74081501729A73c5; + // authorizedBots[0xbf12D3b827a230F7390EbCc9b83b289FdC98ba81] = true; + // authorizedBots[0x7c40f272570fdf9549d6f67493aC250a1DB52F27] = true; + + // unlockDelay = 60 * 60 * 36; + // restakeBonus = 3; + } + + // Ownership Management + function addOwner(address _newOwner) external onlyOwner { + if (owners[_newOwner]) revert AlreadyOwner(); + owners[_newOwner] = true; + } + + function removeOwner(address _owner) external onlyOwner { + if (!owners[_owner]) revert NotOwner(); + if (_owner == msg.sender) revert CannotRemoveSelf(); + owners[_owner] = false; + } + + + /// @notice Function to add a bot to the list (only callable by the contract owner) + function addBot(address bot) external onlyOwner { + if (bot == address(0)) revert InvalidAddress(); + authorizedBots[bot] = true; + } + + // /// @notice Function to remove a bot from the list (only callable by the contract owner) + // function removeBot(address bot) external onlyOwner { + // require(bot != address(0), "Invalid address"); + // authorizedBots[bot] = false; + // } + + // Admin Functions + function updatePool(uint256 _lockupPeriod, uint256 _dailyRewardRate) external onlyOwner { + pool.lockupPeriod = _lockupPeriod * 1 days; + pool.dailyRewardRate = _dailyRewardRate; + emit PoolUpdated(_lockupPeriod, _dailyRewardRate); + } + + + function depositRewards(uint256 _amount) external onlyOwner { + IERC20(pool.tokenAddress).safeTransferFrom(msg.sender, address(this), _amount); + pool.totalRewards = pool.totalRewards + _amount; + emit RewardsDeposited(_amount); + } + + function updateStakeMin(uint256 _amount) external onlyOwner { + minStakeLock = _amount; + emit MinStakeLockUpdated(_amount); + } + + function updateUnlockDelay(uint256 _delay) external onlyOwner { + unlockDelay = _delay; + } + + function updatePoolRewards(uint256 _amount) external onlyOwner { + pool.totalRewards = _amount; + } + + function updateRestakeBonus(uint256 _newBonus) external onlyOwner { + restakeBonus = _newBonus; + } + + /// @notice New Stake Sell Tax + /// @param _newTax The rate expressed in 2 digits, ex: 20 + function updateSellTax(uint256 _newTax) external onlyOwner { + sellTax = _newTax; + } + + /// @notice New Stake Sell Minimum + /// @param _newMin The rate expressed in 2 digits, ex: 30 + function updateSellMin(uint256 _newMin) external onlyOwner { + sellMin = _newMin; + } + + /// @notice New Stake Sell kickback to the buyer + /// @param _newKickback The rate expressed in 2 digits, ex: 5 + function updatesellKickBack(uint256 _newKickback) external onlyOwner { + sellKickBack = _newKickback; + } + + /// @notice Function to add an address to have a fixed daily reward (only callable by the contract owner) + /// @param _addr The address to give a fixed rate + /// @param _rate The fixed rate expressed in 2 digits, ex: 40 + function addFixedRate(address _addr, uint _rate) external onlyOwner { + if (_addr == address(0)) revert InvalidAddress(); + addressFixedRate[_addr] = _rate; + } + + /// @notice Function to remove an address' fixed daily reward (only callable by the contract owner) + /// @param _addr The address to 0 out + function removeFixedRate(address _addr) external onlyOwner { + if (_addr == address(0)) revert InvalidAddress(); + addressFixedRate[_addr] = 0; + } + + // /// @notice Add or edit a tier range *commented out for size constraints* + // function addOrEditTier(uint256 minTokens, uint256 maxTokens, uint256 boostPercentage) public onlyOwner { + // require(minTokens < maxTokens, "Invalid range: minTokens must be < maxTokens"); + // require(!rangesOverlap(minTokens, maxTokens), "Range overlaps with existing tiers"); + + // // Check if editing an existing range + // for (uint256 i = 0; i < boosttiers.length; ++i) { + // if (boosttiers[i].minTokens == minTokens && boosttiers[i].maxTokens == maxTokens) { + // // Edit the existing range + // boosttiers[i].boostPercentage = boostPercentage; + // return; + // } + // } + + // // Add new range + // boosttiers.push(BoostRange(minTokens, maxTokens, boostPercentage)); + + // // Sort the ranges after adding + // sortRanges(); + // } + + // // Check for range overlap + // function rangesOverlap(uint256 minTokens, uint256 maxTokens) internal view returns (bool) { + // for (uint256 i = 0; i < boosttiers.length; ++i) { + // if (minTokens <= boosttiers[i].maxTokens && maxTokens >= boosttiers[i].minTokens) { + // return true; + // } + // } + // return false; + // } + + // /// @notice Sort ranges by minTokens + // function sortRanges() internal { + // for (uint256 i = 0; i < boosttiers.length; ++i) { + // for (uint256 j = i + 1; j < boosttiers.length; j++) { + // if (boosttiers[i].minTokens > boosttiers[j].minTokens) { + // // Swap ranges + // BoostRange memory temp = boosttiers[i]; + // boosttiers[i] = boosttiers[j]; + // boosttiers[j] = temp; + // } + // } + // } + // } + + // /// @notice Remove a range by index + // function removeTier(uint256 index) external onlyOwner { + // require(index < boosttiers.length, "Index out of bounds"); + // for (uint256 i = index; i < boosttiers.length - 1; ++i) { + // boosttiers[i] = boosttiers[i + 1]; + // } + // boosttiers.pop(); + // } + + function withdrawFromStakingPool(uint256 _amount) external onlyOwner { + IERC20(pool.tokenAddress).safeTransfer(msg.sender, _amount); + emit FundsWithdrawn(msg.sender, pool.tokenAddress, _amount); + } + + function withdrawFromVestingPool(address _token, uint256 _amount) external onlyOwner { + IERC20(_token).safeTransfer(msg.sender, _amount); + emit FundsWithdrawn(msg.sender, _token, _amount); + } + + // function setUnlockScheduleByPercentage( + // address _token, + // uint256 _lockTime, // Total lock time in seconds + // uint256 _percentagePerStep // Percentage unlocked per step (in basis points, e.g., 100 = 1%) + // ) external onlyOwner { + // require(_lockTime != 0, "Lock time must be greater than zero"); + // require(_percentagePerStep != 0, "Percentage per step must be greater than zero"); + + // uint256 totalPercentage = 10000; // 100% in basis points + // require(totalPercentage % _percentagePerStep == 0, "Percentage must divide 100% evenly"); + + // uint256 steps = totalPercentage / _percentagePerStep; // Number of steps + // uint256 stepTime = _lockTime / steps; // Time interval per step + + // delete unlockSchedules[_token]; // Clear existing schedule for this token + + // for (uint256 i = 1; i <= steps; ++i) { + // unlockSchedules[_token].push(UnlockStep({ + // timeOffset: stepTime * i, // Time offset for this step + // percentage: _percentagePerStep + // })); + // } + + // emit UnlockScheduleSet(_token); + // } + + /// @notice Get the boost percentage for a given token amount + function getBoost(uint256 depositedTokens) public view returns (uint256) { + for (uint256 i = 0; i < boosttiers.length; ++i) { + if (depositedTokens >= boosttiers[i].minTokens && depositedTokens <= boosttiers[i].maxTokens) { + return boosttiers[i].boostPercentage; + } + } + return 0; // Default boost if no range matches + } + + /// @notice This function will end and clear a user's stakes. + /// @dev Only to be used by bots in emergencies + /// @param user The user whose stakes will be ended and 0'd + function clearStakes(address user) external onlyBot { + uint256 clearedStakes = 0; + + for (uint256 i = 0; i < stakes[user].length; ++i) { + Stake storage stake = stakes[user][i]; + clearedStakes = clearedStakes + stake.amount; + stake.amount = 0; + stake.complete = true; + } + + pool.totalStaked = pool.totalStaked - clearedStakes; + } + + /// @notice This function will end and clear a user's withdraw stakes. + /// @dev Only to be used by bots in emergencies + /// @param user The user whose withdraw stakes will be 0'd + function clearWithdrawStakes(address user) external onlyBot { + uint256 clearedStakes = 0; + + for (uint256 i = 0; i < withdrawStake[user].length; ++i) { + WithdrawStake storage stake = withdrawStake[user][i]; + clearedStakes = clearedStakes + stake.amount; + stake.amount = 0; + } + + withdrawLiabilities -= clearedStakes; + } + + /// @notice Migrates all vestings from an old address to a new address + /// @dev Only to be used by bots for account migrations + /// @param oldAddress The address with existing vestings to migrate from + /// @param newAddress The address to migrate vestings to + function migrateVestings(address oldAddress, address newAddress) external onlyBot { + if (oldAddress == address(0) || newAddress == address(0) || oldAddress == newAddress) revert InvalidAddress(); + + Vesting[] storage oldVestings = vestings[oldAddress]; + uint256 vestingCount = oldVestings.length; + if (vestingCount == 0) revert NoStakesAvailable(); + + Vesting[] storage newVestings = vestings[newAddress]; + + for (uint256 i = 0; i < vestingCount; i++) { + Vesting storage oldVesting = oldVestings[i]; + + // Copy vesting to new address + newVestings.push(oldVesting); + + // Clear old vesting + oldVesting.amount = 0; + oldVesting.bonus = 0; + oldVesting.lockedUntil = 0; + oldVesting.claimedAmount = 0; + oldVesting.claimedBonus = 0; + oldVesting.lastClaimed = 0; + oldVesting.createdAt = 0; + oldVesting.usdAmount = 0; + oldVesting.complete = true; + } + + // Migrate dollars vested + dollarsVested[newAddress] += dollarsVested[oldAddress]; + dollarsVested[oldAddress] = 0; + + // Migrate pending vesting withdrawals + WithdrawVesting[] storage oldWithdrawVestings = withdrawVestingActual[oldAddress]; + uint256 withdrawVestingCount = oldWithdrawVestings.length; + if (withdrawVestingCount > 0) { + WithdrawVesting[] storage newWithdrawVestings = withdrawVestingActual[newAddress]; + for (uint256 i = 0; i < withdrawVestingCount; i++) { + newWithdrawVestings.push(oldWithdrawVestings[i]); + } + delete withdrawVestingActual[oldAddress]; + } + } + + // /** + // * @dev Extends the lastClaimed and unlockTime for all stakes of a given address + // * @param _address The address whose stakes to extend + // * @param _seconds The number of seconds to add to lastClaimed and unlockTime + // */ + // function extendStakes(address _address, uint256 _seconds) external onlyBot { + // if (_seconds == 0) return; // Early exit for zero seconds + + // Stake[] storage userStakes = stakes[_address]; + // uint256 length = userStakes.length; + + // if (length == 0) return; // Early exit for no stakes + + // // Cache the stake reference to avoid repeated array access + // for (uint256 i; i < length;) { + // Stake storage stake = userStakes[i]; + + // // Only extend active stakes with non-zero amounts + // if (!stake.complete && stake.amount > 0) { + // unchecked { + // stake.lastClaimed += _seconds; + // stake.unlockTime += _seconds; + // } + // } + + // unchecked { ++i; } + // } + // } + + + // /** + // * @dev Extends the lockedUntil, lastClaimed, and createdAt for all vestings of a given address + // * @param _address The address whose vestings to extend + // * @param _seconds The number of seconds to add to the timestamps + // */ + // function extendVestings(address _address, uint256 _seconds) external onlyBot { + // if (_seconds == 0) return; // Early exit for zero seconds + + // Vesting[] storage userVestings = vestings[_address]; + // uint256 length = userVestings.length; + + // if (length == 0) return; // Early exit for no vestings + + // // Cache the vesting reference to avoid repeated array access + // for (uint256 i; i < length;) { + // Vesting storage vesting = userVestings[i]; + + // // Only extend active vestings with non-zero amounts + // if (!vesting.complete && vesting.amount > 0) { + // unchecked { + // vesting.lockedUntil += _seconds; + // vesting.lastClaimed += _seconds; + // vesting.createdAt += _seconds; + // } + // } + + // unchecked { ++i; } + // } + // } + + // function adminClearSellStake(address _seller, uint256 _stakeId) external onlyOwner { + // SellStake storage sellStakeEntry = sellStakes[_seller][_stakeId]; + // require(sellStakeEntry.amount != 0, "Sell stake not found"); + + // // Access the original stake. + // Stake storage stake = stakes[_seller][_stakeId]; + // require(stake.amount == 0, "Stake not in sell state"); + + // // Restore the original stake's amount. + // // stake.amount = sellStakeEntry.amount; + + // delete sellStakes[_seller][_stakeId]; + + // // Remove the key from the iteration array using swap-and-pop. + // uint256 index = sellStakeKeyIndex[_seller][_stakeId]; + // uint256 lastIndex = sellStakeKeys.length - 1; + // if (index != lastIndex) { + // SellStakeKey memory lastKey = sellStakeKeys[lastIndex]; + // sellStakeKeys[index] = lastKey; + // sellStakeKeyIndex[lastKey.seller][lastKey.stakeId] = index; + // } + // sellStakeKeys.pop(); + // delete sellStakeKeyIndex[_seller][_stakeId]; + + // emit StakeSaleCancelled(_seller, _stakeId); + // } + + function createStake(uint256 _amount) external { + // Scale up for wei comparison, USDC is 1e6 + if (_amount * 1e12 <= minStakeLock) revert AmountBelowMinimum(); + + // Transfer tokens from the user into the contract + IERC20(pool.tokenAddress).safeTransferFrom(msg.sender, address(this), _amount); + + // Check if user has a fixed reward rate set + uint256 finalRewardRate; + if (addressFixedRate[msg.sender] > 0) { + // Use the fixed rate + finalRewardRate = addressFixedRate[msg.sender]; + } else { + // Default logic, restake = false + finalRewardRate = getUserRewardRate(msg.sender, false); + } + + // Create the stake + stakes[msg.sender].push(Stake({ + amount: _amount, + lastClaimed: block.timestamp, + dailyRewardRate: finalRewardRate, + unlockTime: block.timestamp + pool.lockupPeriod, + complete: false + })); + + // Update total staked + pool.totalStaked += _amount; + + emit Staked(msg.sender, _amount); + } + + + /// @notice Restake an expired stake with a bonus daily reward + function restake(uint256 _stakeIndex, uint256 _restakePercentage) nonReentrant external { + if (_restakePercentage > 100) revert InvalidRestakePercentage(); + Stake storage stake = stakes[msg.sender][_stakeIndex]; + // Ensure there is a stake to claim + if (stake.amount == 0) revert NothingToClaim(); + if (block.timestamp < stake.unlockTime) revert StakeLocked(); + + uint256 _amount = stake.amount; + uint rewards = getPoolRewards(msg.sender, _stakeIndex); + _amount = _amount + rewards; + + uint256 restake_amount = (_amount * _restakePercentage) / 100; + uint256 withdraw_amount = _amount - restake_amount; + + // Update state before external calls + stake.amount = 0; + stake.complete = true; + + // Process withdraw + if (withdraw_amount > 0) { + withdrawLiabilities += withdraw_amount; + + if (pool.totalStaked >= withdraw_amount) { + pool.totalStaked -= withdraw_amount; + } else { + pool.totalStaked = 0; + } + // Create temporary the stake for the user to delay withdraw + withdrawStake[msg.sender].push(WithdrawStake({ + stakeId: _stakeIndex, + amount: withdraw_amount, + unlockTime: block.timestamp + unlockDelay + })); + + // Emit a detailed event + emit RewardClaimed(msg.sender, withdraw_amount); + + } + // Process restake + if (restake_amount > 0) { + // Check if user has a fixed reward rate set + uint256 finalRewardRate; + if (addressFixedRate[msg.sender] > 0) { + // Use the fixed rate + finalRewardRate = addressFixedRate[msg.sender]; + } else { + // restake = true + finalRewardRate = getUserRewardRate(msg.sender, true); + } + + stakes[msg.sender].push(Stake({ + amount: restake_amount, + lastClaimed: block.timestamp, + dailyRewardRate: finalRewardRate, + unlockTime: block.timestamp + pool.lockupPeriod, + complete: false + })); + + emit Staked(msg.sender, restake_amount); + } + } + + function createStakeForUser(address _user, uint256 _amount) external onlyOwner { + if (_amount == 0) revert InvalidAmount(); + + stakes[_user].push(Stake({ + amount: _amount, + lastClaimed: block.timestamp, + dailyRewardRate: pool.dailyRewardRate, + unlockTime: block.timestamp + pool.lockupPeriod, + complete: false + })); + + pool.totalStaked = pool.totalStaked + _amount; + emit Staked(_user, _amount); + } + + + function createStakes(StakeInput[] calldata stakesInput) external onlyBot payable { + uint256 totalLength = stakesInput.length; + + for (uint256 i; i < totalLength;) { + StakeInput calldata stakeInput = stakesInput[i]; + + // Update pool total + pool.totalStaked = pool.totalStaked + stakeInput.amount; + + // Create the stake for the user + stakes[stakeInput.user].push(Stake({ + amount: stakeInput.amount, + lastClaimed: stakeInput.lastClaimed, + dailyRewardRate: stakeInput.dailyRewardRate, + unlockTime: stakeInput.unlockTime, + complete: false + })); + + unchecked { + ++i; + } + } + } + + /// @notice Calculates pending rewards for a specific user's stake + /// @dev Rewards only accumulate until the stake's unlockTime is reached + /// @param _user Address of the stake owner + /// @param _stakeIndex Index of the stake in the user's stakes array + /// @return Accumulated rewards since last claim, stopping at unlockTime if applicable + function getPoolRewards(address _user, uint _stakeIndex) public view returns (uint256) { + Stake storage stake = stakes[_user][_stakeIndex]; + + uint256 endTime = block.timestamp < stake.unlockTime ? block.timestamp : stake.unlockTime; + uint256 elapsedTime = endTime > stake.lastClaimed ? endTime - stake.lastClaimed : 0; + uint256 rewards = (stake.amount * stake.dailyRewardRate * elapsedTime) / 1 days / 10000; + + return rewards; + } + + function getUserRewardRate(address _user, bool isRestake) public view returns (uint256) { + uint256 finalRewardRate = pool.dailyRewardRate + getBoost(dollarsVested[_user]); + if (isRestake) { + finalRewardRate += restakeBonus; + } + return finalRewardRate; + } + + function claimRewards() external nonReentrant { + uint256 totalReward = 0; + + for (uint256 i = 0; i < stakes[msg.sender].length; ++i) { + Stake storage stake = stakes[msg.sender][i]; + if (stake.amount > 0) { + uint rewards = getPoolRewards(msg.sender, i); + totalReward = totalReward + rewards; + stake.lastClaimed = block.timestamp; + } + } + + if (totalReward == 0) revert NothingToClaim(); + if (pool.totalRewards < totalReward) revert InsufficientRewards(); + + pool.totalRewards = pool.totalRewards - totalReward; + IERC20(pool.tokenAddress).safeTransfer(msg.sender, totalReward); + + emit RewardClaimed(msg.sender, totalReward); + } + + + function claimStake(uint256 _stakeIndex) external nonReentrant { + // Ensure the stake index is valid + if (_stakeIndex >= stakes[msg.sender].length) revert InvalidStakeIndex(); + + // Load the stake + Stake storage stake = stakes[msg.sender][_stakeIndex]; + uint256 _amount = stake.amount; + + uint rewards = getPoolRewards(msg.sender, _stakeIndex); + + _amount = _amount + rewards; + + // Ensure there is a stake to claim + if (_amount == 0) revert NothingToClaim(); + + // Ensure the stake is unlocked (if using lockup periods) + if (block.timestamp < stake.unlockTime) revert StakeLocked(); + + // Update state before external calls + stake.amount = 0; + stake.complete = true; + withdrawLiabilities += _amount; + + if (pool.totalStaked >= _amount) { + pool.totalStaked -= _amount; + } else { + pool.totalStaked = 0; + } + + // Create temporary the stake for the user to delay withdraw + withdrawStake[msg.sender].push(WithdrawStake({ + stakeId: _stakeIndex, + amount: _amount, + unlockTime: block.timestamp + unlockDelay + })); + + // Emit a detailed event + emit RewardClaimed(msg.sender, _amount); + } + +/** + * @notice Withdraw a staked amount after its unlock time has passed. + * @dev Locates the stake by `_stakeIndex`, checks that it's unlocked and non-zero, + * and transfers tokens to the caller. For vesting stakes (where `_stakeIndex` >= 1e6), + * the stored amount (in 1e18 decimals) is scaled to USDC's 1e6 decimals by dividing by 1e12. + * + * Requirements: + * - Caller must have at least one stake. + * - The stake must exist, be unlocked, and have a non-zero amount. + * - The contract must have sufficient token balance. + * + * @param _stakeIndex The identifier of the stake to withdraw. + */ +function withdraw(uint256 _stakeIndex) external nonReentrant { + WithdrawStake[] storage userStakes = withdrawStake[msg.sender]; + if (userStakes.length == 0) revert NoStakesAvailable(); + + for (uint256 i = 0; i < userStakes.length; ++i) { + WithdrawStake storage stake = userStakes[i]; + // Skip already withdrawn stakes (amount == 0) + if (stake.stakeId == _stakeIndex && stake.amount != 0) { + if (block.timestamp < stake.unlockTime) revert StakeLocked(); + + uint256 _amount = stake.amount; + + // Convert vesting stake amount to USDC decimals. + if (_stakeIndex >= 1e6) { + _amount = _amount / 1e12; + } + + uint256 poolBalance = IERC20(pool.tokenAddress).balanceOf(address(this)); + if (poolBalance < _amount) revert InsufficientRewards(); + + // Update state before external calls + // withdrawLiabilities is in 1e18, deduct original amount + withdrawLiabilities -= stake.amount; + stake.amount = 0; + + // Transfer tokens + IERC20(pool.tokenAddress).safeTransfer(msg.sender, _amount); + emit StakeWithdrawn(msg.sender, _amount, _stakeIndex); + return; + } + } + + // Revert if no matching stake with non-zero amount was found + revert StakeNotFound(); +} + +/** + * @notice Withdraws vesting tokens after cooldown period + * @param _vestingId The vesting ID to withdraw + */ +function withdrawVestingToken(uint256 _vestingId) external nonReentrant { + WithdrawVesting[] storage userVestings = withdrawVestingActual[msg.sender]; + if (userVestings.length == 0) revert NoStakesAvailable(); + + for (uint256 i = 0; i < userVestings.length; ++i) { + WithdrawVesting storage vestingWithdraw = userVestings[i]; + if (vestingWithdraw.vestingId == _vestingId && vestingWithdraw.amount != 0) { + if (block.timestamp < vestingWithdraw.unlockTime) revert StakeLocked(); + + uint256 _amount = vestingWithdraw.amount; + address _token = vestingWithdraw.token; + + // Check contract has sufficient balance + uint256 tokenBalance = IERC20(_token).balanceOf(address(this)); + if (tokenBalance < _amount) revert InsufficientRewards(); + + // Update state before external calls + vestingWithdraw.amount = 0; + + // Decrement withdraw vesting liabilities for this token + withdrawVestingLiabilities[_token] -= _amount; + + // Transfer tokens + IERC20(_token).safeTransfer(msg.sender, _amount); + emit StakeWithdrawn(msg.sender, _amount, _vestingId); + return; + } + } + + revert StakeNotFound(); +} + + + function compoundAllRewards() external { + uint256 totalReward = 0; + + for (uint256 i = 0; i < stakes[msg.sender].length; ++i) { + Stake storage stake = stakes[msg.sender][i]; + if (stake.amount > 0) { + uint rewards = getPoolRewards(msg.sender, i); + totalReward = totalReward + rewards; + stake.lastClaimed = block.timestamp; + } + } + + if (totalReward <= minStakeLock) revert NotEnoughToCompound(); + + // Check if user has a fixed reward rate set + uint256 finalRewardRate; + if (addressFixedRate[msg.sender] > 0) { + // Use the fixed rate + finalRewardRate = addressFixedRate[msg.sender]; + } else { + // Default logic, restake = false + finalRewardRate = getUserRewardRate(msg.sender, false); + } + + stakes[msg.sender].push(Stake({ + amount: totalReward, + lastClaimed: block.timestamp, + dailyRewardRate: finalRewardRate, + unlockTime: block.timestamp + pool.lockupPeriod, + complete: false + })); + + pool.totalStaked = pool.totalStaked + totalReward; + emit CompoundRewards(msg.sender, totalReward); + } + + function createVesting(address _token, uint256 _amount) external { + if (_amount == 0) revert InvalidAmount(); + address oracle = priceOracles[_token]; + if (oracle == address(0)) revert PriceOracleNotSet(); + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + + uint256 bonus = (_amount * BONUS_PERCENTAGE) / 100; + + uint256 usdPrice = (iPriceOracle(priceOracles[_token]).getLatestPrice(_token) * _amount) / 1e18; + if (usdPrice <= minStakeLock) revert AmountBelowMinimum(); + + // Update user's dollarsVested + dollarsVested[msg.sender] += usdPrice; + // Update token's vestedTotal + vestedTotal[_token] += _amount; + + + vestings[msg.sender].push(Vesting({ + amount: _amount, + bonus: bonus, + lockedUntil: block.timestamp + lockupDuration, + claimedAmount: 0, + claimedBonus: 0, + lastClaimed: block.timestamp, + createdAt: block.timestamp, + token: _token, + complete: false, + usdAmount: usdPrice + })); + + emit VestingCreated(msg.sender, _amount, bonus); + } + + function getUnlockedVesting(address _user, uint256 _vestingIndex) public view returns (uint256) { + Vesting storage vesting = vestings[_user][_vestingIndex]; + uint256 timeElapsed = block.timestamp - vesting.createdAt; + address token = vesting.token; + + uint256 unlockedAmount = 0; + + for (uint256 i = 0; i < unlockSchedules[token].length; ++i) { + UnlockStep storage step = unlockSchedules[token][i]; + uint256 timeTier = step.timeOffset; + uint256 percentage = step.percentage; + + if (timeElapsed >= timeTier) { + unlockedAmount = unlockedAmount + ((vesting.amount * percentage) / 10000); + } + } + + return unlockedAmount; + } + + function getVestingSchedule(address _user, uint256 _vestingIndex) public view returns (uint256[] memory, uint256[] memory) { + Vesting storage vesting = vestings[_user][_vestingIndex]; + address token = vesting.token; + + uint256 scheduleLength = unlockSchedules[token].length; + uint256[] memory unlockTimestamps = new uint256[](scheduleLength); + uint256[] memory unlockPercentages = new uint256[](scheduleLength); + + for (uint256 i = 0; i < scheduleLength; ++i) { + UnlockStep storage step = unlockSchedules[token][i]; + + // Calculate the absolute unlock timestamp + unlockTimestamps[i] = vesting.createdAt + step.timeOffset; + unlockPercentages[i] = step.percentage; // Percentage is stored as scaled by 10000 (e.g., 2500 = 25%) + } + + return (unlockTimestamps, unlockPercentages); + } + + function getUnlockedVestingBonus(address _user, uint256 _vestingIndex) public view returns (uint256) { + Vesting storage vesting = vestings[_user][_vestingIndex]; + uint256 timeElapsed = block.timestamp - vesting.createdAt; + address token = vesting.token; + + uint256 unlockedAmount = 0; + + for (uint256 i = 0; i < unlockSchedules[token].length; ++i) { + UnlockStep storage step = unlockSchedules[token][i]; + uint256 timeTier = step.timeOffset; + uint256 percentage = step.percentage; + uint256 maxBonusAmount = (vesting.usdAmount * BONUS_PERCENTAGE) / 100; + + if (timeElapsed >= timeTier) { + unlockedAmount = unlockedAmount + ((maxBonusAmount * percentage) / 10000); + } + } + + return unlockedAmount; + } + + + function claimVesting(uint256 _vestingIndex) external nonReentrant { + Vesting storage vesting = vestings[msg.sender][_vestingIndex]; + if (vesting.complete) revert StakeComplete(); + uint256 maxClaim = getUnlockedVesting(msg.sender, _vestingIndex); + + if (maxClaim < vesting.claimedAmount) revert InvalidClaimAmount(); + uint256 amountToClaim = maxClaim - vesting.claimedAmount; + if (amountToClaim == 0) revert NothingToClaim(); + + vesting.claimedAmount = vesting.claimedAmount + amountToClaim; + if (vesting.claimedAmount >= vesting.amount) { + vesting.complete = true; + } + // Update user's dollarsVested + if (dollarsVested[msg.sender] > 0) { + uint256 usdPrice = (iPriceOracle(priceOracles[vesting.token]).getLatestPrice(vesting.token) * amountToClaim) / 1e18; + if (usdPrice >= dollarsVested[msg.sender]) { + dollarsVested[msg.sender] = 0; + } else { + dollarsVested[msg.sender] -= usdPrice; + } + } + vestedTotal[vesting.token] -= amountToClaim; + + // Add vesting claims to cooldown queue + withdrawVestingActual[msg.sender].push(WithdrawVesting({ + vestingId: withdrawVestingCounterActual++, + amount: amountToClaim, + unlockTime: block.timestamp + unlockDelay, + token: vesting.token + })); + + // Increment withdraw vesting liabilities for this token + withdrawVestingLiabilities[vesting.token] += amountToClaim; + + emit VestingClaimed(msg.sender, amountToClaim, 0); + } + + function claimAllVestingByToken(address _token) external nonReentrant { + uint256 totalReward = 0; + uint256 vestingsProcessed = 0; + + for (uint256 i = 0; i < vestings[msg.sender].length; ++i) { + Vesting storage vesting = vestings[msg.sender][i]; + + if (vesting.token == _token && !vesting.complete) { + uint256 maxClaim = getUnlockedVesting(msg.sender, i); + if (maxClaim < vesting.claimedAmount) revert InvalidClaimAmount(); + + uint256 amountToClaim = maxClaim - vesting.claimedAmount; + if (amountToClaim > 0) { + vesting.claimedAmount = vesting.claimedAmount + amountToClaim; + totalReward = totalReward + amountToClaim; + vesting.lastClaimed = block.timestamp; + + // Mark vesting as complete if fully claimed + if (vesting.claimedAmount >= vesting.amount) { + vesting.complete = true; + } + + vestingsProcessed++; + } + } + } + + if (totalReward == 0) revert NothingToClaim(); + + // Update user's dollarsVested + if (dollarsVested[msg.sender] > 0) { + uint256 usdPrice = (iPriceOracle(priceOracles[_token]).getLatestPrice(_token) * totalReward) / 1e18; + if (usdPrice >= dollarsVested[msg.sender]) { + dollarsVested[msg.sender] = 0; + } else { + dollarsVested[msg.sender] -= usdPrice; + } + } + + // Update vesting total + vestedTotal[_token] -= totalReward; + + // Add vesting claims to cooldown queue + withdrawVestingActual[msg.sender].push(WithdrawVesting({ + vestingId: withdrawVestingCounterActual++, + amount: totalReward, + unlockTime: block.timestamp + unlockDelay, + token: _token + })); + + // Increment withdraw vesting liabilities for this token + withdrawVestingLiabilities[_token] += totalReward; + + emit RewardClaimed(msg.sender, totalReward); + } + + + function claimBonus(uint256 _vestingIndex) external nonReentrant { + Vesting storage vesting = vestings[msg.sender][_vestingIndex]; + uint256 maxBonus = getUnlockedVestingBonus(msg.sender, _vestingIndex); + + if (maxBonus < vesting.claimedBonus) revert InvalidClaimAmount(); + uint256 bonusToClaim = maxBonus - vesting.claimedBonus; + if (bonusToClaim == 0) revert NothingToClaim(); + + vesting.claimedBonus = vesting.claimedBonus + bonusToClaim; + withdrawLiabilities += bonusToClaim; + + // IERC20(vesting.token).safeTransfer(msg.sender, bonusToClaim); + + // Create temporary the stake for the user to delay withdraw. + // Add 1e6 to the vesting index to distinguish them from normal stakes. + withdrawStake[msg.sender].push(WithdrawStake({ + stakeId: _vestingIndex + 1e6, + amount: bonusToClaim, + unlockTime: block.timestamp + unlockDelay + })); + + emit BonusClaimed(msg.sender, bonusToClaim); + } + + function setPriceOracle(address _token, address _oracle) external onlyOwner { + priceOracles[_token] = _oracle; + } + + function viewRewards(address _user) external view returns (uint256) { + uint256 totalReward = 0; + + for (uint256 i = 0; i < stakes[_user].length; ++i) { + uint rewards = getPoolRewards(_user, i); + totalReward = totalReward + rewards; + } + + return totalReward; + } + + /// @notice View function to get all stakes for a specific address + function getStakes(address user) external view returns (Stake[] memory) { + return stakes[user]; + } + + /// @notice View function to get all vestings for a specific address + function getVestings(address user) external view returns (Vesting[] memory) { + return vestings[user]; + } + + /// @notice View to monitor contract pool deficits + function getPoolStatus() external view returns (uint256) { + uint256 poolBalance = IERC20(pool.tokenAddress).balanceOf(address(this)); + + // If the balance is greater than or equal to liabilities, return 0 + if (poolBalance >= withdrawLiabilities) { + return 0; + } + + // Otherwise, return the deficit (amount needed to cover liabilities) + return withdrawLiabilities - poolBalance; + } + + /** + * @notice Returns the vested amounts and USD values for an array of tokens. + * @param _tokens The array of token addresses to evaluate. + * @return amounts The array of vested amounts for each token. + * @return usdValues The array of USD values for each 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 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); + } + +} \ No newline at end of file diff --git a/hardhat.config.js b/hardhat.config.js index c8693eb..bad07af 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -1,11 +1,8 @@ require("@openzeppelin/hardhat-upgrades"); require("@nomicfoundation/hardhat-ignition-ethers"); -// require("@nomiclabs/hardhat-ethers"); -// require("@nomiclabs/hardhat-etherscan"); require("hardhat-contract-sizer"); -// require("dotenv").config(); -// require("hardhat-gas-reporter"); require("@nomicfoundation/hardhat-verify"); +require("dotenv").config(); const env = process.env; // This is a sample Hardhat task. To learn how to create your own go to @@ -82,15 +79,8 @@ module.exports = { networks: { hardhat: { forking: { - // MAINNET FORK - url: `https://bsc-mainnet.nodereal.io/v1/f82aa3b8072a46ccadf3024a96f0cff4`, - // blockNumber: 30488174, - chainId: 56, - - // TESTNET FORK - // url: `https://bsc-testnet.nodereal.io/v1/f82aa3b8072a46ccadf3024a96f0cff4`, - // blockNumber: 31828689, - // chainId: 97, + url: `https://rpc.soniclabs.com`, + blockNumber: 37000000, }, }, local: { @@ -132,26 +122,32 @@ module.exports = { // gas: 2e9, }, mainnet: { - url: `https://bsc-dataseed1.binance.org`, + url: env.MAINNET_RPC_URL || `https://bsc-dataseed1.binance.org`, chainId: 56, + accounts: env.PRIVATE_KEY ? [env.PRIVATE_KEY] : [], }, base: { - url: `https://base-mainnet.public.blastapi.io`, + url: env.BASE_RPC_URL || `https://base-mainnet.public.blastapi.io`, chainId: 8453, + accounts: env.PRIVATE_KEY ? [env.PRIVATE_KEY] : [], }, sonic: { - url: `https://rpc.soniclabs.com`, + url: env.SONIC_RPC_URL || `https://rpc.soniclabs.com`, chainId: 146, + accounts: env.PRIVATE_KEY ? [env.PRIVATE_KEY] : [], + }, + sonicfork: { + url: "http://127.0.0.1:8545", + forking: { + url: `https://rpc.soniclabs.com`, + blockNumber: 37513000, // Recent block + }, + accounts: env.PRIVATE_KEY ? [env.PRIVATE_KEY] : [], }, }, etherscan: { enable: true, - apiKey: { - bbsc: "verifyContract", - base: "GN555QYEWPDFZ47H1TR5ASK693D38A69GY", - mainnet: "1I15826QJ4HHY2UTGK3EZEA4TNBT68FB83", - sonic: "N6DMIQQNJ7634I1ETH527Z1WZQM2Q6GEW8" - }, + apiKey: "JRKR5T93RQTKVERUJ2NVE4T994TD66QGFH", customChains: [ { network: "bbsc", diff --git a/scripts/deployProxy.js b/scripts/deployProxy.js index 92ec1a8..7fd8d3c 100644 --- a/scripts/deployProxy.js +++ b/scripts/deployProxy.js @@ -1,17 +1,24 @@ const { ethers, upgrades, run } = require("hardhat"); const fs = require("fs"); const path = require("path"); -require('dotenv').config(); +require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); const deploymentFile = path.join(__dirname, "deployedAddresses.json"); async function main() { - const privateKey = process.env.pk; + const privateKey = process.env.PRIVATE_KEY; const network = await hre.network.name; - const wallet = new ethers.Wallet(privateKey, ethers.provider); - const deployer = wallet.connect(ethers.provider); - console.log(`Using private key for account: ${deployer.address}`); + let deployer; + if (network === "hardhat" || network === "localhost") { + // Use default hardhat account for local testing + [deployer] = await ethers.getSigners(); + console.log(`Using default hardhat account: ${deployer.address}`); + } else { + const wallet = new ethers.Wallet(privateKey, ethers.provider); + deployer = wallet.connect(ethers.provider); + console.log(`Using private key for account: ${deployer.address}`); + } console.log("Deploying contracts with the account:", deployer.address); let deploymentData = {}; @@ -19,12 +26,25 @@ async function main() { deploymentData = JSON.parse(fs.readFileSync(deploymentFile, "utf8")); } - const contractName = network === "mainnet" - ? "PacaFinanceWithBoostAndScheduleUSDT" - : "PacaFinanceWithBoostAndScheduleUSDC"; - + // Select contract and proxy address based on network + let contractName; let proxyAddress; - if (!deploymentData.proxyAddress) { + if (network === "base") { + contractName = "PacaFinanceWithBoostAndScheduleBase"; + proxyAddress = process.env.PROXY_ADDRESS_BASE; + } else if (network === "sonic") { + contractName = "PacaFinanceWithBoostAndScheduleSonic"; + proxyAddress = process.env.PROXY_ADDRESS_SONIC || deploymentData.proxyAddress; + } else if (network === "mainnet") { + contractName = "PacaFinanceWithBoostAndScheduleBsc"; + proxyAddress = process.env.PROXY_ADDRESS_BSC || deploymentData.proxyAddress; + } else { + // Default to BSC for other networks (like test networks) + contractName = "PacaFinanceWithBoostAndScheduleBsc"; + proxyAddress = deploymentData.proxyAddress; + } + + if (!proxyAddress) { // Initial deployment console.log("Deploying proxy..."); const Paca = await ethers.getContractFactory(contractName, deployer); @@ -46,12 +66,9 @@ async function main() { await verifyContract(implementationAddress, contractName); } else { // Upgrade - proxyAddress = deploymentData.proxyAddress; console.log("Upgrading proxy..."); const Paca = await ethers.getContractFactory(contractName, deployer); - // //commen tout for mainet - // await upgrades.forceImport(proxyAddress, Paca); // Get current implementation for comparison const oldImplementationAddress = await upgrades.erc1967.getImplementationAddress(proxyAddress); diff --git a/scripts/manualUpgrade.js b/scripts/manualUpgrade.js new file mode 100644 index 0000000..83c278d --- /dev/null +++ b/scripts/manualUpgrade.js @@ -0,0 +1,112 @@ +const { ethers } = require("hardhat"); +const fs = require("fs"); +const path = require("path"); +require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); + +const deploymentFile = path.join(__dirname, "deployedAddresses.json"); + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + const network = await hre.network.name; + + let deployer; + if (network === "hardhat" || network === "localhost") { + [deployer] = await ethers.getSigners(); + console.log(`Using default hardhat account: ${deployer.address}`); + } else { + const wallet = new ethers.Wallet(privateKey, ethers.provider); + deployer = wallet.connect(ethers.provider); + console.log(`Using private key for account: ${deployer.address}`); + } + console.log("Deploying contracts with the account:", deployer.address); + + // Select contract based on network + let contractName; + if (network === "base") { + contractName = "PacaFinanceWithBoostAndScheduleBase"; + } else if (network === "sonic") { + contractName = "PacaFinanceWithBoostAndScheduleSonic"; + } else if (network === "mainnet") { + contractName = "PacaFinanceWithBoostAndScheduleBsc"; + } else { + contractName = "PacaFinanceWithBoostAndScheduleBsc"; + } + + console.log(`Deploying ${contractName} for network: ${network}`); + + // Read deployment data + let deploymentData = {}; + if (fs.existsSync(deploymentFile)) { + deploymentData = JSON.parse(fs.readFileSync(deploymentFile, "utf8")); + } + + if (!deploymentData.proxyAddress) { + console.error("No proxy address found in deployment file!"); + process.exit(1); + } + + const proxyAddress = deploymentData.proxyAddress; + console.log("Proxy address:", proxyAddress); + + // Deploy new implementation contract + console.log("Deploying new implementation contract..."); + const PacaFactory = await ethers.getContractFactory(contractName, deployer); + const newImplementation = await PacaFactory.deploy(); + await newImplementation.waitForDeployment(); + + const newImplementationAddress = await newImplementation.getAddress(); + console.log("New implementation deployed to:", newImplementationAddress); + + // ProxyAdmin contract address + const proxyAdminAddress = "0x3459Fe72D4274d40d449aE1806F8E2149302F28B"; + + // ProxyAdmin ABI with upgradeAndCall function + const proxyAdminABI = [ + "function upgradeAndCall(address proxy, address implementation, bytes data) external payable" + ]; + + // Connect to ProxyAdmin contract + const proxyAdmin = new ethers.Contract(proxyAdminAddress, proxyAdminABI, deployer); + + // Perform upgrade with empty call data + console.log("Performing upgrade via ProxyAdmin..."); + console.log("ProxyAdmin address:", proxyAdminAddress); + const upgradeTx = await proxyAdmin.upgradeAndCall(proxyAddress, newImplementationAddress, "0x"); + console.log("Upgrade transaction hash:", upgradeTx.hash); + + // Wait for transaction confirmation + await upgradeTx.wait(); + console.log("Upgrade completed successfully!"); + + // Update deployment data + deploymentData.implementationAddress = newImplementationAddress; + fs.writeFileSync(deploymentFile, JSON.stringify(deploymentData, null, 2)); + console.log("Deployment data updated"); + + // Verify the new implementation + console.log("Waiting 10 seconds before verification..."); + await new Promise(resolve => setTimeout(resolve, 10000)); + + try { + await hre.run("verify:verify", { + address: newImplementationAddress, + constructorArguments: [] + }); + console.log("Contract verified successfully."); + } catch (err) { + if (err.message.includes("already been verified")) { + console.log("Contract is already verified."); + } else { + console.log("Verification failed:", err.message); + console.log(`\nTo verify manually, run:`); + console.log(`npx hardhat verify --network ${network} ${newImplementationAddress}`); + } + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); \ No newline at end of file