File 1 of 1: TokenStaking.sol
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);
function burn(uint256 value) external;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
contract TokenStaking {
IERC20 public taxFarmingToken;
uint256 public totalStaked;
uint256 public totalRewards;
uint256 private accumulatedRewardsPerToken = 1;
uint256 private constant DECIMALS = 1e12;
struct Stake {
uint256 tokensStaked;
uint256 accumulatedRewardsPerToken;
}
mapping(address => Stake) public stakersInfos;
mapping(address => uint256) public lastTx;
uint256 private constant LOCK_PERIOD = 7 days;
mapping(address => uint256) public lastClaimTime;
event TokenStaked(address indexed user, uint256 amount, uint256 accumulatedRewardsPerToken);
event TokenUnstaked(address indexed user, uint256 amount, uint256 accumulatedRewardsPerToken);
constructor(address _taxFarmingToken) {
taxFarmingToken = IERC20(_taxFarmingToken);
}
modifier blockUser(address user) {
require(lastTx[user] != block.number, "User blocked");
lastTx[user] = block.number;
_;
}
function stake(uint256 amount) external blockUser(msg.sender) {
if (amount == 0) return;
address user = msg.sender;
if (stakersInfos[user].tokensStaked == 0) {
stakersInfos[user].accumulatedRewardsPerToken = accumulatedRewardsPerToken;
} else {
_claim(user);
}
taxFarmingToken.transferFrom(user, address(this), amount);
totalStaked += amount;
stakersInfos[user].tokensStaked += amount;
emit TokenStaked(user, amount, accumulatedRewardsPerToken);
}
function unstake(uint256 amount) external blockUser(msg.sender) {
address user = msg.sender;
require(block.timestamp >= lastClaimTime[user] + LOCK_PERIOD, "Locked for 7 days after last claim");
if (amount == 0 || stakersInfos[user].tokensStaked == 0) return;
require(stakersInfos[user].tokensStaked >= amount, "Not enough staked tokens");
stakersInfos[user].tokensStaked -= amount;
totalStaked -= amount;
taxFarmingToken.transfer(user, amount);
emit TokenUnstaked(user, amount, accumulatedRewardsPerToken);
}
function claim() external blockUser(msg.sender) {
_claim(msg.sender);
}
function _claim(address user) private {
uint256 rewards = getUserRewards(user);
stakersInfos[user].accumulatedRewardsPerToken = accumulatedRewardsPerToken;
if (rewards == 0) return;
(bool success,) = payable(user).call{value: rewards}("");
require(success, "Unable to claim rewards");
lastClaimTime[user] = block.timestamp;
}
function getUserRewards(address user) public view returns (uint256) {
uint256 rewardsPerToken = accumulatedRewardsPerToken - stakersInfos[user].accumulatedRewardsPerToken;
uint256 rewards = (rewardsPerToken * stakersInfos[user].tokensStaked) / DECIMALS;
return rewards;
}
receive() external payable {
require(totalStaked != 0, "No stakers");
uint256 rewardsPerToken = (msg.value * DECIMALS) / totalStaked;
accumulatedRewardsPerToken += rewardsPerToken;
totalRewards += msg.value;
}
}
{
"compilationTarget": {
"src/TokenStaking.sol": "TokenStaking"
},
"evmVersion": "shanghai",
"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/"
]
}