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