From c8da95a2b01ea028e056f64a7ef1d6920fbff941 Mon Sep 17 00:00:00 2001 From: sascha Date: Mon, 7 Jul 2025 19:32:17 +0200 Subject: [PATCH] Error Refactor and Vesting Delay --- contracts/base_paca.sol | 238 +++++++++++++++++++++++++++++++--------- contracts/bsc_paca.sol | 236 ++++++++++++++++++++++++++++++--------- hardhat.config.js | 8 +- 3 files changed, 381 insertions(+), 101 deletions(-) diff --git a/contracts/base_paca.sol b/contracts/base_paca.sol index 6440aea..37701ad 100644 --- a/contracts/base_paca.sol +++ b/contracts/base_paca.sol @@ -11,6 +11,29 @@ interface iPriceOracle { 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 PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUpgradeable { @@ -72,6 +95,13 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp uint256 unlockTime; } + struct WithdrawVesting { + uint256 vestingId; + uint256 amount; + uint256 unlockTime; + address token; + } + struct SellStake { uint256 price; uint256 bonusAmount; @@ -106,6 +136,8 @@ 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; @@ -134,11 +166,11 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp // Modifiers modifier onlyOwner() { - require(owners[msg.sender], "Not authorized"); + if (!owners[msg.sender]) revert NotAuthorized(); _; } modifier onlyBot() { - require(authorizedBots[msg.sender], "Caller is not an authorized bot"); + if (!authorizedBots[msg.sender]) revert NotAuthorized(); _; } @@ -165,19 +197,20 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp // Ownership Management function addOwner(address _newOwner) external onlyOwner { - require(!owners[_newOwner], "Already an owner"); + if (owners[_newOwner]) revert AlreadyOwner(); owners[_newOwner] = true; } function removeOwner(address _owner) external onlyOwner { - require(owners[_owner], "Not an owner"); - require(_owner != msg.sender, "Cannot remove yourself"); + 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 { - require(bot != address(0), "Invalid address"); + if (bot == address(0)) revert InvalidAddress(); authorizedBots[bot] = true; } @@ -194,6 +227,7 @@ contract PacaFinanceWithBoostAndScheduleUSDC 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; @@ -239,14 +273,14 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp /// @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 { - require(_addr != address(0), "Invalid address"); + 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 { - require(_addr != address(0), "Invalid address"); + if (_addr == address(0)) revert InvalidAddress(); addressFixedRate[_addr] = 0; } @@ -381,6 +415,53 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp 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 = withdrawVesting[oldAddress]; + uint256 withdrawVestingCount = oldWithdrawVestings.length; + if (withdrawVestingCount > 0) { + WithdrawVesting[] storage newWithdrawVestings = withdrawVesting[newAddress]; + for (uint256 i = 0; i < withdrawVestingCount; i++) { + newWithdrawVestings.push(oldWithdrawVestings[i]); + } + delete withdrawVesting[oldAddress]; + } + } + // /** // * @dev Extends the lastClaimed and unlockTime for all stakes of a given address // * @param _address The address whose stakes to extend @@ -470,7 +551,7 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp function createStake(uint256 _amount) external { // Scale up for wei comparison, USDC is 1e6 - require(_amount * 1e12 > minStakeLock, "Amount must be greater minStakeLock"); + if (_amount * 1e12 <= minStakeLock) revert AmountBelowMinimum(); // Transfer tokens from the user into the contract IERC20(pool.tokenAddress).safeTransferFrom(msg.sender, address(this), _amount); @@ -503,11 +584,11 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp /// @notice Restake an expired stake with a bonus daily reward function restake(uint256 _stakeIndex, uint256 _restakePercentage) nonReentrant external { - require(_restakePercentage <= 100, "Invalid restake percentage"); + if (_restakePercentage > 100) revert InvalidRestakePercentage(); Stake storage stake = stakes[msg.sender][_stakeIndex]; // Ensure there is a stake to claim - require(stake.amount != 0, "No amount to claim"); - require(block.timestamp >= stake.unlockTime, "Stake is still locked"); + if (stake.amount == 0) revert NothingToClaim(); + if (block.timestamp < stake.unlockTime) revert StakeLocked(); uint256 _amount = stake.amount; uint rewards = getPoolRewards(msg.sender, _stakeIndex); @@ -565,7 +646,7 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp } function createStakeForUser(address _user, uint256 _amount) external onlyOwner { - require(_amount != 0, "Invalid amount"); + if (_amount == 0) revert InvalidAmount(); stakes[_user].push(Stake({ amount: _amount, @@ -639,8 +720,8 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp } } - require(totalReward != 0, "No rewards to claim"); - require(pool.totalRewards >= totalReward, "Insufficient rewards in the pool"); + if (totalReward == 0) revert NothingToClaim(); + if (pool.totalRewards < totalReward) revert InsufficientRewards(); pool.totalRewards = pool.totalRewards - totalReward; IERC20(pool.tokenAddress).safeTransfer(msg.sender, totalReward); @@ -651,7 +732,7 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp function claimStake(uint256 _stakeIndex) external nonReentrant { // Ensure the stake index is valid - require(_stakeIndex < stakes[msg.sender].length, "Invalid stake index"); + if (_stakeIndex >= stakes[msg.sender].length) revert InvalidStakeIndex(); // Load the stake Stake storage stake = stakes[msg.sender][_stakeIndex]; @@ -662,10 +743,10 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp _amount = _amount + rewards; // Ensure there is a stake to claim - require(_amount != 0, "No amount to claim"); + if (_amount == 0) revert NothingToClaim(); // Ensure the stake is unlocked (if using lockup periods) - require(block.timestamp >= stake.unlockTime, "Stake is still locked"); + if (block.timestamp < stake.unlockTime) revert StakeLocked(); // Update state before external calls stake.amount = 0; @@ -704,13 +785,13 @@ contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUp */ function withdraw(uint256 _stakeIndex) external nonReentrant { WithdrawStake[] storage userStakes = withdrawStake[msg.sender]; - require(userStakes.length > 0, "No stakes available for withdrawal"); + 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) { - require(block.timestamp >= stake.unlockTime, "Withdraw Stake is still locked"); + if (block.timestamp < stake.unlockTime) revert StakeLocked(); uint256 _amount = stake.amount; @@ -720,7 +801,7 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { } uint256 poolBalance = IERC20(pool.tokenAddress).balanceOf(address(this)); - require(poolBalance >= _amount, "Insufficient rewards in the pool"); + if (poolBalance < _amount) revert InsufficientRewards(); // Update state before external calls // withdrawLiabilities is in 1e18, deduct original amount @@ -735,7 +816,41 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { } // Revert if no matching stake with non-zero amount was found - revert("Invalid stake index or already withdrawn"); + 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 = withdrawVesting[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; + + // Transfer tokens + IERC20(_token).safeTransfer(msg.sender, _amount); + emit StakeWithdrawn(msg.sender, _amount, _vestingId); + return; + } + } + + // Revert if no matching vesting with non-zero amount was found + revert StakeNotFound(); } @@ -751,7 +866,7 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { } } - require(totalReward > minStakeLock, "Not enough to compound"); + if (totalReward <= minStakeLock) revert NotEnoughToCompound(); // Check if user has a fixed reward rate set uint256 finalRewardRate; @@ -776,15 +891,15 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { } function createVesting(address _token, uint256 _amount) external { - require(_amount != 0, "Amount must be greater than zero"); + if (_amount == 0) revert InvalidAmount(); address oracle = priceOracles[_token]; - require(oracle != address(0), "Price oracle not set for this 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; - require(usdPrice > minStakeLock, "Amount must be greater minStakeLock"); + if (usdPrice <= minStakeLock) revert AmountBelowMinimum(); // Update user's dollarsVested dollarsVested[msg.sender] += usdPrice; @@ -871,12 +986,12 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { function claimVesting(uint256 _vestingIndex) external nonReentrant { Vesting storage vesting = vestings[msg.sender][_vestingIndex]; - require(vesting.complete == false, "Stake is Complete"); + if (vesting.complete) revert StakeComplete(); uint256 maxClaim = getUnlockedVesting(msg.sender, _vestingIndex); - require(maxClaim >= vesting.claimedAmount, "Invalid claim amount"); + if (maxClaim < vesting.claimedAmount) revert InvalidClaimAmount(); uint256 amountToClaim = maxClaim - vesting.claimedAmount; - require(amountToClaim != 0, "No vested amount to claim"); + if (amountToClaim == 0) revert NothingToClaim(); vesting.claimedAmount = vesting.claimedAmount + amountToClaim; if (vesting.claimedAmount >= vesting.amount) { @@ -892,7 +1007,14 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { } } vestedTotal[vesting.token] -= amountToClaim; - IERC20(vesting.token).safeTransfer(msg.sender, amountToClaim); + + // Add vesting claims to cooldown queue + withdrawVesting[msg.sender].push(WithdrawVesting({ + vestingId: withdrawVestingCounter++, + amount: amountToClaim, + unlockTime: block.timestamp + unlockDelay, + token: vesting.token + })); emit VestingClaimed(msg.sender, amountToClaim, 0); } @@ -906,7 +1028,7 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { if (vesting.token == _token && !vesting.complete) { uint256 maxClaim = getUnlockedVesting(msg.sender, i); - require(maxClaim >= vesting.claimedAmount, "Invalid claim amount"); + if (maxClaim < vesting.claimedAmount) revert InvalidClaimAmount(); uint256 amountToClaim = maxClaim - vesting.claimedAmount; if (amountToClaim > 0) { @@ -924,7 +1046,7 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { } } - require(totalReward != 0, "No rewards to claim"); + if (totalReward == 0) revert NothingToClaim(); // Update user's dollarsVested if (dollarsVested[msg.sender] > 0) { @@ -936,13 +1058,16 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { } } - // Ensure the contract has enough balance to fulfill the claim - uint256 poolBalance = IERC20(_token).balanceOf(address(this)); - require(poolBalance >= totalReward, "Insufficient rewards in the pool"); // Update vesting total vestedTotal[_token] -= totalReward; - // Transfer the aggregated reward - IERC20(_token).safeTransfer(msg.sender, totalReward); + + // Add vesting claims to cooldown queue + withdrawVesting[msg.sender].push(WithdrawVesting({ + vestingId: withdrawVestingCounter++, + amount: totalReward, + unlockTime: block.timestamp + unlockDelay, + token: _token + })); emit RewardClaimed(msg.sender, totalReward); } @@ -952,9 +1077,9 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { Vesting storage vesting = vestings[msg.sender][_vestingIndex]; uint256 maxBonus = getUnlockedVestingBonus(msg.sender, _vestingIndex); - require(maxBonus >= vesting.claimedBonus, "Invalid claim amount"); + if (maxBonus < vesting.claimedBonus) revert InvalidClaimAmount(); uint256 bonusToClaim = maxBonus - vesting.claimedBonus; - require(bonusToClaim != 0, "No vested amount to claim"); + if (bonusToClaim == 0) revert NothingToClaim(); vesting.claimedBonus = vesting.claimedBonus + bonusToClaim; withdrawLiabilities += bonusToClaim; @@ -1084,7 +1209,7 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { return userStakes[i]; } } - revert("WithdrawStake with the specified stakeId not found for this user."); + revert StakeNotFound(); } /// @notice Function that lets you look up an address’s stake by vestingId. @@ -1098,7 +1223,7 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { return userStakes[i]; } } - revert("WithdrawStake with the specified stakeId not found for this user."); + revert StakeNotFound(); } /// @notice Function that returns an array of all the user's withdrawStakes. @@ -1108,17 +1233,30 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { 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 withdrawVesting[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; + } + /// @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]; - require(!stake.complete, "Stake already complete"); - require(stake.amount != 0, "Stake amount is 0"); + if (stake.complete) revert StakeComplete(); + if (stake.amount == 0) revert InvalidAmount(); // Ensure the stake isn't already on sale. - require(sellStakes[msg.sender][_stakeId].amount == 0, "Stake already on sale"); - require(price >= (stake.amount * sellMin) / 100, "Price is too low"); + 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({ @@ -1145,11 +1283,11 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { /// @param _stakeId The stake ID to cancel the sale. function cancelSellStake(uint256 _stakeId) external { SellStake storage sellStakeEntry = sellStakes[msg.sender][_stakeId]; - require(sellStakeEntry.amount != 0, "Sell stake not found"); + if (sellStakeEntry.amount == 0) revert StakeNotFound(); // Access the original stake. Stake storage stake = stakes[msg.sender][_stakeId]; - require(stake.amount == 0, "Stake not in sell state"); + if (stake.amount != 0) revert StakeNotInSellState(); // Restore the original stake's amount. stake.amount = sellStakeEntry.amount; @@ -1175,8 +1313,8 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { /// @param newPrice The new price of the stake. function updateSellStake(uint256 _stakeId, uint256 newPrice) external { SellStake storage sellStakeEntry = sellStakes[msg.sender][_stakeId]; - require(sellStakeEntry.amount != 0, "Sell stake not found"); - require(newPrice >= (sellStakeEntry.amount * sellMin) / 100, "New price is too low"); + if (sellStakeEntry.amount == 0) revert StakeNotFound(); + if (newPrice < (sellStakeEntry.amount * sellMin) / 100) revert PriceTooLow(); sellStakeEntry.bonusAmount = (newPrice * sellKickBack) / 100; sellStakeEntry.price = newPrice; @@ -1194,7 +1332,7 @@ function withdraw(uint256 _stakeIndex) external nonReentrant { /// @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]; - require(sellStakeEntry.amount != 0, "Sell stake not available"); + 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); diff --git a/contracts/bsc_paca.sol b/contracts/bsc_paca.sol index 5bcc250..4f2055b 100644 --- a/contracts/bsc_paca.sol +++ b/contracts/bsc_paca.sol @@ -11,6 +11,29 @@ interface iPriceOracle { 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 PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUpgradeable { @@ -72,6 +95,13 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp uint256 unlockTime; } + struct WithdrawVesting { + uint256 vestingId; + uint256 amount; + uint256 unlockTime; + address token; + } + struct SellStake { uint256 price; uint256 bonusAmount; @@ -106,6 +136,8 @@ 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; @@ -134,11 +166,11 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp // Modifiers modifier onlyOwner() { - require(owners[msg.sender], "Not authorized"); + if (!owners[msg.sender]) revert NotAuthorized(); _; } modifier onlyBot() { - require(authorizedBots[msg.sender], "Not bot"); + if (!authorizedBots[msg.sender]) revert NotAuthorized(); _; } @@ -165,19 +197,19 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp // Ownership Management function addOwner(address _newOwner) external onlyOwner { - require(!owners[_newOwner], "Already an owner"); + if (owners[_newOwner]) revert AlreadyOwner(); owners[_newOwner] = true; } function removeOwner(address _owner) external onlyOwner { - require(owners[_owner], "Not an owner"); - require(_owner != msg.sender, "Cannot remove yourself"); + 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 { - require(bot != address(0), "Invalid address"); + if (bot == address(0)) revert InvalidAddress(); authorizedBots[bot] = true; } @@ -239,14 +271,14 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp /// @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 { - require(_addr != address(0), "Invalid address"); + 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 { - require(_addr != address(0), "Invalid address"); + if (_addr == address(0)) revert InvalidAddress(); addressFixedRate[_addr] = 0; } @@ -381,6 +413,53 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp 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 = withdrawVesting[oldAddress]; + uint256 withdrawVestingCount = oldWithdrawVestings.length; + if (withdrawVestingCount > 0) { + WithdrawVesting[] storage newWithdrawVestings = withdrawVesting[newAddress]; + for (uint256 i = 0; i < withdrawVestingCount; i++) { + newWithdrawVestings.push(oldWithdrawVestings[i]); + } + delete withdrawVesting[oldAddress]; + } + } + // /** // * @dev Extends the lastClaimed and unlockTime for all stakes of a given address // * @param _address The address whose stakes to extend @@ -469,7 +548,7 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp // } function createStake(uint256 _amount) external { - require(_amount > minStakeLock, "Amount must be greater minStakeLock"); + if (_amount <= minStakeLock) revert AmountBelowMinimum(); // Transfer tokens from the user into the contract IERC20(pool.tokenAddress).safeTransferFrom(msg.sender, address(this), _amount); @@ -502,11 +581,11 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp /// @notice Restake an expired stake with a bonus daily reward function restake(uint256 _stakeIndex, uint256 _restakePercentage) nonReentrant external { - require(_restakePercentage <= 100, "Invalid percentage"); + if (_restakePercentage > 100) revert InvalidRestakePercentage(); Stake storage stake = stakes[msg.sender][_stakeIndex]; // Ensure there is a stake to claim - require(stake.amount != 0, "No amount to claim"); - require(block.timestamp >= stake.unlockTime, "Stake is locked"); + if (stake.amount == 0) revert NothingToClaim(); + if (block.timestamp < stake.unlockTime) revert StakeLocked(); uint256 _amount = stake.amount; uint rewards = getPoolRewards(msg.sender, _stakeIndex); @@ -564,7 +643,7 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp } function createStakeForUser(address _user, uint256 _amount) external onlyOwner { - require(_amount != 0, "Invalid amount"); + if (_amount == 0) revert InvalidAmount(); stakes[_user].push(Stake({ amount: _amount, @@ -638,8 +717,8 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp } } - require(totalReward != 0, "No rewards"); - require(pool.totalRewards >= totalReward, "Insufficient rewards"); + if (totalReward == 0) revert NothingToClaim(); + if (pool.totalRewards < totalReward) revert InsufficientRewards(); pool.totalRewards = pool.totalRewards - totalReward; IERC20(pool.tokenAddress).safeTransfer(msg.sender, totalReward); @@ -650,7 +729,7 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp function claimStake(uint256 _stakeIndex) external nonReentrant { // Ensure the stake index is valid - require(_stakeIndex < stakes[msg.sender].length, "Invalid stake index"); + if (_stakeIndex >= stakes[msg.sender].length) revert InvalidStakeIndex(); // Load the stake Stake storage stake = stakes[msg.sender][_stakeIndex]; @@ -661,10 +740,10 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp _amount = _amount + rewards; // Ensure there is a stake to claim - require(_amount != 0, "amount 0"); + if (_amount == 0) revert NothingToClaim(); // Ensure the stake is unlocked (if using lockup periods) - require(block.timestamp >= stake.unlockTime, "Stake locked"); + if (block.timestamp < stake.unlockTime) revert StakeLocked(); // Update state before external calls stake.amount = 0; @@ -702,16 +781,16 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp */ function withdraw(uint256 _stakeIndex) external nonReentrant { WithdrawStake[] storage userStakes = withdrawStake[msg.sender]; - require(userStakes.length > 0, "No stakes"); + if (userStakes.length == 0) revert NoStakesAvailable(); for (uint256 i = 0; i < userStakes.length; ++i) { WithdrawStake storage stake = userStakes[i]; if (stake.stakeId == _stakeIndex && stake.amount != 0) { - require(block.timestamp >= stake.unlockTime, "Withdraw locked"); + if (block.timestamp < stake.unlockTime) revert StakeLocked(); uint256 _amount = stake.amount; uint256 poolBalance = IERC20(pool.tokenAddress).balanceOf(address(this)); - require(poolBalance >= _amount, "Insufficient rewards"); + if (poolBalance < _amount) revert InsufficientRewards(); // Update state before external calls withdrawLiabilities -= _amount; @@ -725,9 +804,43 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp } // Revert if no matching stake with non-zero amount was found - revert("Invalid stake"); + 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 = withdrawVesting[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; + + // Transfer tokens + IERC20(_token).safeTransfer(msg.sender, _amount); + emit StakeWithdrawn(msg.sender, _amount, _vestingId); + return; + } + } + + // Revert if no matching vesting with non-zero amount was found + revert StakeNotFound(); +} + function compoundAllRewards() external { uint256 totalReward = 0; @@ -741,7 +854,7 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp } } - require(totalReward > minStakeLock, "Not enough to compound"); + if (totalReward <= minStakeLock) revert NotEnoughToCompound(); // Check if user has a fixed reward rate set uint256 finalRewardRate; @@ -766,15 +879,15 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp } function createVesting(address _token, uint256 _amount) external { - require(_amount != 0, "Amount must be greater than zero"); + if (_amount == 0) revert InvalidAmount(); address oracle = priceOracles[_token]; - require(oracle != address(0), "Oracle not set"); + 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; - require(usdPrice > minStakeLock, "Amount must be greater minStakeLock"); + if (usdPrice <= minStakeLock) revert AmountBelowMinimum(); // Update user's dollarsVested dollarsVested[msg.sender] += usdPrice; @@ -861,12 +974,12 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp function claimVesting(uint256 _vestingIndex) external nonReentrant { Vesting storage vesting = vestings[msg.sender][_vestingIndex]; - require(vesting.complete == false, "Stake Complete"); + if (vesting.complete) revert StakeComplete(); uint256 maxClaim = getUnlockedVesting(msg.sender, _vestingIndex); - require(maxClaim >= vesting.claimedAmount, "Invalid claim"); + if (maxClaim < vesting.claimedAmount) revert InvalidClaimAmount(); uint256 amountToClaim = maxClaim - vesting.claimedAmount; - require(amountToClaim != 0, "Claim 0"); + if (amountToClaim == 0) revert NothingToClaim(); vesting.claimedAmount = vesting.claimedAmount + amountToClaim; if (vesting.claimedAmount >= vesting.amount) { @@ -882,7 +995,14 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp } } vestedTotal[vesting.token] -= amountToClaim; - IERC20(vesting.token).safeTransfer(msg.sender, amountToClaim); + + // Add vesting claims to cooldown queue + withdrawVesting[msg.sender].push(WithdrawVesting({ + vestingId: withdrawVestingCounter++, + amount: amountToClaim, + unlockTime: block.timestamp + unlockDelay, + token: vesting.token + })); emit VestingClaimed(msg.sender, amountToClaim, 0); } @@ -896,7 +1016,7 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp if (vesting.token == _token && !vesting.complete) { uint256 maxClaim = getUnlockedVesting(msg.sender, i); - require(maxClaim >= vesting.claimedAmount, "Invalid claim"); + if (maxClaim < vesting.claimedAmount) revert InvalidClaimAmount(); uint256 amountToClaim = maxClaim - vesting.claimedAmount; if (amountToClaim > 0) { @@ -914,7 +1034,7 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp } } - require(totalReward != 0, "No rewards to claim"); + if (totalReward == 0) revert NothingToClaim(); // Update user's dollarsVested if (dollarsVested[msg.sender] > 0) { @@ -926,13 +1046,16 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp } } - // Ensure the contract has enough balance to fulfill the claim - uint256 poolBalance = IERC20(_token).balanceOf(address(this)); - require(poolBalance >= totalReward, "Insufficient rewards"); // Update vesting total vestedTotal[_token] -= totalReward; - // Transfer the aggregated reward - IERC20(_token).safeTransfer(msg.sender, totalReward); + + // Add vesting claims to cooldown queue + withdrawVesting[msg.sender].push(WithdrawVesting({ + vestingId: withdrawVestingCounter++, + amount: totalReward, + unlockTime: block.timestamp + unlockDelay, + token: _token + })); emit RewardClaimed(msg.sender, totalReward); } @@ -942,9 +1065,9 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp Vesting storage vesting = vestings[msg.sender][_vestingIndex]; uint256 maxBonus = getUnlockedVestingBonus(msg.sender, _vestingIndex); - require(maxBonus >= vesting.claimedBonus, "Invalid claim amount"); + if (maxBonus < vesting.claimedBonus) revert InvalidClaimAmount(); uint256 bonusToClaim = maxBonus - vesting.claimedBonus; - require(bonusToClaim != 0, "No claim"); + if (bonusToClaim == 0) revert NothingToClaim(); vesting.claimedBonus = vesting.claimedBonus + bonusToClaim; withdrawLiabilities += bonusToClaim; @@ -1074,7 +1197,7 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp return userStakes[i]; } } - revert("WithdrawStake not found"); + revert StakeNotFound(); } /// @notice Function that lets you look up an address’s stake by vestingId. @@ -1088,7 +1211,7 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp return userStakes[i]; } } - revert("WithdrawStake not found"); + revert StakeNotFound(); } /// @notice Function that returns an array of all the user's withdrawStakes. @@ -1098,17 +1221,30 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp 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 withdrawVesting[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; + } + /// @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]; - require(!stake.complete, "Stake complete"); - require(stake.amount != 0, "Amount 0"); + if (stake.complete) revert StakeComplete(); + if (stake.amount == 0) revert InvalidAmount(); // Ensure the stake isn't already on sale. - require(sellStakes[msg.sender][_stakeId].amount == 0, "Stake already on sale"); - require(price >= (stake.amount * sellMin) / 100, "Price too low"); + 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({ @@ -1135,11 +1271,11 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp /// @param _stakeId The stake ID to cancel the sale. function cancelSellStake(uint256 _stakeId) external { SellStake storage sellStakeEntry = sellStakes[msg.sender][_stakeId]; - require(sellStakeEntry.amount != 0, "Stake not found"); + if (sellStakeEntry.amount == 0) revert StakeNotFound(); // Access the original stake. Stake storage stake = stakes[msg.sender][_stakeId]; - require(stake.amount == 0, "Stake not for sale"); + if (stake.amount != 0) revert StakeNotInSellState(); // Restore the original stake's amount. stake.amount = sellStakeEntry.amount; @@ -1165,8 +1301,8 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp /// @param newPrice The new price of the stake. function updateSellStake(uint256 _stakeId, uint256 newPrice) external { SellStake storage sellStakeEntry = sellStakes[msg.sender][_stakeId]; - require(sellStakeEntry.amount != 0, "Stake not found"); - require(newPrice >= (sellStakeEntry.amount * sellMin) / 100, "New price too low"); + if (sellStakeEntry.amount == 0) revert StakeNotFound(); + if (newPrice < (sellStakeEntry.amount * sellMin) / 100) revert PriceTooLow(); sellStakeEntry.bonusAmount = (newPrice * sellKickBack) / 100; sellStakeEntry.price = newPrice; @@ -1184,7 +1320,7 @@ contract PacaFinanceWithBoostAndScheduleUSDT is Initializable, ReentrancyGuardUp /// @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]; - require(sellStakeEntry.amount != 0, "Stake not available"); + 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); diff --git a/hardhat.config.js b/hardhat.config.js index bcb7332..c8693eb 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -2,7 +2,7 @@ require("@openzeppelin/hardhat-upgrades"); require("@nomicfoundation/hardhat-ignition-ethers"); // require("@nomiclabs/hardhat-ethers"); // require("@nomiclabs/hardhat-etherscan"); -// require("hardhat-contract-sizer"); +require("hardhat-contract-sizer"); // require("dotenv").config(); // require("hardhat-gas-reporter"); require("@nomicfoundation/hardhat-verify"); @@ -12,6 +12,12 @@ const env = process.env; // https://hardhat.org/guides/create-task.html module.exports = { + contractSizer: { + alphaSort: true, + disambiguatePaths: false, + runOnCompile: true, + strict: true, + }, solidity: { compilers: [ {