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": []
}
}}
Submitted on: 2025-09-19 12:52:43
Comments
Log in to comment.
No comments yet.