Contract Source Code:
File 1 of 1 : LPStaking
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// IERC20 Interface
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// Ownable Contract
abstract contract Ownable {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor() {
_transferOwnership(msg.sender);
}
function owner() public view virtual returns (address) {
return _owner;
}
modifier onlyOwner() {
require(owner() == msg.sender, "Ownable: caller is not the owner");
_;
}
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
_transferOwnership(newOwner);
}
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
// ReentrancyGuard Contract
abstract contract ReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
modifier nonReentrant() {
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
}
// Pausable Contract
abstract contract Pausable {
event Paused(address account);
event Unpaused(address account);
bool private _paused;
constructor() {
_paused = false;
}
function paused() public view virtual returns (bool) {
return _paused;
}
modifier whenNotPaused() {
require(!paused(), "Pausable: paused");
_;
}
modifier whenPaused() {
require(paused(), "Pausable: not paused");
_;
}
function _pause() internal virtual whenNotPaused {
_paused = true;
emit Paused(msg.sender);
}
function _unpause() internal virtual whenPaused {
_paused = false;
emit Unpaused(msg.sender);
}
}
/**
* @title LPStaking Contract
* @notice This contract handles staking of LP tokens with dual rewards and anti-abuse features
*/
contract LPStaking is Ownable, ReentrancyGuard, Pausable {
// ============ Events ============
event Staked(address indexed user, uint256 amount);
event Unstaked(address indexed user, uint256 amount);
event RewardsClaimed(address indexed user, uint256 amount, uint256 secondaryAmount);
event MinimumStakeAmountUpdated(uint256 oldAmount, uint256 newAmount);
event LockPeriodUpdated(uint256 oldPeriod, uint256 newPeriod);
event DynamicRewardRateUpdated(uint256 oldRate, uint256 newRate);
event BonusMultiplierUpdated(uint256 oldMultiplier, uint256 newMultiplier);
event BlacklistStatusUpdated(address indexed user, bool status);
event EmergencyWithdraw(address indexed user, uint256 amount);
event TimeTierUpdated(uint256 tierId, uint256 oldPeriod, uint256 newPeriod);
event MaxRewardRateUpdated(uint256 oldRate, uint256 newRate);
event MaxBonusMultiplierUpdated(uint256 oldMultiplier, uint256 newMultiplier);
event TierThresholdUpdated(uint256 tierId, uint256 oldAmount, uint256 newAmount);
// ============ Constants ============
uint256 public MAX_REWARD_RATE = 10000; // 100%
uint256 public MAX_BONUS_MULTIPLIER = 1000; // 10x
uint256 public TIER1_THRESHOLD = 1000e18; // 1,000 tokens
uint256 public TIER2_THRESHOLD = 10000e18; // 10,000 tokens
uint256 public TIER3_THRESHOLD = 100000e18; // 100,000 tokens
uint256 public TIME_TIER1 = 7 days; // 1 week
uint256 public TIME_TIER2 = 14 days; // 2 weeks
uint256 public TIME_TIER3 = 30 days; // 1 month
// ============ Token Configuration ============
IERC20 public dexLPToken;
IERC20 public HACCoinReward;
// ============ Reward Configuration ============
uint256 public baseRewardRate; // Base reward rate
uint256 public dynamicRewardRate; // Additional dynamic reward during high-traffic periods
uint256 public bonusMultiplier; // Bonus multiplier for early or large contributions
// ============ Staking Configuration ============
uint256 public minimumStakeAmount;
uint256 public lockPeriod;
// ============ User Data ============
mapping(address => uint256) public stakedAmount;
mapping(address => uint256) public stakingStartTime;
mapping(address => uint256) public lastClaimTime;
mapping(address => bool) public blacklisted;
// ============ Constructor ============
constructor(
address _dexLPTokenAddress, // <-- dex LP token address
address _HACCoinContractAddress, // <-- HACCoin token address
uint256 _baseRewardRate,
uint256 _bonusMultiplier,
uint256 _minimumStakeAmount,
uint256 _lockPeriod
) {
require(_dexLPTokenAddress != address(0), "Zero address not allowed");
require(_HACCoinContractAddress != address(0), "Zero address not allowed");
require(_baseRewardRate <= MAX_REWARD_RATE, "Rate too high");
dexLPToken = IERC20(_dexLPTokenAddress);
HACCoinReward = IERC20(_HACCoinContractAddress);
baseRewardRate = _baseRewardRate;
bonusMultiplier = _bonusMultiplier;
minimumStakeAmount = _minimumStakeAmount;
lockPeriod = _lockPeriod;
}
// ============ Modifiers ============
modifier notBlacklisted() {
require(!blacklisted[msg.sender], "Account is blacklisted");
_;
}
// ============ Core Staking Functions ============
function stake(uint256 amount) external nonReentrant whenNotPaused notBlacklisted {
require(amount >= minimumStakeAmount, "Below minimum stake amount");
require(amount > 0, "Cannot stake 0");
// Transfer LP tokens to this contract
dexLPToken.transferFrom(msg.sender, address(this), amount);
// If they already have staked tokens, update their rewards checkpoint
if (stakedAmount[msg.sender] > 0) {
lastClaimTime[msg.sender] = block.timestamp;
} else {
// For first-time stakers, set initial claim time
lastClaimTime[msg.sender] = block.timestamp;
}
// Update staking amount and time
stakedAmount[msg.sender] += amount;
stakingStartTime[msg.sender] = block.timestamp;
emit Staked(msg.sender, amount);
}
function unstake(uint256 amount) external nonReentrant whenNotPaused notBlacklisted {
require(block.timestamp >= stakingStartTime[msg.sender] + lockPeriod, "Still in lock period");
require(stakedAmount[msg.sender] >= amount, "Insufficient staked balance");
// Calculate and distribute pending rewards before unstaking
uint256 pendingReward = calculateReward(msg.sender);
if (pendingReward > 0) {
distributeRewards(msg.sender, pendingReward);
lastClaimTime[msg.sender] = block.timestamp;
}
// Update staking amount and transfer tokens back
stakedAmount[msg.sender] -= amount;
require(dexLPToken.transfer(msg.sender, amount), "Transfer failed");
emit Unstaked(msg.sender, amount);
}
function claimRewards() external nonReentrant whenNotPaused notBlacklisted {
uint256 pendingReward = calculateReward(msg.sender);
require(pendingReward > 0, "No rewards to claim");
// Update last claim time before distribution
lastClaimTime[msg.sender] = block.timestamp;
// Calculate and distribute rewards
uint256 secondaryReward = pendingReward / 10;
distributeRewards(msg.sender, pendingReward);
emit RewardsClaimed(msg.sender, pendingReward, secondaryReward);
}
// ============ View Functions ============
function calculateReward(address user) public view returns (uint256) {
if (stakedAmount[user] == 0) return 0;
uint256 duration = block.timestamp - lastClaimTime[user];
uint256 dynamicRate = baseRewardRate + dynamicRewardRate;
uint256 amountMultiplier = getAmountMultiplier(stakedAmount[user]);
uint256 totalStakingDuration = block.timestamp - stakingStartTime[user];
uint256 timeMultiplier = getTimeMultiplier(totalStakingDuration);
uint256 reward = (stakedAmount[user] * dynamicRate * duration) / 1e18;
reward = reward * amountMultiplier * timeMultiplier / 10000;
reward = reward * bonusMultiplier / 100;
return reward;
}
function getTotalStaked() external view returns (uint256) {
return dexLPToken.balanceOf(address(this));
}
function getUserStakeInfo(address user) external view returns (
uint256 staked,
uint256 startTime,
uint256 pendingRewards,
bool isBlacklisted
) {
return (
stakedAmount[user],
stakingStartTime[user],
calculateReward(user),
blacklisted[user]
);
}
function getLPTokenAllowance(address user) external view returns (uint256) {
return dexLPToken.allowance(user, address(this));
}
// ============ Internal Functions ============
function distributeRewards(address user, uint256 rewardAmount) public {
require(HACCoinReward.balanceOf(address(this)) >= rewardAmount, "Insufficient rewards");
uint256 secondaryReward = rewardAmount / 10;
require(address(this).balance >= secondaryReward, "Insufficient secondary rewards");
require(HACCoinReward.transfer(user, rewardAmount), "Reward transfer failed");
(bool success, ) = payable(user).call{value: secondaryReward}("");
require(success, "Native token transfer failed");
}
// ============ Admin Functions ============
// Pause/Unpause
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
// Rate Management
function setDynamicRewardRate(uint256 rate) external onlyOwner {
require(rate <= MAX_REWARD_RATE, "Rate too high");
uint256 oldRate = dynamicRewardRate;
dynamicRewardRate = rate;
emit DynamicRewardRateUpdated(oldRate, rate);
}
function setBonusMultiplier(uint256 multiplier) external onlyOwner {
require(multiplier > 0 && multiplier <= MAX_BONUS_MULTIPLIER, "Invalid multiplier");
uint256 oldMultiplier = bonusMultiplier;
bonusMultiplier = multiplier;
emit BonusMultiplierUpdated(oldMultiplier, multiplier);
}
// Configuration Management
function setMinimumStakeAmount(uint256 amount) external onlyOwner {
uint256 oldAmount = minimumStakeAmount;
minimumStakeAmount = amount;
emit MinimumStakeAmountUpdated(oldAmount, amount);
}
function setLockPeriod(uint256 period) external onlyOwner {
uint256 oldPeriod = lockPeriod;
lockPeriod = period;
emit LockPeriodUpdated(oldPeriod, period);
}
// Access Control
function setBlacklist(address user, bool status) external onlyOwner {
require(user != address(0), "Invalid address");
blacklisted[user] = status;
emit BlacklistStatusUpdated(user, status);
}
function batchSetBlacklist(address[] calldata users, bool status) external onlyOwner {
for(uint256 i = 0; i < users.length; i++) {
require(users[i] != address(0), "Invalid address");
blacklisted[users[i]] = status;
emit BlacklistStatusUpdated(users[i], status);
}
}
// ============ Emergency Functions ============
function emergencyWithdraw() external nonReentrant {
uint256 amount = stakedAmount[msg.sender];
require(amount > 0, "No staked tokens");
stakedAmount[msg.sender] = 0;
stakingStartTime[msg.sender] = 0;
dexLPToken.transfer(msg.sender, amount);
emit EmergencyWithdraw(msg.sender, amount);
}
function emergencyRewardWithdraw() external onlyOwner {
uint256 rewardBalance = HACCoinReward.balanceOf(address(this));
uint256 nativeBalance = address(this).balance;
if (rewardBalance > 0) {
HACCoinReward.transfer(owner(), rewardBalance);
}
if (nativeBalance > 0) {
(bool success, ) = payable(owner()).call{value: nativeBalance}("");
require(success, "Native token transfer failed");
}
}
function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyOwner {
require(tokenAddress != address(dexLPToken), "Cannot recover staking token");
IERC20(tokenAddress).transfer(owner(), tokenAmount);
}
function getRewardBalances() external view returns (uint256 newTokenBalance, uint256 nativeBalance) {
return (
HACCoinReward.balanceOf(address(this)),
address(this).balance
);
}
// ============ Helper Functions ============
function getAmountMultiplier(uint256 amount) public view returns (uint256) {
if (amount >= TIER3_THRESHOLD) {
return 150; // 1.5x multiplier
} else if (amount >= TIER2_THRESHOLD) {
return 125; // 1.25x multiplier
} else if (amount >= TIER1_THRESHOLD) {
return 110; // 1.1x multiplier
}
return 100; // 1x multiplier (base)
}
function getTimeMultiplier(uint256 duration) public view returns (uint256) {
if (duration >= TIME_TIER3) {
return 150; // 1.5x multiplier
} else if (duration >= TIME_TIER2) {
return 125; // 1.25x multiplier
} else if (duration >= TIME_TIER1) {
return 110; // 1.1x multiplier
}
return 100; // 1x multiplier (base)
}
function getUserMultipliers(address user) external view returns (
uint256 amountMultiplier,
uint256 timeMultiplier,
uint256 totalMultiplier
) {
uint256 duration = block.timestamp - stakingStartTime[user];
amountMultiplier = getAmountMultiplier(stakedAmount[user]);
timeMultiplier = getTimeMultiplier(duration);
totalMultiplier = (amountMultiplier * timeMultiplier * bonusMultiplier) / 1000; // Final multiplier in basis points
return (amountMultiplier, timeMultiplier, totalMultiplier);
}
// ============ Time Tier Functions ============
function setTimeTier1(uint256 newPeriod) external onlyOwner {
require(newPeriod < TIME_TIER2, "TIME_TIER1 must be less than TIME_TIER2");
require(newPeriod > 0, "Period cannot be zero");
uint256 oldPeriod = TIME_TIER1;
TIME_TIER1 = newPeriod;
emit TimeTierUpdated(1, oldPeriod, newPeriod);
}
function setTimeTier2(uint256 newPeriod) external onlyOwner {
require(newPeriod > TIME_TIER1, "TIME_TIER2 must be greater than TIME_TIER1");
require(newPeriod < TIME_TIER3, "TIME_TIER2 must be less than TIME_TIER3");
uint256 oldPeriod = TIME_TIER2;
TIME_TIER2 = newPeriod;
emit TimeTierUpdated(2, oldPeriod, newPeriod);
}
function setTimeTier3(uint256 newPeriod) external onlyOwner {
require(newPeriod > TIME_TIER2, "TIME_TIER3 must be greater than TIME_TIER2");
uint256 oldPeriod = TIME_TIER3;
TIME_TIER3 = newPeriod;
emit TimeTierUpdated(3, oldPeriod, newPeriod);
}
function getTimeTiers() external view returns (uint256, uint256, uint256) {
return (TIME_TIER1, TIME_TIER2, TIME_TIER3);
}
// ============ Setter Functions ============
function setMaxRewardRate(uint256 newRate) external onlyOwner {
uint256 oldRate = MAX_REWARD_RATE;
MAX_REWARD_RATE = newRate;
emit MaxRewardRateUpdated(oldRate, newRate);
}
function setMaxBonusMultiplier(uint256 newMultiplier) external onlyOwner {
uint256 oldMultiplier = MAX_BONUS_MULTIPLIER;
MAX_BONUS_MULTIPLIER = newMultiplier;
emit MaxBonusMultiplierUpdated(oldMultiplier, newMultiplier);
}
function setTierThreshold(uint256 tierId, uint256 newAmount) external onlyOwner {
require(tierId >= 1 && tierId <= 3, "Invalid tier");
uint256 oldAmount;
if (tierId == 1) {
oldAmount = TIER1_THRESHOLD;
require(newAmount < TIER2_THRESHOLD, "Must be less than TIER2");
TIER1_THRESHOLD = newAmount;
} else if (tierId == 2) {
oldAmount = TIER2_THRESHOLD;
require(newAmount > TIER1_THRESHOLD && newAmount < TIER3_THRESHOLD, "Must be between TIER1 and TIER3");
TIER2_THRESHOLD = newAmount;
} else {
oldAmount = TIER3_THRESHOLD;
require(newAmount > TIER2_THRESHOLD, "Must be greater than TIER2");
TIER3_THRESHOLD = newAmount;
}
emit TierThresholdUpdated(tierId, oldAmount, newAmount);
}
struct RewardBreakdown {
uint256 accumulatedRewards; // Total pending rewards
uint256 rewardsPerDay; // Current rate of HACCoin rewards per day
uint256 secondaryRewards; // Accumulated TOPIA rewards
uint256 timeMultiplier; // Current time multiplier (e.g., 110 = 1.1x)
uint256 amountMultiplier; // Current amount multiplier
uint256 effectiveAPR; // Effective APR based on current multipliers
uint256 stakeDuration; // How long they've been staking
uint256 nextMultiplierThreshold; // Time until next multiplier tier
}
function getDetailedRewards(address user) external view returns (RewardBreakdown memory) {
RewardBreakdown memory breakdown;
if (stakedAmount[user] == 0) return breakdown;
// Calculate base numbers
uint256 totalStakingDuration = block.timestamp - stakingStartTime[user];
uint256 dynamicRate = baseRewardRate + dynamicRewardRate;
// Get multipliers
breakdown.timeMultiplier = getTimeMultiplier(totalStakingDuration);
breakdown.amountMultiplier = getAmountMultiplier(stakedAmount[user]);
// Calculate accumulated rewards
breakdown.accumulatedRewards = calculateReward(user);
breakdown.secondaryRewards = breakdown.accumulatedRewards / 10; // TOPIA rewards
// Calculate daily rate
uint256 dailyBase = (stakedAmount[user] * dynamicRate * 86400) / 1e18; // 86400 seconds in a day
breakdown.rewardsPerDay = dailyBase * breakdown.timeMultiplier * breakdown.amountMultiplier * bonusMultiplier / 1000000;
// Calculate effective APR
breakdown.effectiveAPR = (breakdown.rewardsPerDay * 365 * 100) / stakedAmount[user];
// Staking duration info
breakdown.stakeDuration = totalStakingDuration;
// Calculate time until next multiplier
if (totalStakingDuration < TIME_TIER1) {
breakdown.nextMultiplierThreshold = TIME_TIER1 - totalStakingDuration;
} else if (totalStakingDuration < TIME_TIER2) {
breakdown.nextMultiplierThreshold = TIME_TIER2 - totalStakingDuration;
} else if (totalStakingDuration < TIME_TIER3) {
breakdown.nextMultiplierThreshold = TIME_TIER3 - totalStakingDuration;
} else {
breakdown.nextMultiplierThreshold = 0; // Max tier reached
}
return breakdown;
}
// Add receive() function to accept native tokens
receive() external payable {}
}