PRIVStakingV2 (ve-PRIV)
ve-PRIV staking contract with 5 lock tiers, weighted reward distribution, auto-compound, and tier upgrades.
Overview
PRIVStakingV2 is a ve-PRIV staking contract that distributes rewards proportionally to each staker's effective stake (raw amount multiplied by a lock-tier multiplier). Longer lock commitments earn disproportionately more rewards. It replaces the flat-lock PRIVStaking (V1) model with five configurable lock tiers, auto-compounding with a 15% bonus, and tier upgrades.
Both V1 and V2 implement the IPRIVStaking interface (fundRewardsPool), so either can receive fee distributions from FeeManagerV2.
| Property | Value |
|---|---|
| Contract | PRIVStakingV2 |
| Address (Base Sepolia) | 0xFd0b01756178DADCBCfCc1ED337c0e6Dde29B62E |
| Minimum Stake | 1 PRIV |
| Min Total Effective for Rewards | 1,000 PRIV |
| Lock Tiers | 5 (Flex, Bronze, Silver, Gold, Diamond) |
| Auto-Compound Bonus | 15% |
| Precision | 1e18 |
| Network | Base (Chain ID: 8453) |
Key Features
- 5 Lock Tiers: Flex (7d, 1.0x), Bronze (30d, 2.0x), Silver (90d, 3.5x), Gold (180d, 5.0x), Diamond (365d, 8.0x)
- Weighted Reward Distribution: Rewards accrue proportionally to effective stake, not raw amount
- Auto-Compound: Re-stake rewards with a 15% bonus (deducted from pool, not minted)
- Tier Upgrades: Upgrade to a higher tier anytime; downgrades are blocked
- First Depositor Attack Prevention: Rewards only accrue when totalEffectiveStake >= 1,000 PRIV
- Reward Pool Protection:
totalPendingRewardstracking prevents owner from draining staker rewards - Anyone Can Fund:
fundRewardsPool()is permissionless (used by FeeManagerV2) - Emergency Pause: Contract can be paused by owner
Lock Tiers
| Index | Name | Lock Duration | Multiplier (BPS) | Multiplier |
|---|---|---|---|---|
| 0 | Flex | 7 days | 10,000 | 1.0x |
| 1 | Bronze | 30 days | 20,000 | 2.0x |
| 2 | Silver | 90 days | 35,000 | 3.5x |
| 3 | Gold | 180 days | 50,000 | 5.0x |
| 4 | Diamond | 365 days | 80,000 | 8.0x |
Tier configurations can be updated by the owner via setTier(), but must maintain ascending order for both duration and multiplier.
How It Works
User chooses tier --> Stakes PRIV --> Effective stake calculated --> Rewards accrue
| | | |
v v v v
Lock period set Tokens locked amount × multiplier / 10000 rewardRate × time
/ totalEffectiveStake
|
v
Claim, auto-compound, or unstake after lockReward Calculation
Rewards use a weighted reward-per-token model. The key difference from V1 is that rewards are distributed based on effective stake rather than raw token amount:
rewardPerToken = rewardPerTokenStored +
(timeElapsed × rewardRate × PRECISION / totalEffectiveStake)
userReward = (effectiveAmount × (rewardPerToken - userRewardPerTokenPaid) / PRECISION)
+ pendingRewardsImportant: Rewards only accrue when totalEffectiveStake >= MIN_TOTAL_EFFECTIVE_FOR_REWARDS (1,000 PRIV). This prevents first depositor attacks where a small stake could capture outsized rewards.
APY Estimation
APY is calculated per tier since each tier has a different multiplier:
estimatedAPY(tierIndex) = (rewardRate × 365 days × multiplierBps × 10000)
/ (totalEffectiveStake × BPS_DENOMINATOR)Returns APY in basis points (100 = 1%).
Contract Interface
Constants
uint256 public constant PRECISION = 1e18;
uint256 public constant BPS_DENOMINATOR = 10_000;
uint256 public constant MIN_STAKE_AMOUNT = 1e18; // 1 PRIV
uint256 public constant MIN_TOTAL_EFFECTIVE_FOR_REWARDS = 1000e18; // 1,000 PRIV effective
uint256 public constant AUTO_COMPOUND_BONUS_BPS = 1500; // 15%
uint256 public constant NUM_TIERS = 5;State Variables
IERC20 public immutable privToken; // The PRIV token contract
LockTier[5] public tiers; // Lock tier configurations (index 0-4)
uint256 public rewardRate; // Tokens per second (scaled by PRECISION)
uint256 public totalStaked; // Total actual PRIV staked
uint256 public totalEffectiveStake; // Total weighted stake (sum of all effective amounts)
uint256 public rewardsPool; // Total rewards available in the pool
uint256 public rewardPerTokenStored; // Accumulated reward per effective token
uint256 public lastUpdateTime; // Last time rewards were updated
uint256 public totalPendingRewards; // Total pending rewards owed to all stakersStructs
LockTier
struct LockTier {
uint256 duration; // Lock duration in seconds
uint256 multiplierBps; // Reward multiplier in BPS (10000 = 1x)
}StakePosition
struct StakePosition {
uint256 amount; // Actual PRIV deposited
uint256 effectiveAmount; // amount × multiplierBps / BPS_DENOMINATOR
uint8 tierIndex; // Lock tier (0=Flex, 1=Bronze, 2=Silver, 3=Gold, 4=Diamond)
uint256 stakedAt; // Timestamp of last stake/upgrade
uint256 unlockTime; // When tokens can be withdrawn
}Functions
User Functions
stake
Stake PRIV tokens with a chosen lock tier.
function stake(uint256 amount, uint8 tierIndex) external nonReentrant whenNotPaused| Parameter | Type | Description |
|---|---|---|
amount | uint256 | Amount of PRIV to stake (18 decimals) |
tierIndex | uint8 | Lock tier (0=Flex, 1=Bronze, 2=Silver, 3=Gold, 4=Diamond) |
Requirements:
- Amount must be greater than 0
- First stake must be at least MIN_STAKE_AMOUNT (1 PRIV)
- Tier index must be 0-4
- Cannot downgrade tier on existing position
- User must have approved the contract
- Contract must not be paused
Effects:
- Transfers tokens from user to contract
- Calculates effective amount based on tier multiplier
- If upgrading tier on existing position, recalculates entire effective amount
- Resets lock timer to
block.timestamp + tier.duration
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
- Clears position entirely
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
autoCompound
Re-stake pending rewards with a 15% bonus. The bonus is deducted from the rewards pool, not minted.
function autoCompound() external nonReentrant whenNotPausedRequirements:
- User must have an active position
- User must have pending rewards greater than 0
- Rewards pool must have enough to cover reward + 15% bonus
- Contract must not be paused
Effects:
- Adds reward + bonus to the user's staked amount
- Recalculates effective amount with current tier multiplier
- Resets lock timer
- Deducts total compound amount from rewards pool
upgradeTier
Upgrade to a higher lock tier for a better reward multiplier without depositing additional tokens.
function upgradeTier(uint8 newTierIndex) external nonReentrant whenNotPaused| Parameter | Type | Description |
|---|---|---|
newTierIndex | uint8 | The tier to upgrade to (must be strictly higher than current) |
Requirements:
- User must have an active position
- New tier must be strictly higher than current tier (same tier reverts with
SameTier) - Contract must not be paused
Effects:
- Recalculates effective amount with new tier's multiplier
- Resets lock timer to new tier's duration
- Does not require additional token deposit
fundRewardsPool
Add tokens to the rewards pool. Anyone can call this function. FeeManagerV2 calls this via processFees() to route the staker share of protocol fees.
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 effective token value.
function rewardPerToken() public view returns (uint256)getPosition
Get the full stake position for a user.
function getPosition(address account) external view returns (StakePosition memory)getTier
Get the lock tier configuration for a given index.
function getTier(uint8 tierIndex) external view returns (LockTier 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 (0 if already unlocked).
function timeUntilUnlock(address account) external view returns (uint256)estimatedAPY
Get the estimated APY for a specific tier based on current parameters.
function estimatedAPY(uint8 tierIndex) external view returns (uint256)| Parameter | Type | Description |
|---|---|---|
tierIndex | uint8 | The tier to estimate APY for (0-4) |
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, scaled by 1e18).
function setRewardRate(uint256 newRate) external onlyOwnersetTier
Update a lock tier's duration and multiplier. Does not affect existing positions. Tiers must maintain ascending order.
function setTier(uint8 tierIndex, uint256 duration, uint256 multiplierBps) external onlyOwner| Parameter | Type | Description |
|---|---|---|
tierIndex | uint8 | Which tier to update (0-4) |
duration | uint256 | New lock duration in seconds |
multiplierBps | uint256 | New multiplier in basis points (10000 = 1x) |
withdrawRewards
Withdraw excess rewards from the pool. Only withdraws from available rewards (pool minus totalPendingRewards), protecting staker rewards.
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, uint8 tierIndex, uint256 effectiveAmount, 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 rewards are auto-compounded
event AutoCompounded(address indexed user, uint256 rewardAmount, uint256 bonusAmount, uint256 newEffectiveAmount);
/// @notice Emitted when a user upgrades their lock tier
event TierUpgraded(address indexed user, uint8 oldTier, uint8 newTier, uint256 newEffectiveAmount, uint256 newUnlockTime);
/// @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 a tier configuration is updated
event TierUpdated(uint8 indexed tierIndex, uint256 duration, uint256 multiplierBps);
/// @notice Emitted when rewards are withdrawn by owner
event RewardsWithdrawn(uint256 amount);Errors
error InvalidAddress();
error InvalidAmount();
error InvalidTier();
error BelowMinimumStake();
error NoStake();
error TokensLocked();
error CannotDowngradeTier();
error InsufficientRewardsPool();
error InvalidRewardRate();
error InsufficientAvailableRewards();
error InvalidTierConfig();
error SameTier();Usage Examples
Stake Tokens (Diamond Tier)
import { useWriteContract, useReadContract } from 'wagmi'
import { parseEther } from 'viem'
function useStakePriv() {
const { writeContract } = useWriteContract()
return async (amount: string, tierIndex: number) => {
const amountWei = parseEther(amount)
// 1. Approve staking contract
await writeContract({
address: PRIV_TOKEN_ADDRESS,
abi: privTokenAbi,
functionName: 'approve',
args: [STAKING_V2_ADDRESS, amountWei],
})
// 2. Stake tokens with tier selection
// Tier indexes: 0=Flex, 1=Bronze, 2=Silver, 3=Gold, 4=Diamond
await writeContract({
address: STAKING_V2_ADDRESS,
abi: stakingV2Abi,
functionName: 'stake',
args: [amountWei, tierIndex],
})
}
}Check Position and Pending Rewards
function useStakingPosition(address: string) {
const { data: position } = useReadContract({
address: STAKING_V2_ADDRESS,
abi: stakingV2Abi,
functionName: 'getPosition',
args: [address],
})
const { data: pendingRewards } = useReadContract({
address: STAKING_V2_ADDRESS,
abi: stakingV2Abi,
functionName: 'earned',
args: [address],
})
return { position, pendingRewards }
}Auto-Compound Rewards
function useAutoCompound() {
const { writeContract } = useWriteContract()
return () => {
writeContract({
address: STAKING_V2_ADDRESS,
abi: stakingV2Abi,
functionName: 'autoCompound',
})
}
}Upgrade Tier
function useUpgradeTier() {
const { writeContract } = useWriteContract()
return (newTierIndex: number) => {
writeContract({
address: STAKING_V2_ADDRESS,
abi: stakingV2Abi,
functionName: 'upgradeTier',
args: [newTierIndex],
})
}
}Check APY per Tier
function useEstimatedAPY(tierIndex: number) {
const { data: apyBps } = useReadContract({
address: STAKING_V2_ADDRESS,
abi: stakingV2Abi,
functionName: 'estimatedAPY',
args: [tierIndex],
})
// 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_V2_ADDRESS, amountWei],
})
await writeContract({
address: STAKING_V2_ADDRESS,
abi: stakingV2Abi,
functionName: 'fundRewardsPool',
args: [amountWei],
})
}Security Notes
-
First Depositor Attack Prevention: The contract requires a minimum of 1,000 PRIV total effective stake 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.
totalPendingRewardsis tracked separately and subtracted from the pool when calculatingavailableRewardsForWithdrawal(). -
Lock Duration: Users should understand their chosen tier's lock duration before staking. Once staked, tokens cannot be withdrawn until the lock period expires. Tier upgrades and auto-compounds reset the lock timer.
-
Tier Ordering Invariant: The
setTier()admin function enforces that tiers maintain ascending order of both duration and multiplier, preventing misconfigurations. -
Reentrancy Protection: All state-changing functions use the
nonReentrantmodifier. -
Emergency Pause: The owner can pause the contract in emergencies. Users cannot stake, unstake, claim, compound, or upgrade while paused.
V1 Backward Compatibility
PRIVStaking (V1) remains deployed for backward compatibility. Both V1 and V2 implement the IPRIVStaking interface, which defines fundRewardsPool(uint256). FeeManagerV2 can be configured to route the staker share of fees to either contract.
| Feature | V1 (PRIVStaking) | V2 (PRIVStakingV2) |
|---|---|---|
| Lock model | Single configurable duration | 5 lock tiers |
| Multipliers | None | 1.0x to 8.0x |
| Reward distribution | Proportional to raw amount | Proportional to effective stake |
| Auto-compound | Not available | 15% bonus |
| Tier upgrades | N/A | Supported |
| Partial unstake | Supported | Full unstake only |
Source Code
PrivPresale
Multi-stage presale contract with 10 progressive pricing stages, ETH/USDC/card payments, Chainlink oracle integration, and 20% TGE + 6-month linear vesting.
BountyEscrow
Data bounty escrow contract with three payment tiers, oracle-driven quality assurance, auto-payout on approval, timelocked payouts, and fee routing through FeeManager.