1+ // SPDX-License-Identifier: GPL-3.0-only
2+ pragma solidity ^ 0.8.20 ;
3+
4+ import { ERC721 } from "openzeppelin-contracts/token/ERC721/ERC721.sol " ;
5+ import { SafeERC20, IERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol " ;
6+ import { Ownable } from "openzeppelin-contracts/access/Ownable.sol " ;
7+
8+ contract EnsoStaking is ERC721 , Ownable {
9+ using SafeERC20 for IERC20 ;
10+
11+ IERC20 public immutable token;
12+ uint256 public nextPositionId;
13+
14+ uint256 maxMultiplier;
15+ uint256 maxRange;
16+ uint64 maxPeriod;
17+ uint64 minPeriod;
18+
19+ mapping (uint256 => Position) public positions;
20+ mapping (address => uint256 ) public stakes; // depositor => stake // TODO: purpose of this is informational, but maybe it could be used for voting in future? if so, should have a timestamp checkpoint
21+ mapping (address => uint256 ) public delegateStakes; // delegate => stake
22+ mapping (address => uint256 ) public rewardsPerStake; // delegate => value per stake;
23+ mapping (address => uint256 ) public totalRewards; // delegate => rewards earned;
24+
25+ uint256 private PRECISION = 10 ** 18 ;
26+
27+ struct Position {
28+ uint64 expiry;
29+ address delegate;
30+ uint256 deposit;
31+ uint256 stake;
32+ uint256 rewardsCheckpoint;
33+ }
34+
35+ event NewPosition (uint256 positionId , uint64 expiry , address delegate );
36+ event Deposit (uint256 positionId , uint256 depositAdded , uint256 stakeAdded );
37+ event Redeem (uint256 positionId , uint256 depositRemoved , uint256 stakeRemoved );
38+
39+
40+ constructor (address _owner , uint64 _minPeriod , uint64 _maxPeriod , uint256 _maxMulitplier ) Ownable (_owner) {
41+ minPeriod = _minPeriod;
42+ maxPeriod = _maxPeriod;
43+ maxRange = _maxPeriod - _minPeriod;
44+ maxMultiplier = _maxMulitplier;
45+ }
46+
47+ function createPosition (uint256 deposit , uint64 period , address receiver , address delegate ) external {
48+ if (period > maxPeriod || period < minPeriod) revert InvalidStakingPeriod (period);
49+ if (! validators[delegate]) revert NotValidator (delegate);
50+ token.safeTransferFrom (msg .sender , address (this ), deposit);
51+ uint256 range = period - minPeriod;
52+ uint256 stake = deposit + (deposit * maxMultiplier * range / maxRange);
53+ uint64 expiry = block .timestamp + period;
54+ uint256 positionId = nextPositionId;
55+ nextPositionId++ ;
56+ _mint (receiver, positionId);
57+ positions[positionId] = Position (expiry, delegate, deposit, stake, rewardsPerStake[delegate]);
58+ stakes[receiver] += stake;
59+ delegatedStakes[delegate] += stake;
60+ emit NewPosition (positionId, expiry);
61+ emit Deposit (positionId, deposit, stake);
62+ }
63+
64+ function deposit (uint256 positionId , uint256 amount ) public {
65+ collectRewards (positionId);
66+
67+ address account = _ownerOf (positionId);
68+ Position storage position = positions[positionId];
69+
70+ uint64 timestamp = block .timestamp ;
71+ uint256 stake;
72+ // multiplier is only applied if the period left til expiry is more than the min period
73+ if (position.expiry <= (timestamp + minPeriod)) {
74+ stake = amount;
75+ } else {
76+ uint256 period = position.expiry - timestamp;
77+ uint256 range = period - minPeriod;
78+ stake = amount + (amount * maxMultiplier * range / maxRange);
79+ }
80+ position.stake += stake;
81+ position.deposit += amount;
82+ stakes[account] += stake;
83+ delegatedStakes[position.delegate] += stake;
84+ emit Deposit (positionId, amount, stake);
85+ }
86+
87+ function redeem (uint256 positionId , uint256 amount , address receiver ) external {
88+ collectRewards (positionId);
89+
90+ address account = _ownerOf (positionId);
91+ if (msg .sender != account) revert InvalidSender (account, msg .sender );
92+
93+ Position storage position = positions[positionId];
94+ if (block .timestamp < position.expiry) revert PositionNotExpired (position.expiry, block .timestamp );
95+ if (amount > position.stake) revert InsufficientStake (position.stake, amount);
96+
97+ uint256 withdraw = position.deposit * amount / position.stake;
98+ position.stake -= amount;
99+ position.deposit -= withdraw;
100+ stakes[account] -= amount;
101+ delegatedStakes[position.delegate] -= amount;
102+ token.safeTransfer (receiver, withdraw);
103+ emit Redeem (positionId, withdraw, amount);
104+ }
105+
106+ function issueRewards (address delegate , uint256 amount ) external {
107+ token.safeTransferFrom (msg .sender , address (this ), amount);
108+ rewardsPerStake[delegate] += amount * PRECISION / delegateStakes[delegate];
109+ totalRewards[delegate] += amount;
110+ }
111+
112+ // TODO: here we are sending rewards to the user, but instead do we want to keep funds on contract and update a mapping?
113+ // this way we could claim from many positions and only transfer once, or claim and reinvest. this goes for deposit and
114+ // redeem too. do we just want to claim rewards without collecting them?
115+ function collectRewards (uint256 positionId ) public {
116+ if (positionId >= nextPositionId) revert InvalidPositionId (positionId);
117+ Position storage position = positions[positionId];
118+ uint256 rewards = _availableRewards (position);
119+ position.rewardsCheckpoint = rewardsPerStake[position.delegate];
120+ address account = _ownerOf (positionId);
121+ token.safeTransfer (account, rewards);
122+ emit RewardsCollected (positionId, account, rewards);
123+ }
124+
125+ function availableRewards (uint256 positionId ) external view returns (uint256 rewards ) {
126+ Position memory position = positions[positionId];
127+ rewards = _availableRewards (position);
128+ }
129+
130+ function _availableRewards (Position memory position ) internal view returns (uint256 rewards ) {
131+ uint256 rewardDiff = rewardsPerStake[position.delegate] - position.rewardsCheckpoint;
132+ rewards = rewardDiff * position.stake / PRECISION;
133+ }
134+ }
0 commit comments