编译器
0.8.24+commit.e11b9ed9
文件 1 的 5:Context.sol
pragma solidity ^0.8.20;
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}
文件 2 的 5:IERC20.sol
pragma solidity ^0.8.20;
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
文件 3 的 5:Ownable.sol
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
abstract contract Ownable is Context {
address private _owner;
error OwnableUnauthorizedAccount(address account);
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
modifier onlyOwner() {
_checkOwner();
_;
}
function owner() public view virtual returns (address) {
return _owner;
}
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
文件 4 的 5:ReentrancyGuard.sol
pragma solidity ^0.8.20;
abstract contract ReentrancyGuard {
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
uint256 private _status;
error ReentrancyGuardReentrantCall();
constructor() {
_status = NOT_ENTERED;
}
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
if (_status == ENTERED) {
revert ReentrancyGuardReentrantCall();
}
_status = ENTERED;
}
function _nonReentrantAfter() private {
_status = NOT_ENTERED;
}
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == ENTERED;
}
}
文件 5 的 5:StakingContract.sol
pragma solidity 0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract StakingContract is ReentrancyGuard, Ownable {
IERC20 public basicToken;
uint256 public totalStaked;
uint256 public totalStakedAccruingRewards;
uint256 public rewardRate;
uint256 public lastUpdateTime;
uint256 public rewardPerTokenStored;
uint256 public unstakeTimeLock = 7 days;
uint256 public unstakeFeePercent = 0;
uint256 public emissionStart;
uint256 public emissionEnd;
uint256 public feesAccrued;
uint256 public pendingRewardsStored;
uint256 private constant MAX_FEE = 200;
uint256 private constant BASIS_POINTS = 10000;
uint256 private constant MAX_TIMELOCK = 15 days;
struct Staker {
uint256 amountStaked;
uint256 rewardDebt;
uint256 rewards;
uint256 unstakeInitTime;
}
mapping(address => Staker) public stakers;
event UnstakeInitiated(address indexed user);
event Staked(address indexed user, uint256 amount);
event Unstaked(address indexed user, uint256 amount);
event RewardPaid(address indexed user, uint256 reward);
event UnstakeFeePercentUpdated(uint256 newFeePercent);
event UnstakeTimeLockUpdated(uint256 newTimeLock);
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
pendingRewardsStored = pendingRewards();
lastUpdateTime = lastApplicableTime();
Staker storage staker = stakers[account];
if (staker.unstakeInitTime == 0 ) {
staker.rewards = earned(account);
staker.rewardDebt = rewardPerTokenStored;
}
_;
}
constructor(IERC20 _basicToken, uint256 _rewardRate, uint256 _emissionStart, uint256 _emissionDuration) Ownable(msg.sender) {
basicToken = _basicToken;
rewardRate = _rewardRate;
emissionStart = _emissionStart;
emissionEnd = emissionStart + _emissionDuration;
}
function lastApplicableTime() public view returns (uint256) {
return block.timestamp < emissionEnd ? block.timestamp : emissionEnd;
}
function pendingRewards() public view returns (uint256) {
uint256 totalStakedAccruingRewardsCached = totalStakedAccruingRewards;
if (totalStakedAccruingRewardsCached == 0) return pendingRewardsStored;
uint256 fullPendingRewards = (lastApplicableTime() - lastUpdateTime) * rewardRate * 1e18;
uint256 roundingError = fullPendingRewards % totalStakedAccruingRewardsCached;
return pendingRewardsStored + fullPendingRewards - roundingError;
}
function rewardPerToken() public view returns (uint256) {
if (totalStakedAccruingRewards == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored + (
(lastApplicableTime() - lastUpdateTime) * rewardRate * 1e18 / totalStakedAccruingRewards
);
}
function earned(address account) public view returns (uint256) {
Staker storage staker = stakers[account];
if (staker.unstakeInitTime != 0) {
return staker.rewards;
}
return (
staker.amountStaked *
(rewardPerToken() - staker.rewardDebt) / 1e18
) + staker.rewards;
}
function stake(uint256 _amount) external nonReentrant updateReward(msg.sender) {
require(_amount > 0, "Cannot stake 0");
require(block.timestamp >= emissionStart, "Staking not yet started");
require(block.timestamp <= emissionEnd, "Staking period has ended");
Staker storage staker = stakers[msg.sender];
require(staker.unstakeInitTime == 0, "Cannot stake after initiating unstake.");
totalStaked += _amount;
totalStakedAccruingRewards += _amount;
staker.amountStaked += _amount;
require(basicToken.transferFrom(msg.sender, address(this), _amount), "Token deposit failed");
emit Staked(msg.sender, _amount);
}
function initiateUnstake() external updateReward(msg.sender) {
Staker storage staker = stakers[msg.sender];
require(staker.amountStaked > 0, "No tokens staked");
require(staker.unstakeInitTime == 0, "Unstake already initiated");
staker.unstakeInitTime = block.timestamp;
totalStakedAccruingRewards -= staker.amountStaked;
emit UnstakeInitiated(msg.sender);
}
function completeUnstake() external nonReentrant updateReward(msg.sender) {
Staker storage staker = stakers[msg.sender];
require(staker.amountStaked > 0, "No tokens staked");
require(staker.unstakeInitTime > 0, "Unstake not initiated");
require(block.timestamp >= staker.unstakeInitTime + unstakeTimeLock, "Timelock not yet passed");
uint256 amount = staker.amountStaked;
uint256 reward = staker.rewards;
uint256 fee = amount * unstakeFeePercent / BASIS_POINTS;
uint256 amountAfterFee = amount - fee;
uint256 availableForRewards = _getFreeContractBalance();
feesAccrued += fee;
totalStaked -= amount;
staker.amountStaked = 0;
staker.rewardDebt = 0;
staker.unstakeInitTime = 0;
uint256 totalAmount = amountAfterFee;
if (reward > 0 && availableForRewards >= reward) {
if (pendingRewardsStored >= reward * 1e18) pendingRewardsStored -= reward * 1e18;
totalAmount += reward;
staker.rewards = 0;
emit RewardPaid(msg.sender, reward);
}
require(basicToken.transfer(msg.sender, totalAmount), "Unstake transfer failed");
emit Unstaked(msg.sender, amount);
}
function claimReward() external nonReentrant updateReward(msg.sender) {
Staker storage staker = stakers[msg.sender];
uint256 reward = staker.rewards;
require(reward > 0, "No rewards to claim");
uint256 availableForRewards = _getFreeContractBalance();
require(availableForRewards >= reward, "Insufficient funds for reward");
if (pendingRewardsStored >= reward * 1e18) pendingRewardsStored -= reward * 1e18;
staker.rewards = 0;
require(basicToken.transfer(msg.sender, reward), "Reward transfer failed");
emit RewardPaid(msg.sender, reward);
}
function setUnstakeFeePercent(uint256 _newFee) external onlyOwner {
require(_newFee <= MAX_FEE, "Unstake fee exceeds 2%, maximum allowed");
unstakeFeePercent = _newFee;
emit UnstakeFeePercentUpdated(_newFee);
}
function setUnstakeTimeLock(uint256 _newTimeLock) external onlyOwner {
require(_newTimeLock <= MAX_TIMELOCK, "Time lock must be between 0 to 15 days");
unstakeTimeLock = _newTimeLock;
emit UnstakeTimeLockUpdated(_newTimeLock);
}
function getRemainingUnstakeTime(address _staker) external view returns (uint256) {
Staker storage staker = stakers[_staker];
if (block.timestamp < staker.unstakeInitTime + unstakeTimeLock) {
return (staker.unstakeInitTime + unstakeTimeLock) - block.timestamp;
} else {
return 0;
}
}
function withdrawFees(uint256 amount) external onlyOwner {
require(amount <= feesAccrued, "Amount exceeds accrued fees");
feesAccrued -= amount;
require(basicToken.transfer(msg.sender, amount), "Fee withdrawal failed");
}
function withdrawRemainingTokens() external updateReward(address(0)) onlyOwner {
require(block.timestamp > emissionEnd, "Cannot withdraw before emission end");
uint256 freeContractBalanceCached = _getFreeContractBalance();
uint256 pendingRewardsCached = pendingRewards() / 1e18;
require(freeContractBalanceCached > pendingRewardsCached, "No remaining tokens to withdraw");
uint256 remainingBalance = freeContractBalanceCached - pendingRewardsCached;
require(basicToken.transfer(msg.sender, remainingBalance), "Token withdrawal failed");
}
function _getFreeContractBalance() internal view returns (uint256) {
return basicToken.balanceOf(address(this)) - totalStaked - feesAccrued;
}
}
{
"compilationTarget": {
"src/StakingContract.sol": "StakingContract"
},
"evmVersion": "paris",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": [
":@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
":ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/",
":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/",
":forge-std/=lib/forge-std/src/",
":openzeppelin-contracts/=lib/openzeppelin-contracts/"
]
}
[{"inputs":[{"internalType":"contract IERC20","name":"_basicToken","type":"address"},{"internalType":"uint256","name":"_rewardRate","type":"uint256"},{"internalType":"uint256","name":"_emissionStart","type":"uint256"},{"internalType":"uint256","name":"_emissionDuration","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"OwnableInvalidOwner","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"OwnableUnauthorizedAccount","type":"error"},{"inputs":[],"name":"ReentrancyGuardReentrantCall","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":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"reward","type":"uint256"}],"name":"RewardPaid","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Staked","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newFeePercent","type":"uint256"}],"name":"UnstakeFeePercentUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"}],"name":"UnstakeInitiated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newTimeLock","type":"uint256"}],"name":"UnstakeTimeLockUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Unstaked","type":"event"},{"inputs":[],"name":"basicToken","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"claimReward","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"completeUnstake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"earned","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"emissionEnd","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"emissionStart","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feesAccrued","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_staker","type":"address"}],"name":"getRemainingUnstakeTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"initiateUnstake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"lastApplicableTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastUpdateTime","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":"pendingRewards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pendingRewardsStored","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"rewardPerToken","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rewardPerTokenStored","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rewardRate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_newFee","type":"uint256"}],"name":"setUnstakeFeePercent","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_newTimeLock","type":"uint256"}],"name":"setUnstakeTimeLock","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"stake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"stakers","outputs":[{"internalType":"uint256","name":"amountStaked","type":"uint256"},{"internalType":"uint256","name":"rewardDebt","type":"uint256"},{"internalType":"uint256","name":"rewards","type":"uint256"},{"internalType":"uint256","name":"unstakeInitTime","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalStaked","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalStakedAccruingRewards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unstakeFeePercent","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unstakeTimeLock","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdrawFees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawRemainingTokens","outputs":[],"stateMutability":"nonpayable","type":"function"}]