PRIV ProtocolPRIV Docs
Contracts

PRIVStaking

Stake PRIV tokens to earn time-based rewards with configurable lock durations.

Overview

PRIVStaking is a staking contract that allows PRIV token holders to earn rewards based on the amount staked and time duration. It implements a reward-per-token model with configurable reward rates and lock periods.

PropertyValue
Minimum Stake1 PRIV
Minimum Total Staked for Rewards1,000 PRIV
Precision1e18
NetworkBase (Chain ID: 8453)

Key Features

  • Time-based Rewards: Rewards accumulate based on stake amount and duration
  • Configurable Lock Period: Owner can set required lock duration for unstaking
  • First Depositor Attack Prevention: Rewards only accrue when totalStaked is at least 1,000 PRIV
  • Anyone Can Fund: fundRewardsPool() is permissionless
  • Partial Unstaking: Users can unstake portions of their stake
  • Emergency Pause: Contract can be paused by owner

How It Works

User stakes PRIV  -->  Rewards accrue over time  -->  Claim or unstake
       |                       |                           |
       v                       v                           v
  Lock period starts    rewardRate * time         Tokens + rewards
                        / totalStaked             returned to user

Reward Calculation

Rewards are calculated using the reward-per-token model:

rewardPerToken = rewardPerTokenStored + (timeElapsed * rewardRate * PRECISION / totalStaked)

userReward = (userStake * (rewardPerToken - userRewardPerTokenPaid) / PRECISION) + pendingRewards

Important: Rewards only accrue when totalStaked >= MIN_TOTAL_STAKED_FOR_REWARDS (1,000 PRIV). This prevents first depositor attacks where a small stake could capture outsized rewards.

APY Estimation

estimatedAPY = (rewardRate * 365 days * 10000) / totalStaked

Returns APY in basis points (100 = 1%).


Contract Interface

Constants

/// @notice Precision factor for reward calculations
uint256 public constant PRECISION = 1e18;

/// @notice Minimum stake amount (1 PRIV)
uint256 public constant MIN_STAKE_AMOUNT = 1e18;

/// @notice Minimum total staked before rewards start accruing
uint256 public constant MIN_TOTAL_STAKED_FOR_REWARDS = 1000e18;

State Variables

/// @notice The PRIV token contract
IERC20 public immutable privToken;

/// @notice Reward rate in tokens per second (scaled by PRECISION)
uint256 public rewardRate;

/// @notice Time lock duration for unstaking (in seconds)
uint256 public lockDuration;

/// @notice Total amount of PRIV tokens staked
uint256 public totalStaked;

/// @notice Total rewards available in the pool
uint256 public rewardsPool;

Structs

StakeInfo

struct StakeInfo {
    uint256 amount;      // Amount staked
    uint256 stakedAt;    // Timestamp of stake
    uint256 unlockTime;  // Timestamp when tokens can be unstaked
}

Functions

User Functions

stake

Stake PRIV tokens to start earning rewards.

function stake(uint256 amount) external nonReentrant whenNotPaused
ParameterTypeDescription
amountuint256Amount of PRIV to stake (18 decimals)

Requirements:

  • Amount must be greater than 0
  • First stake must be at least MIN_STAKE_AMOUNT (1 PRIV)
  • User must have approved the contract
  • Contract must not be paused

Effects:

  • Transfers tokens from user to contract
  • Updates user's stake info
  • Resets lock timer to block.timestamp + lockDuration

unstake

Unstake all tokens and claim pending rewards.

function unstake() external nonReentrant whenNotPaused

Requirements:

  • User must have staked tokens
  • Lock period must have elapsed (block.timestamp >= unlockTime)
  • Contract must not be paused

Effects:

  • Claims all pending rewards
  • Returns all staked tokens to user
  • Resets stake info to zero

unstakePartial

Unstake a portion of staked tokens without claiming rewards.

function unstakePartial(uint256 amount) external nonReentrant whenNotPaused
ParameterTypeDescription
amountuint256Amount of PRIV to unstake

Requirements:

  • Amount must be greater than 0 and not exceed staked amount
  • Lock period must have elapsed
  • Contract must not be paused

claimRewards

Claim pending rewards without unstaking.

function claimRewards() external nonReentrant whenNotPaused

Requirements:

  • User must have pending rewards greater than 0
  • Rewards pool must have sufficient balance
  • Contract must not be paused

fundRewardsPool

Add tokens to the rewards pool. Anyone can call this function.

function fundRewardsPool(uint256 amount) external nonReentrant
ParameterTypeDescription
amountuint256Amount of PRIV to add to pool

View Functions

earned

Calculate pending rewards for a user.

function earned(address account) public view returns (uint256)

rewardPerToken

Get the current reward per token value.

function rewardPerToken() public view returns (uint256)

getStakeInfo

Get the stake info for a user.

function getStakeInfo(address account) external view returns (StakeInfo memory)

isLocked

Check if a user's tokens are still locked.

function isLocked(address account) external view returns (bool)

timeUntilUnlock

Get the remaining time until unlock.

function timeUntilUnlock(address account) external view returns (uint256)

estimatedAPY

Get the estimated APY based on current parameters.

function estimatedAPY() external view returns (uint256)

Returns: APY in basis points (100 = 1%)


availableRewardsForWithdrawal

Get rewards available for owner withdrawal (pool minus pending rewards).

function availableRewardsForWithdrawal() external view returns (uint256)

Admin Functions

setRewardRate

Update the reward rate (tokens per second).

function setRewardRate(uint256 newRate) external onlyOwner

setLockDuration

Update the lock duration for new stakes.

function setLockDuration(uint256 newDuration) external onlyOwner

withdrawRewards

Withdraw excess rewards from the pool (only available rewards, not pending).

function withdrawRewards(uint256 amount) external onlyOwner

pause / unpause

Emergency pause controls.

function pause() external onlyOwner
function unpause() external onlyOwner

Events

/// @notice Emitted when a user stakes tokens
event Staked(address indexed user, uint256 amount, uint256 unlockTime);

/// @notice Emitted when a user unstakes tokens
event Unstaked(address indexed user, uint256 amount);

/// @notice Emitted when a user claims rewards
event RewardsClaimed(address indexed user, uint256 amount);

/// @notice Emitted when the rewards pool is funded
event RewardsPoolFunded(address indexed funder, uint256 amount);

/// @notice Emitted when the reward rate is updated
event RewardRateUpdated(uint256 oldRate, uint256 newRate);

/// @notice Emitted when the lock duration is updated
event LockDurationUpdated(uint256 oldDuration, uint256 newDuration);

/// @notice Emitted when rewards are withdrawn by owner
event RewardsWithdrawn(uint256 amount);

Errors

error InvalidAddress();
error InvalidAmount();
error BelowMinimumStake();
error NoStake();
error TokensLocked();
error InsufficientRewardsPool();
error InvalidRewardRate();
error TransferFailed();
error InsufficientAvailableRewards();

Usage Examples

Stake Tokens

import { useWriteContract, useReadContract } from 'wagmi'
import { parseEther } from 'viem'

function useStakePriv() {
  const { writeContract } = useWriteContract()

  return async (amount: string) => {
    const amountWei = parseEther(amount)

    // 1. Approve staking contract
    await writeContract({
      address: PRIV_TOKEN_ADDRESS,
      abi: privTokenAbi,
      functionName: 'approve',
      args: [STAKING_ADDRESS, amountWei],
    })

    // 2. Stake tokens
    await writeContract({
      address: STAKING_ADDRESS,
      abi: stakingAbi,
      functionName: 'stake',
      args: [amountWei],
    })
  }
}

Check Pending Rewards

function usePendingRewards(address: string) {
  return useReadContract({
    address: STAKING_ADDRESS,
    abi: stakingAbi,
    functionName: 'earned',
    args: [address],
  })
}

Claim Rewards

function useClaimRewards() {
  const { writeContract } = useWriteContract()

  return () => {
    writeContract({
      address: STAKING_ADDRESS,
      abi: stakingAbi,
      functionName: 'claimRewards',
    })
  }
}

Check APY

function useEstimatedAPY() {
  const { data: apyBps } = useReadContract({
    address: STAKING_ADDRESS,
    abi: stakingAbi,
    functionName: 'estimatedAPY',
  })

  // Convert basis points to percentage
  const apyPercent = apyBps ? Number(apyBps) / 100 : 0
  return apyPercent
}

Fund Rewards Pool

// Anyone can fund the rewards pool
async function fundPool(amount: string) {
  const amountWei = parseEther(amount)

  await writeContract({
    address: PRIV_TOKEN_ADDRESS,
    abi: privTokenAbi,
    functionName: 'approve',
    args: [STAKING_ADDRESS, amountWei],
  })

  await writeContract({
    address: STAKING_ADDRESS,
    abi: stakingAbi,
    functionName: 'fundRewardsPool',
    args: [amountWei],
  })
}

Security Notes

  1. First Depositor Attack Prevention: The contract requires a minimum of 1,000 PRIV total staked before rewards start accruing. This prevents attackers from staking small amounts to capture outsized rewards.

  2. Reward Pool Protection: The owner cannot withdraw rewards that are owed to stakers. The totalPendingRewards is tracked separately to ensure user rewards are protected.

  3. Lock Duration: Users should be aware of the lock duration before staking. Once staked, tokens cannot be withdrawn until the lock period expires.

  4. Reentrancy Protection: All state-changing functions use nonReentrant modifier.

  5. Emergency Pause: The owner can pause the contract in emergencies. Users cannot stake, unstake, or claim while paused.


Source Code

View on GitHub