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.
| Property | Value |
|---|---|
| Minimum Stake | 1 PRIV |
| Minimum Total Staked for Rewards | 1,000 PRIV |
| Precision | 1e18 |
| Network | Base (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 userReward Calculation
Rewards are calculated using the reward-per-token model:
rewardPerToken = rewardPerTokenStored + (timeElapsed * rewardRate * PRECISION / totalStaked)
userReward = (userStake * (rewardPerToken - userRewardPerTokenPaid) / PRECISION) + pendingRewardsImportant: 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) / totalStakedReturns 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| Parameter | Type | Description |
|---|---|---|
amount | uint256 | Amount 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 whenNotPausedRequirements:
- 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| Parameter | Type | Description |
|---|---|---|
amount | uint256 | Amount 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 whenNotPausedRequirements:
- 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| Parameter | Type | Description |
|---|---|---|
amount | uint256 | Amount 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 onlyOwnersetLockDuration
Update the lock duration for new stakes.
function setLockDuration(uint256 newDuration) external onlyOwnerwithdrawRewards
Withdraw excess rewards from the pool (only available rewards, not pending).
function withdrawRewards(uint256 amount) external onlyOwnerpause / unpause
Emergency pause controls.
function pause() external onlyOwner
function unpause() external onlyOwnerEvents
/// @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
-
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.
-
Reward Pool Protection: The owner cannot withdraw rewards that are owed to stakers. The
totalPendingRewardsis tracked separately to ensure user rewards are protected. -
Lock Duration: Users should be aware of the lock duration before staking. Once staked, tokens cannot be withdrawn until the lock period expires.
-
Reentrancy Protection: All state-changing functions use
nonReentrantmodifier. -
Emergency Pause: The owner can pause the contract in emergencies. Users cannot stake, unstake, or claim while paused.