SlvrLiquidityStaking

0x7d888f4ca88fc3578aefc45c82482bd66415dfea

Verification
Verified
v0.8.33+commit.64118f21
Type
Contract
9,394 bytes
ABI entries
66
33 read · 16 write
License
none

Contract information

Address
0x7d888f4ca88fc3578aefc45c82482bd66415dfea
Chain
Robinhood Chain (4663)
Compiler
v0.8.33+commit.64118f21
Optimization
Enabled
Creation tx
0x0a9bae5469…4321ae30f6

Token

Name
Symbol
Decimals
Holders
0

Read contract (33)

EARLY_WITHDRAWAL_FEE_BPS()uint256
EARLY_WITHDRAWAL_PERIOD()uint256
EPOCH_DURATION()uint256
LP_TOKEN()address
MAX_UNLOCK()uint256
RELEASE_PERIOD()uint256
TARGET_EPOCHS()uint256
availableRewards()uint256
depositForWhitelist(address)bool
getCurrentEpoch()uint256
getCurrentReleaseRate()uint256
getMONPerLPPerYearScaled()uint256
getPendingRewards(address)uint256
getPoolInfo()uint256, uint256, uint256, uint256, uint256, uint256
getRewardsPerYear()uint256
getUserEffectiveMultiplier(address)uint256
getUserMultiplierPoints(address)uint256
getUserPoints(address)uint256
getUserStaked(address)uint256
getUserUnclaimedPoints(address)uint256
growthFund()address
lastTimeRewardApplicable()uint256
lastUpdateTime()uint256
owner()address
pendingOwner()address
periodFinish()uint256
queuedRewards()uint256
rewardPerWeightStored()uint256
rewardRate()uint256
totalStaked()uint256
totalWeight()uint256
treasury()address
userInfo(address)uint256, uint256, uint256, uint64, uint64, uint256, uint256, uint256

Events (11)

DepositDepositForWhitelistUpdatedEmergencyRescueGrowthFundUpdatedOwnershipTransferStartedOwnershipTransferredRewardClaimedRewardDepositedRewardRateUpdatedTreasuryUpdatedWithdraw

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "lpToken_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "growthFund_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Deposit",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "whitelisted",
        "type": "bool"
      }
    ],
    "name": "DepositForWhitelistUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "EmergencyRescue",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldFund",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newFund",
        "type": "address"
      }
    ],
    "name": "GrowthFundUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "RewardClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "depositor",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "RewardDeposited",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newRate",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "periodFinish",
        "type": "uint256"
      }
    ],
    "name": "RewardRateUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldTreasury",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newTreasury",
        "type": "address"
      }
    ],
    "name": "TreasuryUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "Withdraw",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "EARLY_WITHDRAWAL_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "EARLY_WITHDRAWAL_PERIOD",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "EPOCH_DURATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LP_TOKEN",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_UNLOCK",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "RELEASE_PERIOD",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TARGET_EPOCHS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "accruePoints",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "availableRewards",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimPoints",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimRewards",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "claimRewardsTo",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "deposit",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "depositFor",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "depositForWhitelist",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "depositRewards",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "emergencyRescueERC20",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getCurrentEpoch",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getCurrentReleaseRate",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getMONPerLPPerYearScaled",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getPendingRewards",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getPoolInfo",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "rewardRate_",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "periodFinish_",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalWeight_",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "rewardsPerYear",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "ethPerLPPerYearScaled",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalStaked_",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getRewardsPerYear",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getUserEffectiveMultiplier",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getUserMultiplierPoints",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getUserPoints",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getUserStaked",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getUserUnclaimedPoints",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "growthFund",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lastTimeRewardApplicable",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lastUpdateTime",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "periodFinish",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "queuedRewards",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rewardPerWeightStored",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rewardRate",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "whitelisted",
        "type": "bool"
      }
    ],
    "name": "setDepositForWhitelist",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "growthFund_",
        "type": "address"
      }
    ],
    "name": "setGrowthFund",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      }
    ],
    "name": "setTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalStaked",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalWeight",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "userInfo",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "stake",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "points",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "unclaimedPoints",
        "type": "uint256"
      },
      {
        "internalType": "uint64",
        "name": "lastAccrualTime",
        "type": "uint64"
      },
      {
        "internalType": "uint64",
        "name": "lastDepositTime",
        "type": "uint64"
      },
      {
        "internalType": "uint256",
        "name": "userWeight",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "userRewardPerWeightPaid",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "rewards",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "skipRewards",
        "type": "bool"
      },
      {
        "internalType": "address",
        "name": "rewardRecipient",
        "type": "address"
      }
    ],
    "name": "withdraw",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "withdraw",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/// @title SlvrLiquidityStaking
/// @notice LP token staking with multiplier points and ETH rewards
/// @notice Rewards stream linearly over 5 days from each deposit
/// @notice Points accrue over time and increase reward share (multiplier: 1x to 2x)
/// @notice Points are absolute (stake-epochs), multiplier is relative to current stake
contract SlvrLiquidityStaking is ReentrancyGuard, Ownable2Step {
    using SafeERC20 for IERC20;
    
    IERC20 public immutable LP_TOKEN;

    address public growthFund; // Only growth fund can deposit rewards
    address public treasury; // Receives early withdrawal fees

    uint256 public constant EARLY_WITHDRAWAL_PERIOD = 6 hours;
    uint256 public constant EARLY_WITHDRAWAL_FEE_BPS = 200; // 2%
    uint256 public constant TARGET_EPOCHS = 7; // 7 days to reach max multiplier (for points system)
    uint256 public constant EPOCH_DURATION = 1 days; // 1 day per epoch
    uint256 public constant MAX_UNLOCK = 1e18; // 100% unlock (scaled by 1e18)
    uint256 public constant RELEASE_PERIOD = 5 days; // Rewards stream over 5 days from deposit

    // Total LP tokens staked
    uint256 public totalStaked;

    // Streaming rewards system (Synthetix-style)
    uint256 public rewardRate; // ETH per second (streaming rate)
    uint256 public periodFinish; // Timestamp when current reward stream ends
    uint256 public lastUpdateTime; // Last time rewardPerWeightStored was updated
    uint256 public rewardPerWeightStored; // Accumulated rewards per weight unit (scaled by 1e18)
    uint256 public totalWeight; // Sum of all user weights (stake * multiplier)
    uint256 public queuedRewards; // Rewards deposited when totalWeight == 0

    // User staking info
    struct UserInfo {
        uint256 stake; // Staked LP token amount
        uint256 points; // Absolute points (stake-epochs accumulated, claimed)
        uint256 unclaimedPoints; // Unclaimed points (stake-epochs not yet claimed)
        uint64 lastAccrualTime; // Last timestamp when points were checkpointed
        uint64 lastDepositTime; // Last time user deposited (for early withdrawal check)
        uint256 userWeight; // User's weight (stake * multiplier)
        uint256 userRewardPerWeightPaid; // Last rewardPerWeightStored when user was updated
        uint256 rewards; // Accrued but not yet claimed rewards (ETH)
    }

    mapping(address => UserInfo) public userInfo;
    
    // Whitelist for depositFor() to prevent griefing attacks
    mapping(address => bool) public depositForWhitelist;

    event Deposit(address indexed user, uint256 amount);
    event Withdraw(address indexed user, uint256 amount, uint256 fee);
    event RewardDeposited(address indexed depositor, uint256 amount);
    event RewardClaimed(address indexed user, uint256 amount);
    event GrowthFundUpdated(address indexed oldFund, address indexed newFund);
    event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury);
    event DepositForWhitelistUpdated(address indexed account, bool whitelisted);
    event EmergencyRescue(address indexed token, address indexed to, uint256 amount);
    event RewardRateUpdated(uint256 newRate, uint256 periodFinish);

    constructor(address lpToken_, address growthFund_, address treasury_, address owner_) Ownable(owner_) {
        require(lpToken_ != address(0), "lpToken=0");
        require(growthFund_ != address(0), "growthFund=0");
        require(treasury_ != address(0), "treasury=0");
        LP_TOKEN = IERC20(lpToken_);
        growthFund = growthFund_;
        treasury = treasury_;
        lastUpdateTime = block.timestamp;
        periodFinish = block.timestamp;
    }

    /// @notice Set growth fund address
    function setGrowthFund(address growthFund_) external onlyOwner {
        require(growthFund_ != address(0), "growthFund=0");
        address oldFund = growthFund;
        growthFund = growthFund_;
        emit GrowthFundUpdated(oldFund, growthFund_);
    }

    /// @notice Set treasury address (receives early withdrawal fees)
    function setTreasury(address treasury_) external onlyOwner {
        require(treasury_ != address(0), "treasury=0");
        address oldTreasury = treasury;
        treasury = treasury_;
        emit TreasuryUpdated(oldTreasury, treasury_);
    }

    /// @notice Add or remove an address from the depositFor whitelist
    /// @dev Only whitelisted addresses can call depositFor() to prevent griefing
    /// @param account Address to whitelist/unwhitelist
    /// @param whitelisted Whether to whitelist (true) or remove (false)
    function setDepositForWhitelist(address account, bool whitelisted) external onlyOwner {
        require(account != address(0), "account=0");
        depositForWhitelist[account] = whitelisted;
        emit DepositForWhitelistUpdated(account, whitelisted);
    }

    /// @notice Get current epoch (days since epoch 0)
    /// @dev Kept for backward compatibility, but not used for point accrual
    function getCurrentEpoch() public view returns (uint256) {
        return block.timestamp / EPOCH_DURATION;
    }

    /// @notice Checkpoint unclaimed points based on elapsed time
    /// @dev Points accrue into unclaimedPoints as stake-epochs based on actual time elapsed
    /// @dev This prevents gaming by staking just before epoch boundaries
    function _checkpointUnclaimedPoints(UserInfo storage u) internal {
        if (u.stake == 0) {
            u.lastAccrualTime = uint64(block.timestamp);
            return;
        }

        uint256 now_ = block.timestamp;
        uint256 lastAccrual = uint256(u.lastAccrualTime);
        
        if (now_ > lastAccrual) {
            uint256 delta = now_ - lastAccrual;
            // Accrue points: stake * delta / EPOCH_DURATION
            // This gives stake-epochs (stake amount in wei * epochs elapsed)
            // No need for 1e18 multiplier since stake is already in wei
            u.unclaimedPoints += (u.stake * delta) / EPOCH_DURATION;
            u.lastAccrualTime = uint64(now_);
        }
    }

    /// @notice Claim unclaimed points (move them to claimed points)
    /// @dev Claimed points drive progress calculation
    function _claimPoints(UserInfo storage u) internal {
        uint256 amt = u.unclaimedPoints;
        if (amt > 0) {
            u.points += amt;        // claimed points drive progress
            u.unclaimedPoints = 0;
        }
    }

    /// @notice Calculate user's progress toward max unlock (0 to 1e18)
    /// @dev progress = min(1, points / (stake * TARGET_EPOCHS) * 1e18)
    /// @dev Uses only claimed points (not unclaimed)
    /// @dev When stake increases, progress naturally dilutes (points stay constant)
    /// @dev Points are stored as stake-epochs: stake * time / EPOCH_DURATION (stake in wei)
    function _getProgress(UserInfo storage u) internal view returns (uint256) {
        if (u.stake == 0) return 0;

        // progress = (points / (stake * TARGET_EPOCHS)) * 1e18, capped at 1e18
        // Points are stored as stake-epochs: stake * time / EPOCH_DURATION
        // Target is: stake * TARGET_EPOCHS
        // Ratio gives epochs / TARGET_EPOCHS, then multiply by 1e18 for precision
        uint256 denom = u.stake * TARGET_EPOCHS;
        if (denom == 0) return 0;
        
        uint256 raw = (u.points * 1e18) / denom;
        return raw > MAX_UNLOCK ? MAX_UNLOCK : raw;
    }

    /// @notice Get user's effective multiplier (1e18 = 1x, 2e18 = 2x)
    /// @dev multiplier = 1x + progress * 1x (linear from 1x to 2x)
    function getUserEffectiveMultiplier(address user) external view returns (uint256) {
        UserInfo storage u = userInfo[user];
        uint256 progress = _getProgress(u);
        // multiplier = 1e18 + progress (gives 1x to 2x)
        return 1e18 + progress;
    }

    /// @notice Get user's claimed points (absolute stake-epochs)
    function getUserPoints(address user) external view returns (uint256) {
        return userInfo[user].points;
    }

    /// @notice Get user's unclaimed points
    function getUserUnclaimedPoints(address user) external view returns (uint256) {
        UserInfo memory u = userInfo[user];
        if (u.stake == 0) return u.unclaimedPoints;

        // Calculate unclaimed points including time since last checkpoint
        uint256 now_ = block.timestamp;
        uint256 lastAccrual = uint256(u.lastAccrualTime);
        if (now_ <= lastAccrual) return u.unclaimedPoints;

        uint256 delta = now_ - lastAccrual;
        // Accrue points: stake * delta / EPOCH_DURATION
        // This gives stake-epochs (stake amount in wei * epochs elapsed)
        return u.unclaimedPoints + ((u.stake * delta) / EPOCH_DURATION);
    }

    /// @notice Get the last time rewards were applicable (capped at periodFinish)
    function lastTimeRewardApplicable() public view returns (uint256) {
        return block.timestamp < periodFinish ? block.timestamp : periodFinish;
    }

    /// @notice Update reward tracking for a user (or globally if account is address(0))
    /// @dev Accrues rewards based on time elapsed and current rewardRate
    function _updateReward(address account) internal {
        uint256 t = lastTimeRewardApplicable();
        if (totalWeight > 0) {
            // CRITICAL: only ever advance lastUpdateTime forward. `t` is min(now, periodFinish), so
            // when periodFinish is in the past (stream ended / not yet (re)started) it would otherwise
            // snap lastUpdateTime backwards, and the next accrual would credit the entire elapsed gap
            // at the full rewardRate — over-distributing far more than was deposited and potentially
            // rendering the contract insolvent so claims revert. Guarding on t > lastUpdateTime keeps
            // accrual bounded to the live streaming window.
            if (t > lastUpdateTime) {
                rewardPerWeightStored += ((t - lastUpdateTime) * rewardRate * 1e18) / totalWeight;
                lastUpdateTime = t;
            }
        }
        // When totalWeight == 0, don't update lastUpdateTime to prevent stale periodFinish values
        // It will be reset properly when the first staker deposits and processes queued rewards

        if (account != address(0)) {
            UserInfo storage u = userInfo[account];
            if (u.userWeight > 0) {
                uint256 earned = (u.userWeight * (rewardPerWeightStored - u.userRewardPerWeightPaid)) / 1e18;
                u.rewards += earned;
            }
            u.userRewardPerWeightPaid = rewardPerWeightStored;
        }
    }

    /// @notice Recompute user's weight based on stake and multiplier
    /// @dev Weight = stake * multiplier, where multiplier = 1e18 + progress (1x to 2x)
    /// @dev Uses defensive math to prevent DoS if invariants drift
    function _recomputeWeight(UserInfo storage u) internal {
        uint256 oldWeight = u.userWeight;
        
        if (u.stake == 0) {
            // Defensive: prevent underflow if oldWeight > totalWeight
            if (totalWeight >= oldWeight) {
                totalWeight -= oldWeight;
            } else {
                totalWeight = 0;
            }
            u.userWeight = 0;
            return;
        }

        uint256 progress = _getProgress(u);
        uint256 multiplier = 1e18 + progress; // 1x to 2x
        uint256 newWeight = (u.stake * multiplier) / 1e18;

        // Defensive: prevent underflow if oldWeight > totalWeight (rounding drift, bugs, etc.)
        if (totalWeight >= oldWeight) {
            totalWeight -= oldWeight;
        } else {
            totalWeight = 0;
        }
        totalWeight += newWeight;
        u.userWeight = newWeight;
    }


    /// @notice Deposit LP tokens to stake
    /// @param amount Amount of LP tokens to deposit
    function deposit(uint256 amount) external nonReentrant {
        _depositFor(msg.sender, amount);
    }

    /// @notice Deposit LP tokens to stake on behalf of another user
    /// @dev Only whitelisted addresses can call this to prevent griefing attacks
    /// @param user Address to credit the stake to
    /// @param amount Amount of LP tokens to deposit
    function depositFor(address user, uint256 amount) external nonReentrant {
        require(user != address(0), "user=0");
        require(depositForWhitelist[msg.sender] || msg.sender == user, "not whitelisted");
        _depositFor(user, amount);
    }

    /// @notice Internal function to handle deposit logic
    /// @param user Address to credit the stake to
    /// @param amount Amount of LP tokens to deposit
    function _depositFor(address user, uint256 amount) internal {
        require(amount > 0, "amount=0");

        UserInfo storage u = userInfo[user];
        bool wasFirstStaker = totalWeight == 0;
        bool isNewUser = u.userWeight == 0;

        _updateReward(address(0));

        if (!isNewUser) {
            _updateReward(user);
        }

        _checkpointUnclaimedPoints(u);

        uint256 before = LP_TOKEN.balanceOf(address(this));
        LP_TOKEN.safeTransferFrom(msg.sender, address(this), amount);
        uint256 after_ = LP_TOKEN.balanceOf(address(this));
        uint256 received = after_ - before;
        require(received > 0, "no tokens received");

        u.stake += received;
        if (u.lastDepositTime == 0) {
            u.lastDepositTime = uint64(block.timestamp);
        } else if (msg.sender == user || depositForWhitelist[msg.sender]) {
            u.lastDepositTime = uint64(block.timestamp);
        }
        totalStaked += received;

        _recomputeWeight(u);

        u.userRewardPerWeightPaid = rewardPerWeightStored;

        if (wasFirstStaker && totalWeight > 0) {
            // Pool is transitioning empty -> non-empty. Before resetting the stream, recover the
            // un-streamed remainder [lastUpdateTime, periodFinish] * rewardRate. While the pool sat
            // empty, rewardPerWeightStored could not advance (the totalWeight==0 guard in
            // _updateReward), so that scheduled ETH — which is already sitting in the contract — was
            // never credited to anyone. lastUpdateTime is pinned at the moment the pool emptied, so
            // this single fold captures both the empty gap [emptied, now] AND the still-active tail
            // [now, periodFinish]; folding only the tail (as before) permanently stranded the gap,
            // and native rewards cannot be rescued.
            if (periodFinish > lastUpdateTime) {
                queuedRewards += (periodFinish - lastUpdateTime) * rewardRate;
            }

            // Reset accrual to start NOW so this staker cannot retroactively harvest the empty
            // interval (an unbounded, stake-independent reward capture). Must run even when there
            // were no pre-existing queued rewards, e.g. a mid-stream pool emptying and refilling.
            lastUpdateTime = block.timestamp;
            _updateReward(address(0));

            if (queuedRewards > 0) {
                uint256 total = queuedRewards;
                queuedRewards = 0;

                // Start a new stream with queued rewards (recovered remainder + anything queued
                // while empty). Carry the sub-period dust remainder forward.
                uint256 rate = total / RELEASE_PERIOD;
                uint256 remainder = total - (rate * RELEASE_PERIOD);

                rewardRate = rate;
                queuedRewards += remainder; // Carry remainder forward
                periodFinish = block.timestamp + RELEASE_PERIOD;
                emit RewardRateUpdated(rewardRate, periodFinish);
            }
        }

        emit Deposit(user, received);
    }

    /// @notice Internal function to handle withdraw logic
    /// @param user Address of the user withdrawing
    /// @param amount Amount of LP tokens to withdraw
    /// @param skipRewards If true, skip claiming rewards
    /// @param rewardRecipient Address to receive rewards (if skipRewards is false). If address(0), uses user
    function _withdrawInternal(address user, uint256 amount, bool skipRewards, address rewardRecipient) internal {
        require(amount > 0, "amount=0");
        require(!skipRewards || rewardRecipient == address(0), "skipRewards requires rewardRecipient=0");
        UserInfo storage u = userInfo[user];
        require(u.stake >= amount, "insufficient staked");

        _updateReward(address(0));
        _updateReward(user);

        _checkpointUnclaimedPoints(u);
        _claimPoints(u);

        uint256 rewardAmount = u.rewards;
        if (rewardAmount > 0) {
            if (!skipRewards) {
                u.rewards = 0;
                address recipient = rewardRecipient != address(0) ? rewardRecipient : user;
                (bool ok,) = payable(recipient).call{value: rewardAmount}("");
                require(ok, "reward xfer failed");
                emit RewardClaimed(user, rewardAmount);
            } else {
                require(rewardAmount == 0, "claim first");
            }
        }

        uint256 fee = 0;
        if (block.timestamp < u.lastDepositTime + EARLY_WITHDRAWAL_PERIOD) {
            fee = (amount * EARLY_WITHDRAWAL_FEE_BPS) / 10000;
        }

        u.stake -= amount;
        totalStaked -= amount;

        u.points = 0;
        u.unclaimedPoints = 0;
        u.lastAccrualTime = uint64(block.timestamp);

        _recomputeWeight(u);

        u.userRewardPerWeightPaid = rewardPerWeightStored;

        // Transfer LP tokens back to user (minus fee)
        uint256 withdrawAmount = amount - fee;
        LP_TOKEN.safeTransfer(user, withdrawAmount);

        // Send fee to treasury (or burn if preferred)
        if (fee > 0) {
            LP_TOKEN.safeTransfer(treasury, fee);
        }

        emit Withdraw(user, amount, fee);
    }

    /// @notice Withdraw LP tokens from staking
    /// @dev IMPORTANT: Claims all claimable rewards before resetting points and forfeiting remaining locked rewards
    /// @dev This ensures users don't lose their pending rewards when withdrawing
    /// @param amount Amount of LP tokens to withdraw
    function withdraw(uint256 amount) external nonReentrant {
        _withdrawInternal(msg.sender, amount, false, address(0));
    }

    /// @notice Withdraw LP tokens from staking with options
    /// @dev IMPORTANT: Claims all claimable rewards before resetting points and forfeiting remaining locked rewards
    /// @dev This ensures users don't lose their pending rewards when withdrawing
    /// @param amount Amount of LP tokens to withdraw
    /// @param skipRewards If true, skip claiming rewards (useful for contract wallets that can't receive native)
    /// @param rewardRecipient Address to receive rewards (if skipRewards is false). If address(0), uses msg.sender
    function withdraw(uint256 amount, bool skipRewards, address rewardRecipient) external nonReentrant {
        _withdrawInternal(msg.sender, amount, skipRewards, rewardRecipient);
    }

    /// @notice Deposit rewards (called by growth fund)
    /// @dev Rewards stream linearly over 5 days from deposit time
    /// @dev Each deposit creates a new 5-day stream (rolls leftover + new into new period)
    /// @dev If totalWeight == 0, rewards are queued until someone stakes
    function depositRewards() external payable nonReentrant {
        require(msg.sender == growthFund, "only growth fund");
        uint256 amount = msg.value;
        require(amount > 0, "amount=0");

        if (totalWeight == 0) {
            queuedRewards += amount;
            emit RewardDeposited(msg.sender, amount);
            return;
        }

        bool isFirstRewardDeposit = (rewardRate == 0);
        
        if (isFirstRewardDeposit) {
            lastUpdateTime = block.timestamp;
        }
        
        _updateReward(address(0));

        uint256 total = amount + queuedRewards;
        queuedRewards = 0;

        // Calculate new reward rate: roll leftover + new into 5-day stream
        uint256 numerator;
        if (block.timestamp >= periodFinish) {
            // No active stream, start fresh
            numerator = total;
        } else {
            // Active stream exists, roll leftover + new into new 5-day period
            uint256 remaining = periodFinish - block.timestamp;
            uint256 leftover = remaining * rewardRate;
            numerator = total + leftover;
        }

        // Calculate rate and remainder to prevent dust accumulation
        uint256 rate = numerator / RELEASE_PERIOD;
        uint256 remainder = numerator - (rate * RELEASE_PERIOD);
        
        rewardRate = rate;
        queuedRewards += remainder;

        // Anchor accrual to now: if the previous stream had already ended, lastUpdateTime is stuck at
        // the old (past) periodFinish, and without this the next _updateReward would credit the whole
        // gap between the old finish and now at the new rate. The preceding _updateReward already
        // settled accrual under the old rate, so it is safe to move the anchor forward here.
        lastUpdateTime = block.timestamp;
        periodFinish = block.timestamp + RELEASE_PERIOD;

        emit RewardRateUpdated(rewardRate, periodFinish);
        emit RewardDeposited(msg.sender, amount);
    }

    /// @notice Claim points (move unclaimed to claimed)
    function claimPoints() external nonReentrant {
        UserInfo storage u = userInfo[msg.sender];
        if (u.stake == 0) return;

        _updateReward(address(0));
        _updateReward(msg.sender);

        _checkpointUnclaimedPoints(u);
        _claimPoints(u);

        _recomputeWeight(u);

        u.userRewardPerWeightPaid = rewardPerWeightStored;
    }

    /// @notice Accrue unclaimed points for any user (anyone can call)
    /// @dev Updates unclaimed points to current epoch for the specified user
    /// @dev This allows keepers or other users to help update points for others
    /// @param user Address of the user to accrue points for
    function accruePoints(address user) external nonReentrant {
        UserInfo storage u = userInfo[user];
        if (u.stake > 0) {
            _checkpointUnclaimedPoints(u);
        }
    }

    /// @notice Internal function to claim rewards to a specified address
    /// @param user Address of the user claiming rewards
    /// @param to Address to receive the rewards
    function _claimRewardsToInternal(address user, address to) internal {
        require(to != address(0), "to=0");
        UserInfo storage u = userInfo[user];
        require(u.stake > 0, "not staking");

        _updateReward(address(0));
        _updateReward(user);

        _checkpointUnclaimedPoints(u);
        _claimPoints(u);
        
        _recomputeWeight(u);

        u.userRewardPerWeightPaid = rewardPerWeightStored;

        uint256 rewardAmount = u.rewards;
        if (rewardAmount > 0) {
            u.rewards = 0;
            (bool success,) = payable(to).call{value: rewardAmount}("");
            require(success, "transfer failed");
            emit RewardClaimed(user, rewardAmount);
        }
    }

    /// @notice Claim pending ETH rewards
    /// @dev Auto-claims points before calculating claimable rewards
    function claimRewards() external nonReentrant {
        _claimRewardsToInternal(msg.sender, msg.sender);
    }

    /// @notice Claim pending ETH rewards to a specified address
    /// @dev Auto-claims points before calculating claimable rewards
    /// @dev Allows contract wallets to specify a recipient that can receive native tokens
    /// @param to Address to receive the rewards
    function claimRewardsTo(address to) external nonReentrant {
        _claimRewardsToInternal(msg.sender, to);
    }

    /// @notice Get pending rewards for a user (view function)
    /// @param user Address of the user
    function getPendingRewards(address user) external view returns (uint256) {
        UserInfo memory u = userInfo[user];
        
        if (u.stake == 0) {
            return u.rewards;
        }

        // Calculate user's current weight (stake * multiplier)
        // This ensures we calculate pending rewards even if weight hasn't been checkpointed yet
        uint256 currentWeight = u.userWeight;
        if (currentWeight == 0) {
            // Weight not yet computed, calculate it now (for view purposes)
            // Calculate progress inline (same logic as _getProgress but for memory struct)
            uint256 progress = 0;
            if (u.stake > 0) {
                uint256 denom = u.stake * TARGET_EPOCHS;
                if (denom > 0) {
                    uint256 raw = (u.points * 1e18) / denom;
                    progress = raw > MAX_UNLOCK ? MAX_UNLOCK : raw;
                }
            }
            uint256 multiplier = 1e18 + progress; // 1x to 2x
            currentWeight = (u.stake * multiplier) / 1e18;
        }

        // Calculate current rewardPerWeightStored (view-only)
        uint256 t = lastTimeRewardApplicable();
        uint256 currentRewardPerWeightStored = rewardPerWeightStored;
        
        if (totalWeight > 0) {
            uint256 timeElapsed = t > lastUpdateTime ? t - lastUpdateTime : 0;
            if (timeElapsed > 0) {
                currentRewardPerWeightStored += (timeElapsed * rewardRate * 1e18) / totalWeight;
            }
        }

        // Calculate pending rewards: already accrued + new since last update
        // Use currentWeight (computed above) instead of u.userWeight
        uint256 pending = (currentWeight * (currentRewardPerWeightStored - u.userRewardPerWeightPaid)) / 1e18;
        
        return u.rewards + pending;
    }

    /// @notice Get user's staked LP token amount
    /// @param user Address of the user
    function getUserStaked(address user) external view returns (uint256) {
        return userInfo[user].stake;
    }

    /// @notice Get user's multiplier points (for compatibility, returns claimed points)
    /// @param user Address of the user
    function getUserMultiplierPoints(address user) external view returns (uint256) {
        return userInfo[user].points;
    }


    /// @notice Get available rewards balance in the contract
    /// @dev Returns the native token (ETH) balance of the contract
    /// @dev This can be compared against total claimable rewards to verify accounting invariants
    /// @return The contract's native token balance
    function availableRewards() external view returns (uint256) {
        return address(this).balance;
    }

    /// @notice Get current reward release rate
    /// @dev Returns the current streaming rate per second
    /// @return Current reward rate per second (ETH per second)
    function getCurrentReleaseRate() external view returns (uint256) {
        return rewardRate;
    }

    /// @notice Get estimated rewards per year based on current release rate
    /// @dev Calculates: rewardRate * seconds per year
    /// @return Estimated ETH rewards per year (in wei)
    function getRewardsPerYear() external view returns (uint256) {
        if (rewardRate == 0) return 0;
        uint256 secondsPerYear = 365 days;
        return rewardRate * secondsPerYear;
    }

    /// @notice Get ETH per LP per year (scaled by 10000)
    /// @dev Returns (rewards per year / total staked) * 10000
    /// @dev WARNING: This is NOT a true APR percentage. It divides ETH (rewards) by LP tokens (stake),
    /// @dev which are different units. To get true APR, multiply by ETH/LP price ratio in the UI.
    /// @dev Returns 0 if no stakers or no rewards
    /// @return ETH per LP per year scaled by 10000 (e.g., 500 = 0.05 ETH per LP per year)
    function getMONPerLPPerYearScaled() external view returns (uint256) {
        if (totalStaked == 0) return 0;
        uint256 rewardsPerYear = this.getRewardsPerYear();
        if (rewardsPerYear == 0) return 0;
        // ETH per LP per year (scaled by 10000) = (rewardsPerYear / totalStaked) * 10000
        return (rewardsPerYear * 10000) / totalStaked;
    }

    /// @notice Get comprehensive pool information
    /// @return rewardRate_ Current reward rate per second (ETH per second)
    /// @return periodFinish_ Timestamp when current reward stream ends
    /// @return totalWeight_ Total weight (sum of all user weights)
    /// @return rewardsPerYear Estimated rewards per year (ETH)
    /// @return ethPerLPPerYearScaled ETH per LP per year scaled by 10000 (NOT true APR - requires price conversion)
    /// @return totalStaked_ Total LP tokens staked
    function getPoolInfo() external view returns (
        uint256 rewardRate_,
        uint256 periodFinish_,
        uint256 totalWeight_,
        uint256 rewardsPerYear,
        uint256 ethPerLPPerYearScaled,
        uint256 totalStaked_
    ) {
        rewardRate_ = rewardRate;
        periodFinish_ = periodFinish;
        totalWeight_ = totalWeight;
        rewardsPerYear = this.getRewardsPerYear();
        ethPerLPPerYearScaled = this.getMONPerLPPerYearScaled();
        totalStaked_ = totalStaked;
    }

    /// @notice Receive native tokens (ETH) for rewards
    /// @dev Reverts to prevent funds from being sent without updating reward rate
    receive() external payable {
        revert("use depositRewards");
    }

    /// @notice Emergency rescue function for unrelated ERC20 tokens
    /// @dev Only owner can call. Cannot rescue LP_TOKEN (staked by users) or native tokens
    /// @param token Address of the ERC20 token to rescue
    /// @param to Address to send the rescued tokens to
    /// @param amount Amount of tokens to rescue (0 = full balance)
    function emergencyRescueERC20(address token, address to, uint256 amount) external onlyOwner {
        require(token != address(0), "token=0");
        require(to != address(0), "to=0");
        require(token != address(LP_TOKEN), "cannot rescue LP_TOKEN");
        
        IERC20 tokenContract = IERC20(token);
        uint256 balance = tokenContract.balanceOf(address(this));
        uint256 rescueAmount = amount == 0 ? balance : amount;
        require(rescueAmount > 0, "amount=0");
        require(rescueAmount <= balance, "insufficient balance");
        
        tokenContract.safeTransfer(to, rescueAmount);
        emit EmergencyRescue(token, to, rescueAmount);
    }
}
Chain explorer2628msChain node80ms