SPCX INDEX
SPCXStakingRewards.sol· solidity
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.20;
3
4import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5import "@openzeppelin/contracts/access/Ownable.sol";
6
7contract SPCXStakingRewards is Ownable {
8    IERC20 public immutable stakingToken;
9    IERC20 public immutable rewardToken;
10
11    uint256 public totalStaked;
12    uint256 public accRewardPerShare;
13
14    mapping(address => uint256) public staked;
15    mapping(address => uint256) public rewardDebt;
16
17    address public keeper;
18
19    event Staked(address indexed user, uint256 amount);
20    event Unstaked(address indexed user, uint256 amount);
21    event Claimed(address indexed user, uint256 amount);
22    event ProfitDistributed(uint256 amount);
23    event KeeperUpdated(address keeper);
24
25    modifier onlyKeeper() {
26        require(msg.sender == keeper, "Not keeper");
27        _;
28    }
29
30    constructor(
31        address _stakingToken,
32        address _rewardToken,
33        address _keeper
34    ) Ownable(msg.sender) {
35        stakingToken = IERC20(_stakingToken);
36        rewardToken = IERC20(_rewardToken);
37        keeper = _keeper;
38    }
39
40    function setKeeper(address _keeper) external onlyOwner {
41        keeper = _keeper;
42        emit KeeperUpdated(_keeper);
43    }
44
45    function stake(uint256 amount) external {
46        require(amount > 0, "Amount zero");
47
48        _claim(msg.sender);
49
50        stakingToken.transferFrom(msg.sender, address(this), amount);
51
52        staked[msg.sender] += amount;
53        totalStaked += amount;
54
55        rewardDebt[msg.sender] =
56            (staked[msg.sender] * accRewardPerShare) / 1e18;
57
58        emit Staked(msg.sender, amount);
59    }
60
61    function unstake(uint256 amount) external {
62        require(amount > 0, "Amount zero");
63        require(staked[msg.sender] >= amount, "Too much");
64
65        _claim(msg.sender);
66
67        staked[msg.sender] -= amount;
68        totalStaked -= amount;
69
70        rewardDebt[msg.sender] =
71            (staked[msg.sender] * accRewardPerShare) / 1e18;
72
73        stakingToken.transfer(msg.sender, amount);
74
75        emit Unstaked(msg.sender, amount);
76    }
77
78    function claim() external {
79        _claim(msg.sender);
80
81        rewardDebt[msg.sender] =
82            (staked[msg.sender] * accRewardPerShare) / 1e18;
83    }
84
85    function distributeProfit(uint256 amount) external onlyKeeper {
86        require(amount > 0, "Amount zero");
87        require(totalStaked > 0, "No stakers");
88
89        rewardToken.transferFrom(msg.sender, address(this), amount);
90
91        accRewardPerShare += (amount * 1e18) / totalStaked;
92
93        emit ProfitDistributed(amount);
94    }
95
96    function pendingRewards(address user) public view returns (uint256) {
97        uint256 accumulated =
98            (staked[user] * accRewardPerShare) / 1e18;
99
100        if (accumulated < rewardDebt[user]) {
101            return 0;
102        }
103
104        return accumulated - rewardDebt[user];
105    }
106
107    function _claim(address user) internal {
108        uint256 reward = pendingRewards(user);
109
110        if (reward > 0) {
111            rewardToken.transfer(user, reward);
112            emit Claimed(user, reward);
113        }
114    }
115}