// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
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);
}
contract Ownable {
error NotOwner();
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor() {
address msgSender = msg.sender;
_owner = msgSender;
emit OwnershipTransferred(address(0), msgSender);
}
function owner() public view returns (address) {
return _owner;
}
modifier onlyOwner() {
if (_owner != msg.sender) revert NotOwner();
_;
}
function renounceOwnership() public virtual onlyOwner {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
}
contract OZKStake is Ownable {
error StakingWindowClosed();
error StakingInProgress();
error StakingNotInProgress();
error InsufficientBalance();
error NothingStaked();
error NotAllowed();
event Stake(address sender, uint256 cycle, uint256 amount);
event Unstake(address sender, uint256 cycle, uint256 amount);
uint256 public constant STAKING_OPEN_WINDOW = 3 days;
address public immutable TOKEN;
uint256 public immutable STAKING_DURATION;
mapping(uint256 => uint256) public depositedAmount;
mapping(uint256 => mapping(address => uint256)) public userStakedAmountMap;
mapping(uint256 => uint256) public totalStakedToken;
bool public allowEmergencyTokenWithdraw = false;
uint256 public cycle;
uint256 public openTime;
constructor(address token_, uint256 stakingDuration_) {
TOKEN = token_;
STAKING_DURATION = stakingDuration_;
}
receive() external payable {}
// owners
function open() external payable onlyOwner {
uint256 __openTime = openTime;
if (block.timestamp >= __openTime && block.timestamp <= __openTime + STAKING_DURATION) {
revert StakingInProgress();
}
cycle += 1;
openTime = block.timestamp;
_deposit();
}
function deposit() public payable onlyOwner {
if (!isInProgress()) revert StakingNotInProgress();
_deposit();
}
function emergencyWithdraw() external onlyOwner {
(bool success,) = msg.sender.call{value: address(this).balance}("");
require(success);
}
function syncDepositedAmount(uint256 amount, uint256 cycle_) external onlyOwner {
if (address(this).balance < amount) revert InsufficientBalance();
depositedAmount[cycle_] = amount;
}
function setAllowTokenWithdrawal(bool allow_) external onlyOwner {
allowEmergencyTokenWithdraw = allow_;
}
// external
function stake(uint256 amount_) external {
if (!isOpen()) revert StakingWindowClosed();
uint256 __cycle = cycle;
userStakedAmountMap[__cycle][msg.sender] += amount_;
totalStakedToken[__cycle] += amount_;
emit Stake(msg.sender, __cycle, amount_);
IERC20(TOKEN).transferFrom(msg.sender, address(this), amount_);
}
function unstake(uint256 cycle_) external {
if (cycle_ == cycle && block.timestamp <= openTime + STAKING_DURATION) revert StakingInProgress();
// check amount staked
uint256 __amountStaked = userStakedAmountMap[cycle_][msg.sender];
if (__amountStaked == 0) revert NothingStaked();
// calculate amount
uint256 __totalStaked = totalStakedToken[cycle_];
uint256 claimableReward = 0;
if (__totalStaked != 0) {
claimableReward = depositedAmount[cycle_] * __amountStaked / __totalStaked;
}
// update state
userStakedAmountMap[cycle_][msg.sender] -= __amountStaked;
emit Unstake(msg.sender, cycle_, __amountStaked);
// withdraw
IERC20(TOKEN).transfer(msg.sender, __amountStaked);
(bool success,) = msg.sender.call{value: claimableReward}("");
require(success);
}
function emergencyTokenWithdraw(uint256 cycle_) external {
if (!allowEmergencyTokenWithdraw) revert NotAllowed();
// check amount staked
uint256 __amountStaked = userStakedAmountMap[cycle_][msg.sender];
if (__amountStaked == 0) revert NothingStaked();
// update state
userStakedAmountMap[cycle_][msg.sender] -= __amountStaked;
emit Unstake(msg.sender, cycle_, __amountStaked);
// withdraw
IERC20(TOKEN).transfer(msg.sender, __amountStaked);
}
// views
function isOpen() public view returns (bool) {
uint256 __openTime = openTime;
if (block.timestamp >= __openTime && block.timestamp <= __openTime + STAKING_OPEN_WINDOW) {
return true;
}
return false;
}
function isInProgress() public view returns (bool) {
uint256 __openTime = openTime;
if (block.timestamp >= __openTime && block.timestamp <= __openTime + STAKING_DURATION) {
return true;
}
return false;
}
function claimable(uint256 cycle_, address addr) external view returns (uint256) {
uint256 __amountStaked = userStakedAmountMap[cycle_][addr];
uint256 __totalStaked = totalStakedToken[cycle_];
if (__totalStaked == 0) return 0;
return depositedAmount[cycle_] * __amountStaked / __totalStaked;
}
// private
function _deposit() private {
depositedAmount[cycle] += msg.value;
}
}
{
"compilationTarget": {
"OZKStake.sol": "OZKStake"
},
"evmVersion": "shanghai",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"address","name":"token_","type":"address"},{"internalType":"uint256","name":"stakingDuration_","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"InsufficientBalance","type":"error"},{"inputs":[],"name":"NotAllowed","type":"error"},{"inputs":[],"name":"NotOwner","type":"error"},{"inputs":[],"name":"NothingStaked","type":"error"},{"inputs":[],"name":"StakingInProgress","type":"error"},{"inputs":[],"name":"StakingNotInProgress","type":"error"},{"inputs":[],"name":"StakingWindowClosed","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"cycle","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Stake","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"cycle","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Unstake","type":"event"},{"inputs":[],"name":"STAKING_DURATION","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STAKING_OPEN_WINDOW","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TOKEN","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"allowEmergencyTokenWithdraw","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"cycle_","type":"uint256"},{"internalType":"address","name":"addr","type":"address"}],"name":"claimable","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"cycle","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"deposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"depositedAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"cycle_","type":"uint256"}],"name":"emergencyTokenWithdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"emergencyWithdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"isInProgress","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isOpen","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"open","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"openTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"allow_","type":"bool"}],"name":"setAllowTokenWithdrawal","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount_","type":"uint256"}],"name":"stake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"cycle_","type":"uint256"}],"name":"syncDepositedAmount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"totalStakedToken","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"cycle_","type":"uint256"}],"name":"unstake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"userStakedAmountMap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"stateMutability":"payable","type":"receive"}]