Description:
Decentralized Finance (DeFi) protocol contract providing Staking functionality.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
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);
}
abstract contract Ownable {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor() {
_owner = msg.sender;
emit OwnershipTransferred(address(0), 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 {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
}
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;
}
}
contract DogenariiStaking is Ownable, ReentrancyGuard {
IERC20 public immutable stakingToken;
bool public emergencyWithdrawEnabled;
uint256 public totalPrincipalStaked;
bool public paused;
struct PoolInfo {
uint256 apy;
uint256 unlockTime;
uint256 totalStaked;
bool isActive;
}
struct StakeInfo {
uint256 amount;
uint256 startTime;
uint256 apy;
uint256 rewardDebt;
uint256 stakerIndex;
}
PoolInfo[] public poolInfo;
mapping(uint256 => mapping(address => StakeInfo)) public userStakes;
mapping(uint256 => address[]) public stakersInPool;
uint256 public constant BASIS_POINTS = 100;
uint256 public constant DAYS_IN_YEAR = 365;
uint256 public constant PRECISION_FACTOR = 1e18;
event PoolAdded(uint256 indexed pid, uint256 apy, uint256 unlockTime);
event PoolUpdated(uint256 indexed pid, uint256 apy, uint256 unlockTime, bool isActive);
event Staked(address indexed user, uint256 indexed pid, uint256 amount);
event Unstaked(address indexed user, uint256 indexed pid, uint256 amount, uint256 reward);
event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount);
event RewardsDeposited(address indexed funder, uint256 amount);
event EmergencyTokenRecovery(address indexed token, address indexed to, uint256 amount);
event EmergencyNativeRecovery(address indexed to, uint256 amount);
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
constructor(address _stakingTokenAddress) {
stakingToken = IERC20(_stakingTokenAddress);
emergencyWithdrawEnabled = false;
}
function setPaused(bool _paused) external onlyOwner {
paused = _paused;
}
function addPool(uint256 _apy, uint256 _unlockTime) external onlyOwner {
require(_apy > 0 && _apy <= 50, "APY must be 1-50%");
require(_unlockTime > block.timestamp, "Unlock time must be in the future");
require(_unlockTime <= block.timestamp + 730 days, "Unlock time max 2 years");
poolInfo.push(PoolInfo({
apy: _apy,
unlockTime: _unlockTime,
totalStaked: 0,
isActive: true
}));
emit PoolAdded(poolInfo.length - 1, _apy, _unlockTime);
}
function updatePool(uint256 _pid, uint256 _apy, uint256 _unlockTime, bool _isActive) external onlyOwner {
require(_pid < poolInfo.length, "Pool does not exist");
require(_apy > 0 && _apy <= 50, "APY must be 1-50%");
require(_unlockTime > block.timestamp, "Unlock time must be in the future");
require(_unlockTime <= block.timestamp + 730 days, "Unlock time max 2 years");
poolInfo[_pid].apy = _apy;
poolInfo[_pid].unlockTime = _unlockTime;
poolInfo[_pid].isActive = _isActive;
emit PoolUpdated(_pid, _apy, _unlockTime, _isActive);
}
function setEmergencyWithdraw(bool _enabled) external onlyOwner {
emergencyWithdrawEnabled = _enabled;
}
function depositRewards(uint256 _amount) external onlyOwner {
require(_amount > 0, "Amount must be greater than zero");
stakingToken.transferFrom(msg.sender, address(this), _amount);
emit RewardsDeposited(msg.sender, _amount);
}
function recoverTokens(address _tokenAddress, uint256 _amount) external onlyOwner {
require(_tokenAddress != address(stakingToken), "Cannot recover the staking token");
IERC20(_tokenAddress).transfer(owner(), _amount);
emit EmergencyTokenRecovery(_tokenAddress, owner(), _amount);
}
function recoverNativeETH() external onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No native balance to recover");
(bool success, ) = payable(owner()).call{value: balance}("");
require(success, "Native transfer failed");
emit EmergencyNativeRecovery(owner(), balance);
}
// Public User Functions
function stake(uint256 _pid, uint256 _amount) external nonReentrant whenNotPaused {
require(_pid < poolInfo.length, "Pool does not exist");
PoolInfo storage pool = poolInfo[_pid];
require(block.timestamp < pool.unlockTime, "Pool has already unlocked");
require(pool.isActive, "Pool is not active");
require(_amount > 0, "Cannot stake zero tokens");
StakeInfo storage userStake = userStakes[_pid][msg.sender];
if (userStake.amount > 0) {
uint256 pendingRewards = calculateRewards(_pid, msg.sender);
userStake.rewardDebt += pendingRewards;
} else {
userStake.stakerIndex = stakersInPool[_pid].length;
stakersInPool[_pid].push(msg.sender);
}
pool.totalStaked += _amount;
totalPrincipalStaked += _amount;
userStake.amount += _amount;
userStake.startTime = block.timestamp;
userStake.apy = pool.apy;
stakingToken.transferFrom(msg.sender, address(this), _amount);
emit Staked(msg.sender, _pid, _amount);
}
function unstake(uint256 _pid) external nonReentrant whenNotPaused {
_unstake(_pid, false);
}
function emergencyWithdraw(uint256 _pid) external nonReentrant whenNotPaused {
require(emergencyWithdrawEnabled, "Emergency withdraw is not enabled");
_unstake(_pid, true);
}
// --- Internal Logic ---
function _unstake(uint256 _pid, bool _isEmergency) private {
require(_pid < poolInfo.length, "Pool does not exist");
PoolInfo storage pool = poolInfo[_pid];
StakeInfo storage userStake = userStakes[_pid][msg.sender];
address stakerAddress = msg.sender;
require(userStake.amount > 0, "You have no active stake in this pool");
uint256 principal = userStake.amount;
uint256 reward = 0;
if (_isEmergency) {
emit EmergencyWithdraw(stakerAddress, _pid, principal);
} else {
require(block.timestamp >= pool.unlockTime, "Pool is still locked");
reward = userStake.rewardDebt + calculateRewards(_pid, stakerAddress);
emit Unstaked(stakerAddress, _pid, principal, reward);
}
pool.totalStaked -= principal;
totalPrincipalStaked -= principal;
uint256 indexToRemove = userStake.stakerIndex;
address lastStaker = stakersInPool[_pid][stakersInPool[_pid].length - 1];
if (stakerAddress != lastStaker) {
stakersInPool[_pid][indexToRemove] = lastStaker;
userStakes[_pid][lastStaker].stakerIndex = indexToRemove;
}
stakersInPool[_pid].pop();
delete userStakes[_pid][stakerAddress];
uint256 totalWithdrawal = principal + reward;
require(stakingToken.balanceOf(address(this)) >= totalWithdrawal, "Insufficient funds in contract");
stakingToken.transfer(stakerAddress, totalWithdrawal);
}
// --- View Functions ---
function getAvailableRewards() public view returns (uint256) {
uint256 contractBalance = stakingToken.balanceOf(address(this));
return contractBalance <= totalPrincipalStaked ? 0 : contractBalance - totalPrincipalStaked;
}
function getTotalRewardLiability() external view returns (uint256[] memory rewardsPerPool, uint256 grandTotalReward) {
uint256 numPools = poolInfo.length;
rewardsPerPool = new uint256[](numPools);
for (uint256 pid = 0; pid < numPools; pid++) {
uint256 poolTotalReward;
PoolInfo memory pool = poolInfo[pid];
address[] memory stakers = stakersInPool[pid];
for (uint256 i = 0; i < stakers.length; i++) {
StakeInfo memory userStake = userStakes[pid][stakers[i]];
if (pool.unlockTime > userStake.startTime) {
uint256 totalStakingDurationInSeconds = pool.unlockTime - userStake.startTime;
uint256 totalStakingDurationInDays = totalStakingDurationInSeconds / 1 days;
uint256 fullReward = (userStake.amount * userStake.apy * totalStakingDurationInDays) / (BASIS_POINTS * DAYS_IN_YEAR);
poolTotalReward += userStake.rewardDebt + fullReward;
}
}
rewardsPerPool[pid] = poolTotalReward;
grandTotalReward += poolTotalReward;
}
return (rewardsPerPool, grandTotalReward);
}
function calculateRewards(uint256 _pid, address _user) public view returns (uint256) {
StakeInfo memory userStake = userStakes[_pid][_user];
if (userStake.amount == 0) {
return 0;
}
PoolInfo memory pool = poolInfo[_pid];
uint256 endTime = block.timestamp > pool.unlockTime ? pool.unlockTime : block.timestamp;
if (userStake.startTime >= endTime) {
return 0;
}
uint256 stakingDurationInSeconds = endTime - userStake.startTime;
uint256 stakingDurationInDays = stakingDurationInSeconds / 1 days;
return (userStake.amount * userStake.apy * stakingDurationInDays) / (BASIS_POINTS * DAYS_IN_YEAR);
}
function getPoolUnlockTime(uint256 _pid) external view returns (uint256) {
require(_pid < poolInfo.length, "Pool does not exist");
return poolInfo[_pid].unlockTime;
}
function poolLength() external view returns (uint256) {
return poolInfo.length;
}
}
Submitted on: 2025-09-17 15:28:40
Comments
Log in to comment.
No comments yet.