Error Refactor and Vesting Delay

This commit is contained in:
2025-07-07 19:32:17 +02:00
parent ace13dea70
commit c8da95a2b0
3 changed files with 381 additions and 101 deletions

View File

@@ -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 addresss 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);

View File

@@ -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 addresss 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);