OneWayDayStaking

Description:

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

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "degenvault.sol": {
      "content": "// SPDX-License-Identifier: MIT\r
pragma solidity ^0.8.19;\r
\r
/*\r
Simple 1-way immutable staking contract\r
- Deployer must approve reward tokens to the contract and call fundAndStart(rewardAmount, duration)\r
- fundAndStart transfers reward tokens into contract and starts the staking period (one-way; deployer can't withdraw)\r
- Users stake ERC20 tokens, earn proportional rewards over `duration` seconds\r
- APY view functions included\r
- Uses basic reentrancy guard and careful require checks\r
*/\r
\r
interface IERC20 {\r
    function totalSupply() external view returns (uint256);\r
    function balanceOf(address owner) external view returns (uint256);\r
    function transfer(address to, uint256 amount) external returns (bool);\r
    function approve(address spender, uint256 amount) external returns (bool);\r
    function transferFrom(address from, address to, uint256 amount) external returns (bool);\r
    function decimals() external view returns (uint8);\r
}\r
\r
contract OneWayDayStaking {\r
    // ========== STATE ==========\r
    IERC20 public immutable stakingToken; // token users stake\r
    IERC20 public immutable rewardToken;  // token used to pay rewards (can be same as stakingToken)\r
    address public immutable deployer;    // who deployed\r
    bool public started;\r
\r
    uint256 public rewardAmount;   // total reward allocated for the duration\r
    uint256 public startTimestamp; // timestamp when staking started\r
    uint256 public duration;       // seconds, e.g., 24 hours = 86400\r
    uint256 public rewardRate;     // reward tokens per second (scaled naturally)\r
\r
    // staking accounting\r
    uint256 public totalStaked;\r
    mapping(address => uint256) public balanceOf;\r
    mapping(address => uint256) public userRewardPerTokenPaid;\r
    mapping(address => uint256) public rewards; // accrued but not claimed\r
\r
    // reward-per-token accumulation (scaled by 1e18)\r
    uint256 public rewardPerTokenStored;\r
    uint256 private constant PRECISION = 1e18;\r
\r
    // reentrancy guard\r
    uint256 private _status;\r
    modifier nonReentrant() {\r
        require(_status != 2, "reentrant");\r
        _status = 2;\r
        _;\r
        _status = 1;\r
    }\r
\r
    // events\r
    event FundedAndStarted(address indexed funder, uint256 rewardAmount, uint256 duration, uint256 startTimestamp);\r
    event Staked(address indexed user, uint256 amount);\r
    event Withdrawn(address indexed user, uint256 amount);\r
    event RewardPaid(address indexed user, uint256 reward);\r
    event EmergencyRecovered(address indexed to, uint256 amount); // only for any accidentally sent ETH\r
\r
    constructor(address _stakingToken, address _rewardToken) {\r
        require(_stakingToken != address(0), "staking token 0");\r
        require(_rewardToken != address(0), "reward token 0");\r
        stakingToken = IERC20(_stakingToken);\r
        rewardToken  = IERC20(_rewardToken);\r
        deployer = msg.sender;\r
        _status = 1;\r
    }\r
\r
    // ========== CORE FUNCTIONS ==========\r
\r
    /// @notice fund the contract with reward tokens and start the staking period\r
    /// @dev This is atomic: contract transferFroms rewardAmount from caller. Caller must approve first.\r
    /// @param _rewardAmount total rewards to distribute\r
    /// @param _duration seconds for distribution (use 86400 for 24 hours)\r
    function fundAndStart(uint256 _rewardAmount, uint256 _duration) external nonReentrant {\r
        require(!started, "already started");\r
        require(_rewardAmount > 0, "reward 0");\r
        require(_duration >= 1 minutes, "duration too short");\r
        // transfer reward tokens into contract\r
        bool ok = rewardToken.transferFrom(msg.sender, address(this), _rewardAmount);\r
        require(ok, "reward transfer failed");\r
        // set rewards and start\r
        rewardAmount = _rewardAmount;\r
        duration = _duration;\r
        startTimestamp = block.timestamp;\r
        rewardRate = _rewardAmount / _duration; // integer division; remainder left in contract but harmless\r
        started = true;\r
        emit FundedAndStarted(msg.sender, _rewardAmount, _duration, startTimestamp);\r
    }\r
\r
    /// @notice stake tokens to earn rewards\r
    function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {\r
        require(started, "not started");\r
        require(amount > 0, "amount 0");\r
        bool ok = stakingToken.transferFrom(msg.sender, address(this), amount);\r
        require(ok, "stake transfer failed");\r
        totalStaked += amount;\r
        balanceOf[msg.sender] += amount;\r
        emit Staked(msg.sender, amount);\r
    }\r
\r
    /// @notice withdraw staked tokens (and optionally claim rewards). Allowed anytime.\r
    function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) {\r
        require(amount > 0, "amount 0");\r
        require(balanceOf[msg.sender] >= amount, "insufficient");\r
        balanceOf[msg.sender] -= amount;\r
        totalStaked -= amount;\r
        bool ok = stakingToken.transfer(msg.sender, amount);\r
        require(ok, "withdraw transfer failed");\r
        emit Withdrawn(msg.sender, amount);\r
    }\r
\r
    /// @notice claim accumulated rewards\r
    function claimReward() public nonReentrant updateReward(msg.sender) {\r
        uint256 reward = rewards[msg.sender];\r
        if (reward > 0) {\r
            rewards[msg.sender] = 0;\r
            bool ok = rewardToken.transfer(msg.sender, reward);\r
            require(ok, "reward transfer failed");\r
            emit RewardPaid(msg.sender, reward);\r
        }\r
    }\r
\r
    /// @notice convenience: exit = withdraw + claim\r
    function exit() external {\r
        withdraw(balanceOf[msg.sender]);\r
        claimReward();\r
    }\r
\r
    // ========== VIEWS (endpoints useful for nextjs) ==========\r
\r
    /// @notice how much reward per token is accumulated up to now (scaled by PRECISION)\r
    function rewardPerToken() public view returns (uint256) {\r
        if (totalStaked == 0) {\r
            return rewardPerTokenStored;\r
        }\r
        uint256 lastTime = lastTimeRewardApplicable();\r
        uint256 elapsed = lastTime - startTimestamp;\r
        if (elapsed > duration) elapsed = duration;\r
        uint256 added = rewardRate * elapsed * PRECISION / (totalStaked == 0 ? 1 : totalStaked);\r
        // BUT rewardPerTokenStored already accounted for previous time; need delta = rewardRate * (lastTime - lastUpdate)\r
        // To keep it simple and gas-light, we assume rewardPerTokenStored is up-to-date by modifier on state-changing calls.\r
        // For view, compute theoretical current extra:\r
        // we'll instead compute theoretical rewardPerToken as rewardPerTokenStored + (rewardRate * (lastTime - startTimestamp) * PRECISION)/totalStaked\r
        // but this duplicates logic; okay for view.\r
        if (startTimestamp == 0 || lastTime <= startTimestamp) {\r
            return rewardPerTokenStored;\r
        }\r
        uint256 secondsSinceStart = lastTime - startTimestamp;\r
        if (secondsSinceStart > duration) secondsSinceStart = duration;\r
        uint256 computed = rewardPerTokenStored + (rewardRate * secondsSinceStart * PRECISION) / (totalStaked == 0 ? 1 : totalStaked);\r
        return computed;\r
    }\r
\r
    /// @notice how much a given account has earned so far (call before/after staking to update)\r
    function earned(address account) public view returns (uint256) {\r
        uint256 rpt = rewardPerToken();\r
        return (balanceOf[account] * (rpt - userRewardPerTokenPaid[account])) / PRECISION + rewards[account];\r
    }\r
\r
    /// @notice current remaining reward balance (in reward token units)\r
    function rewardRemaining() public view returns (uint256) {\r
        if (!started) return rewardAmount;\r
        // reward already paid = rewardAmount - current contract balance\r
        uint256 bal = rewardToken.balanceOf(address(this));\r
        return bal;\r
    }\r
\r
    /// @notice seconds left in reward distribution (0 if finished or not started)\r
    function secondsLeft() public view returns (uint256) {\r
        if (!started) return 0;\r
        if (block.timestamp >= startTimestamp + duration) return 0;\r
        return (startTimestamp + duration) - block.timestamp;\r
    }\r
\r
    /// @notice APY (scaled by 1e18). Multiply by 100 then divide by 1e18 to get %.\r
    /// APY = (rewardRate * secondsPerYear) / totalStaked\r
    function getCurrentAPY() public view returns (uint256) {\r
        if (totalStaked == 0) return 0;\r
        uint256 secondsPerYear = 365 * 24 * 60 * 60; // 31536000\r
        // rewardRate * secondsPerYear / totalStaked, scale by PRECISION\r
        uint256 apy = (rewardRate * secondsPerYear * PRECISION) / totalStaked;\r
        return apy;\r
    }\r
\r
    /// @notice returns user APY (same as current APY because APY is independent of user stake)\r
    function getUserAPY(address) external view returns (uint256) {\r
        return getCurrentAPY();\r
    }\r
\r
    // ========== INTERNAL HELPERS ==========\r
    function lastTimeRewardApplicable() internal view returns (uint256) {\r
        if (!started) return 0;\r
        uint256 end = startTimestamp + duration;\r
        return block.timestamp < end ? block.timestamp : end;\r
    }\r
\r
    // modifier to update stored accounting before mutating stake/withdraw/claim\r
    modifier updateReward(address account) {\r
        if (started) {\r
            // update rewardPerTokenStored up to now (be careful with totalStaked==0)\r
            uint256 lastTime = lastTimeRewardApplicable();\r
            if (lastTime > startTimestamp && totalStaked > 0) {\r
                uint256 secondsSinceStart = lastTime - startTimestamp;\r
                if (secondsSinceStart > duration) secondsSinceStart = duration;\r
                // compute total distributed so far = min(elapsed, duration) * rewardRate\r
                uint256 distributed = secondsSinceStart * rewardRate;\r
                // rewardPerTokenStored = distributed * PRECISION / totalStaked\r
                // But if update called many times, we should track previously distributed. For simplicity, we compute delta since last accounted via rewardPerTokenStored\r
                // We'll treat rewardPerTokenStored as a cumulative marker that is updated here.\r
                // We compute a theoretical rewardPerTokenTotal = distributed * PRECISION / totalStaked\r
                // But to avoid double counting across calls without tracking lastUpdate, instead we keep the simpler approach:\r
                rewardPerTokenStored = rewardPerToken(); // call view computation (not super gas optimal but simple)\r
            }\r
        }\r
        if (account != address(0)) {\r
            // update user's rewards\r
            rewards[account] = earned(account);\r
            userRewardPerTokenPaid[account] = rewardPerTokenStored;\r
        }\r
        _;\r
    }\r
\r
    // ========== SAFETY / ADMIN (very limited) ==========\r
    // To avoid trapped ETH, allow owner to recover accidental ETH only (no token recovery)\r
    function recoverETH(address payable to) external {\r
        require(msg.sender == deployer, "only deployer");\r
        uint256 bal = address(this).balance;\r
        if (bal > 0) {\r
            (bool s,) = to.call{value: bal}("");\r
            require(s, "eth send fail");\r
            emit EmergencyRecovered(to, bal);\r
        }\r
    }\r
\r
    // fallback/receive to accept ETH (rare)\r
    receive() external payable {}\r
}\r
"
    }
  },
  "settings": {
    "optimizer": {
      "enabled": false,
      "runs": 200
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "remappings": []
  }
}}

Tags:
ERC20, Token, Factory|addr:0xfe8c25022068f5294031b5cd382190fd1d8a47e7|verified:true|block:23395812|tx:0x91186140d28092ef168f34b340942c140047043b9f24c035a8be215ffbc80870|first_check:1758279161

Submitted on: 2025-09-19 12:52:43

Comments

Log in to comment.

No comments yet.