Skip to main content

Overview

The Vault Staking contract enables users to stake supported tokens for a fixed duration and earn yield based on token-specific APY rates and lock duration multipliers. Contract Address: 0xB156a66521BCB5A903daA42879A3e562E402Fa41 Verification: View on BaseScan

Key Features

  • Time-locked staking with flexible durations (1-12 months)
  • Token-specific APY rates (BTC: 10%, SOL: 9%, USDC: 6%)
  • APY multipliers based on lock duration
  • Early withdrawal with proportional penalties
  • Emergency withdrawal mechanism
  • Pausable for security
  • Reentrancy protection

Supported Tokens

Only these tokens can be staked:
TokenAddressDecimalsAPY
BTC0x7d9E31f5cCac4b9c8566f343A6bD6f3263DFcC91810%
SOL0x241ECE6Dce0E0825F9992410B3fA5d4b8fC8d19999%
USDC0xBEE08798a3634e29F47e3d277C9d11507D55F66a66%

APY Multipliers

Longer lock durations earn higher yields:
Lock DurationMultiplierEffective APY (BTC)
1 month1.0x10%
3 months1.2x12%
6 months1.5x15%
12 months2.0x20%

Main Functions

stakeToken

Stake ERC20 tokens with a specified lock duration.
token
address
required
Address of the token to stake (must be BTC, SOL, or USDC)
amount
uint256
required
Amount of tokens to stake (in token’s smallest unit)
lockDuration
uint256
required
Lock duration in seconds (MIN: 30 days, MAX: 365 days)
Constants:
  • MIN_LOCK_DURATION: 2,592,000 seconds (30 days)
  • MAX_LOCK_DURATION: 31,536,000 seconds (365 days)

stakeETH

Stake native ETH (payable function).
lockDuration
uint256
required
Lock duration in seconds
ETH staking is currently supported but not actively used in the frontend. The platform focuses on ERC20 token staking (BTC, SOL, USDC).

withdraw

Withdraw tokens after the lock period expires.
stakeId
uint256
required
The unique ID of the stake to withdraw
Withdraws the full amount including earned yield when the unlock date is reached.

withdrawEarly

Withdraw tokens before the lock period expires (with penalty).
stakeId
uint256
required
The unique ID of the stake to withdraw early
Penalty Calculation:
// Earned yield = (elapsed time / total time) × total yield
earnedYield = (elapsedTime / totalLockTime) × totalYield

// Penalty = unearned yield (deducted from principal)
penalty = totalYield - earnedYield

// User receives: principal - penalty
amountAfterPenalty = principal - penalty
Penalties are sent to the Treasury wallet.

calculatePenalty

View function to calculate early withdrawal penalty before executing.
stakeId
uint256
required
The stake ID to calculate penalty for
Returns:
  • penalty (uint256) - Penalty amount in tokens
  • amountAfterPenalty (uint256) - Amount user will receive

Stake Structure

Each stake contains:
struct Stake {
  uint256 amount;          // Principal amount staked
  uint256 lockDate;        // Timestamp when stake was created
  uint256 unlockDate;      // Timestamp when stake can be withdrawn
  uint256 lockDuration;    // Duration in seconds
  uint256 apy;             // APY rate (with multiplier applied)
  address token;           // Address of staked token
  bool withdrawn;          // Whether stake has been withdrawn
  uint256 totalYield;      // Total yield to be earned
}

Integration Example

Staking tokens from the frontend:
import { parseUnits, formatUnits } from 'viem';
import { VAULT_STAKING_ADDRESS, TOKEN_ADDRESSES } from '@/services/vaultService';

const VAULT_STAKING_ABI = [
  {
    inputs: [
      { internalType: 'address', name: 'token', type: 'address' },
      { internalType: 'uint256', name: 'amount', type: 'uint256' },
      { internalType: 'uint256', name: 'lockDuration', type: 'uint256' },
    ],
    name: 'stakeToken',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
];

// Stake 0.001 BTC for 6 months
const tokenAddress = TOKEN_ADDRESSES.BTC;
const amount = parseUnits('0.001', 8); // BTC has 8 decimals
const lockDuration = 15_552_000; // 180 days in seconds

// Step 1: Approve vault contract
const approveData = `0x095ea7b3${
  VAULT_STAKING_ADDRESS.slice(2).padStart(64, '0')
}${
  amount.toString(16).padStart(64, '0')
}`;

const approveTx = await walletClient.sendTransaction({
  to: tokenAddress,
  data: approveData,
});

await publicClient.waitForTransactionReceipt({ hash: approveTx });

// Step 2: Stake tokens
const stakeData = await walletClient.writeContract({
  address: VAULT_STAKING_ADDRESS,
  abi: VAULT_STAKING_ABI,
  functionName: 'stakeToken',
  args: [tokenAddress, amount, lockDuration],
});

Querying Stakes

Retrieve user’s stakes:
import { getUserStakeIds, getStakeDetails } from '@/services/vaultService';

// Get all stake IDs for user
const stakeIds = await getUserStakeIds(userAddress);
console.log('User has', stakeIds.length, 'stakes');

// Get details for each stake
for (const stakeId of stakeIds) {
  const stake = await getStakeDetails(stakeId);
  
  if (stake) {
    console.log('Stake ID:', stakeId);
    console.log('Amount:', formatUnits(stake.amount, 8));
    console.log('Token:', stake.token);
    console.log('Unlock Date:', new Date(Number(stake.unlockDate) * 1000));
    console.log('Total Yield:', formatUnits(stake.totalYield, 8));
    console.log('Withdrawn:', stake.withdrawn);
  }
}

Penalty Calculation Example

Calculating early withdrawal penalty:
import { calculateEarlyWithdrawalPenalty } from '@/services/vaultService';

const principal = 1000; // 1000 USDC
const lockDate = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 days ago
const unlockDate = Date.now() + 150 * 24 * 60 * 60 * 1000; // 150 days from now
const totalYield = 50; // 50 USDC expected yield
const currentTime = Date.now();

const penalty = calculateEarlyWithdrawalPenalty(
  principal,
  lockDate,
  unlockDate,
  totalYield,
  currentTime
);

console.log('Penalty Amount:', penalty.penalty); // Unearned yield
console.log('Earned Yield:', penalty.earnedYield); // Proportional yield
console.log('Penalty %:', penalty.penaltyPercentage.toFixed(2) + '%');
console.log('You will receive:', penalty.amountAfterPenalty);
console.log('Days remaining:', penalty.remainingDays);

// Example output:
// Penalty Amount: 41.67 USDC
// Earned Yield: 8.33 USDC
// Penalty %: 83.33%
// You will receive: 958.33 USDC (1000 - 41.67)
// Days remaining: 150
Critical: Early withdrawal penalties are deducted from the principal, not just forfeiting yield. Users receive principal - penalty, where penalty equals unearned yield.

Contract Events

Staked

Emitted when tokens are staked:
event Staked(
  address indexed user,
  uint256 indexed stakeId,
  address indexed token,
  uint256 amount,
  uint256 lockDuration,
  uint256 unlockDate
);

Withdrawn

Emitted when tokens are withdrawn:
event Withdrawn(
  address indexed user,
  uint256 indexed stakeId,
  address indexed token,
  uint256 amount,
  uint256 penalty,
  bool isEarlyWithdrawal
);

PenaltySentToTreasury

Emitted when penalties are collected:
event PenaltySentToTreasury(
  address indexed token,
  uint256 amount
);

Admin Functions

pause / unpause

Pause all staking operations in case of emergency:
function pause() external onlyOwner;
function unpause() external onlyOwner;

addSupportedToken / removeSupportedToken

Manage which tokens can be staked:
function addSupportedToken(address token) external onlyOwner;
function removeSupportedToken(address token) external onlyOwner;

setTokenAPY

Update APY rate for a specific token:
function setTokenAPY(address token, uint256 apy) external onlyOwner;

setLiquidityPool

Update the linked liquidity pool address:
function setLiquidityPool(address newPool) external onlyOwner;

updateTreasuryWallet

Change the treasury wallet that receives penalties:
function updateTreasuryWallet(address newTreasury) external onlyOwner;
Current Treasury: 0x39c0b97A8F2194fcd7396296F7697a84dd81077A

toggleEmergencyWithdrawal

Enable emergency withdrawals (bypasses time locks):
function toggleEmergencyWithdrawal(bool enabled) external onlyOwner;
Emergency withdrawal should only be enabled in critical situations as it allows users to bypass lock periods.

Security Features

Reentrancy Protection

All state-changing functions use OpenZeppelin’s ReentrancyGuard:
modifier nonReentrant();

Pausable

Contract can be paused to prevent operations:
modifier whenNotPaused();

Ownable

Admin functions restricted to owner:
modifier onlyOwner();

Safe Token Transfers

Uses OpenZeppelin’s SafeERC20 for secure transfers:
using SafeERC20 for IERC20;

Error Handling

Common errors:
ErrorCauseSolution
”Token not supported”Token not in whitelistUse BTC, SOL, or USDC
”Lock duration too short”Duration < 30 daysIncrease lock duration
”Lock duration too long”Duration > 365 daysDecrease lock duration
”Stake not found”Invalid stake IDCheck stake ID
”Already withdrawn”Stake already claimedCannot withdraw twice
”EnforcedPause”Contract pausedWait for unpause

Gas Optimization Tips

  • Batch approvals: Approve maximum amount once instead of per stake
  • Query off-chain: Use getUserStakes and getStake for read-only data
  • Cache stake IDs: Store stake IDs in frontend to avoid repeated queries

Liquidity Pool

Vault references pool for liquidity checks

Contract Overview

View all contract addresses

Build docs developers (and LLMs) love