KTTYStaking

Description:

ERC20 token contract with Pausable capabilities. Standard implementation for fungible tokens on Ethereum.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

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

/* --------------------------------------------------------- */
/* --------------------- Minimal Interfaces ---------------- */
/* --------------------------------------------------------- */

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address a) external view returns (uint256);
    function allowance(address owner, address spender) external view returns (uint256);
    function transfer(address to, uint256 v) external returns (bool);
    function approve(address spender, uint256 v) external returns (bool);
    function transferFrom(address from, address to, uint256 v) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 v);
    event Approval(address indexed owner, address indexed spender, uint256 v);
}

/* --------------------------------------------------------- */
/* --------------------- SafeERC20 Lite -------------------- */
/* --------------------------------------------------------- */

library SafeERC20 {
    function safeTransfer(IERC20 t, address to, uint256 v) internal {
        require(t.transfer(to, v), "SafeERC20: transfer failed");
    }
    function safeTransferFrom(IERC20 t, address from, address to, uint256 v) internal {
        require(t.transferFrom(from, to, v), "SafeERC20: transferFrom failed");
    }
    function safeApprove(IERC20 t, address s, uint256 v) internal {
        require(t.approve(s, v), "SafeERC20: approve failed");
    }
}

/* --------------------------------------------------------- */
/* ------------------------ Context ------------------------ */
/* --------------------------------------------------------- */

abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }
}

/* --------------------------------------------------------- */
/* --------------------- Ownable2Step Lite ----------------- */
/* --------------------------------------------------------- */

abstract contract Ownable2Step is Context {
    address private _owner;
    address private _pendingOwner;

    event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    error NotOwner();
    error NotPendingOwner();

    constructor(address initialOwner) {
        require(initialOwner != address(0), "Owner=0");
        _owner = initialOwner;
        emit OwnershipTransferred(address(0), initialOwner);
    }

    modifier onlyOwner() {
        if (_owner != _msgSender()) revert NotOwner();
        _;
    }

    function owner() public view returns (address) { return _owner; }
    function pendingOwner() public view returns (address) { return _pendingOwner; }

    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "newOwner=0");
        _pendingOwner = newOwner;
        emit OwnershipTransferStarted(_owner, newOwner);
    }

    function acceptOwnership() public {
        if (_msgSender() != _pendingOwner) revert NotPendingOwner();
        address old = _owner;
        _owner = _pendingOwner;
        _pendingOwner = address(0);
        emit OwnershipTransferred(old, _owner);
    }
}

/* --------------------------------------------------------- */
/* --------------------- ReentrancyGuard ------------------- */
/* --------------------------------------------------------- */

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, "Reentrancy");
        _status = _ENTERED;
        _;
        _status = _NOT_ENTERED;
    }
}

/* --------------------------------------------------------- */
/* ------------------------- Pausable ---------------------- */
/* --------------------------------------------------------- */

abstract contract Pausable is Context {
    event Paused(address account);
    event Unpaused(address account);
    bool private _paused;

    modifier whenNotPaused() {
        require(!_paused, "Paused");
        _;
    }
    modifier whenPaused() {
        require(_paused, "Not paused");
        _;
    }
    function paused() public view returns (bool) { return _paused; }
    function _pause() internal whenNotPaused { _paused = true; emit Paused(_msgSender()); }
    function _unpause() internal whenPaused { _paused = false; emit Unpaused(_msgSender()); }
}

/* --------------------------------------------------------- */
/* ----------------------- KTTY Staking -------------------- */
/* --------------------------------------------------------- */

contract KTTYStaking is Ownable2Step, ReentrancyGuard, Pausable {
    using SafeERC20 for IERC20;

    /* -------------------- Immutable config -------------------- */
    IERC20 public immutable stakingToken;       // KTTY
    IERC20 public immutable rewardsToken;       // KTTY (same as staking)
    uint256 public immutable rewardDuration;    // seconds for a reward cycle

    /* -------------------- Reward accounting ------------------- */
    uint256 public periodFinish;                // timestamp when rewards end
    uint256 public rewardRate;                  // tokens per second
    uint256 public lastUpdateTime;              // last reward calc timestamp
    uint256 public rewardPerTokenStored;        // scaled by 1e18

    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public rewards;

    /* -------------------- Staking balances -------------------- */
    uint256 public totalStaked;
    mapping(address => uint256) public balances;

    /* ------------------------- Events ------------------------- */
    event Staked(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event RewardPaid(address indexed user, uint256 reward);
    event RewardNotified(uint256 amount, uint256 newRate, uint256 periodFinish);
    event PausedByOwner(address indexed by);
    event UnpausedByOwner(address indexed by);

    /* ------------------------- Errors ------------------------- */
    error ZeroAmount();
    error InsufficientBalance();
    error RecoverInvalidToken();
    error AmountExceedsExcess();

    /* ----------------------- Constructor ---------------------- */
    /// @param _token KTTY token address (staking & rewards)
    /// @param _rewardDuration Reward duration in seconds (e.g., 14 days = 1209600)
    constructor(address _token, uint256 _rewardDuration)
        Ownable2Step(msg.sender)
    {
        require(_token != address(0), "token=0");
        require(_rewardDuration > 0, "duration=0");
        stakingToken = IERC20(_token);
        rewardsToken = IERC20(_token);
        rewardDuration = _rewardDuration;
    }

    /* -------------------- View helpers -------------------- */

    function lastTimeRewardApplicable() public view returns (uint256) {
        return block.timestamp < periodFinish ? block.timestamp : periodFinish;
    }

    function rewardPerToken() public view returns (uint256) {
        if (totalStaked == 0) return rewardPerTokenStored;
        uint256 timeDelta = lastTimeRewardApplicable() - lastUpdateTime;
        return rewardPerTokenStored + (timeDelta * rewardRate * 1e18) / totalStaked;
    }

    function earned(address account) public view returns (uint256) {
        uint256 rpt = rewardPerToken();
        return ((balances[account] * (rpt - userRewardPerTokenPaid[account])) / 1e18) + rewards[account];
    }

    /* ---------------- Internal reward update ------------------ */

    modifier updateReward(address account) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = lastTimeRewardApplicable();
        if (account != address(0)) {
            rewards[account] = earned(account);
            userRewardPerTokenPaid[account] = rewardPerTokenStored;
        }
        _;
    }

    /* ----------------------- User actions --------------------- */

    function stake(uint256 amount)
        external
        nonReentrant
        whenNotPaused
        updateReward(msg.sender)
    {
        if (amount == 0) revert ZeroAmount();
        totalStaked += amount;
        balances[msg.sender] += amount;
        SafeERC20.safeTransferFrom(stakingToken, msg.sender, address(this), amount);
        emit Staked(msg.sender, amount);
    }

    function withdraw(uint256 amount)
        public
        nonReentrant
        updateReward(msg.sender)
    {
        if (amount == 0) revert ZeroAmount();
        if (balances[msg.sender] < amount) revert InsufficientBalance();
        totalStaked -= amount;
        balances[msg.sender] -= amount;
        SafeERC20.safeTransfer(stakingToken, msg.sender, amount);
        emit Withdrawn(msg.sender, amount);
    }

    function getReward()
        public
        nonReentrant
        updateReward(msg.sender)
    {
        uint256 reward = rewards[msg.sender];
        if (reward > 0) {
            rewards[msg.sender] = 0;
            SafeERC20.safeTransfer(rewardsToken, msg.sender, reward);
            emit RewardPaid(msg.sender, reward);
        }
    }

    function exit() external {
        withdraw(balances[msg.sender]);
        getReward();
    }

    /* ------------------- Owner: reward funding ----------------- */

    /// @notice Owner funds rewards. Must approve this contract to pull `amount` first.
    /// If called mid-period, leftover is rolled into the new rate.
    function notifyRewardAmount(uint256 amount)
        external
        onlyOwner
        updateReward(address(0))
    {
        if (amount == 0) revert ZeroAmount();

        uint256 newRate;
        if (block.timestamp >= periodFinish) {
            newRate = amount / rewardDuration;
        } else {
            uint256 remaining = periodFinish - block.timestamp;
            uint256 leftover = remaining * rewardRate;
            newRate = (amount + leftover) / rewardDuration;
        }

        // Pull fresh rewards from owner → contract
        SafeERC20.safeTransferFrom(rewardsToken, owner(), address(this), amount);

        rewardRate = newRate;
        lastUpdateTime = block.timestamp;
        periodFinish = block.timestamp + rewardDuration;

        emit RewardNotified(amount, newRate, periodFinish);
    }

    /* ------------------------ Admin ops ------------------------ */

    function pause() external onlyOwner {
        _pause();
        emit PausedByOwner(msg.sender);
    }

    function unpause() external onlyOwner {
        _unpause();
        emit UnpausedByOwner(msg.sender);
    }

    /// @notice Emergency: withdraw stake without claiming rewards (when paused).
    function emergencyWithdraw() external nonReentrant whenPaused {
        uint256 bal = balances[msg.sender];
        if (bal == 0) revert InsufficientBalance();
        totalStaked -= bal;
        balances[msg.sender] = 0;
        // DO NOT update/zero rewards → forfeits pending rewards by design
        SafeERC20.safeTransfer(stakingToken, msg.sender, bal);
        emit Withdrawn(msg.sender, bal);
    }

    /// @notice Recover non-staking tokens accidentally sent to this contract.
    function recoverMistakenToken(address token, uint256 amount) external onlyOwner {
        if (token == address(stakingToken)) {
            // Only allow withdrawing EXCESS beyond user stakes
            uint256 excess = IERC20(token).balanceOf(address(this)) - totalStaked;
            if (amount > excess) revert AmountExceedsExcess();
            SafeERC20.safeTransfer(IERC20(token), owner(), amount);
        } else {
            // Allow recovery of any other token
            SafeERC20.safeTransfer(IERC20(token), owner(), amount);
        }
    }
}

Tags:
ERC20, Token, Pausable|addr:0x68d972eef3713f8f014c5c92535cc94b952feb12|verified:true|block:23436738|tx:0x115cf875e50b6db4fc395969a569a50d3d8d557d48f61e6541291e64bc140a79|first_check:1758789621

Submitted on: 2025-09-25 10:40:21

Comments

Log in to comment.

No comments yet.