-
Notifications
You must be signed in to change notification settings - Fork 87
/
StableJoeStaking.sol
353 lines (303 loc) Β· 14.4 KB
/
StableJoeStaking.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol";
/**
* @title Stable JOE Staking
* @author Trader Joe
* @notice StableJoeStaking is a contract that allows JOE deposits and receives stablecoins sent by MoneyMaker's daily
* harvests. Users deposit JOE and receive a share of what has been sent by MoneyMaker based on their participation of
* the total deposited JOE. It is similar to a MasterChef, but we allow for claiming of different reward tokens
* (in case at some point we wish to change the stablecoin rewarded).
* Every time `updateReward(token)` is called, We distribute the balance of that tokens as rewards to users that are
* currently staking inside this contract, and they can claim it using `withdraw(0)`
*/
contract StableJoeStaking is Initializable, OwnableUpgradeable {
using SafeMathUpgradeable for uint256;
using SafeERC20Upgradeable for IERC20Upgradeable;
/// @notice Info of each user
struct UserInfo {
uint256 amount;
mapping(IERC20Upgradeable => uint256) rewardDebt;
/**
* @notice We do some fancy math here. Basically, any point in time, the amount of JOEs
* entitled to a user but is pending to be distributed is:
*
* pending reward = (user.amount * accRewardPerShare) - user.rewardDebt[token]
*
* Whenever a user deposits or withdraws JOE. Here's what happens:
* 1. accRewardPerShare (and `lastRewardBalance`) gets updated
* 2. User receives the pending reward sent to his/her address
* 3. User's `amount` gets updated
* 4. User's `rewardDebt[token]` gets updated
*/
}
IERC20Upgradeable public joe;
/// @dev Internal balance of JOE, this gets updated on user deposits / withdrawals
/// this allows to reward users with JOE
uint256 public internalJoeBalance;
/// @notice Array of tokens that users can claim
IERC20Upgradeable[] public rewardTokens;
mapping(IERC20Upgradeable => bool) public isRewardToken;
/// @notice Last reward balance of `token`
mapping(IERC20Upgradeable => uint256) public lastRewardBalance;
address public feeCollector;
/// @notice The deposit fee, scaled to `DEPOSIT_FEE_PERCENT_PRECISION`
uint256 public depositFeePercent;
/// @notice The precision of `depositFeePercent`
uint256 public DEPOSIT_FEE_PERCENT_PRECISION;
/// @notice Accumulated `token` rewards per share, scaled to `ACC_REWARD_PER_SHARE_PRECISION`
mapping(IERC20Upgradeable => uint256) public accRewardPerShare;
/// @notice The precision of `accRewardPerShare`
uint256 public ACC_REWARD_PER_SHARE_PRECISION;
/// @dev Info of each user that stakes JOE
mapping(address => UserInfo) private userInfo;
/// @notice Emitted when a user deposits JOE
event Deposit(address indexed user, uint256 amount, uint256 fee);
/// @notice Emitted when owner changes the deposit fee percentage
event DepositFeeChanged(uint256 newFee, uint256 oldFee);
/// @notice Emitted when a user withdraws JOE
event Withdraw(address indexed user, uint256 amount);
/// @notice Emitted when a user claims reward
event ClaimReward(address indexed user, address indexed rewardToken, uint256 amount);
/// @notice Emitted when a user emergency withdraws its JOE
event EmergencyWithdraw(address indexed user, uint256 amount);
/// @notice Emitted when owner adds a token to the reward tokens list
event RewardTokenAdded(address token);
/// @notice Emitted when owner removes a token from the reward tokens list
event RewardTokenRemoved(address token);
/**
* @notice Initialize a new StableJoeStaking contract
* @dev This contract needs to receive an ERC20 `_rewardToken` in order to distribute them
* (with MoneyMaker in our case)
* @param _rewardToken The address of the ERC20 reward token
* @param _joe The address of the JOE token
* @param _feeCollector The address where deposit fees will be sent
* @param _depositFeePercent The deposit fee percent, scalled to 1e18, e.g. 3% is 3e16
*/
function initialize(
IERC20Upgradeable _rewardToken,
IERC20Upgradeable _joe,
address _feeCollector,
uint256 _depositFeePercent
) external initializer {
__Ownable_init();
require(address(_rewardToken) != address(0), "StableJoeStaking: reward token can't be address(0)");
require(address(_joe) != address(0), "StableJoeStaking: joe can't be address(0)");
require(_feeCollector != address(0), "StableJoeStaking: fee collector can't be address(0)");
require(_depositFeePercent <= 5e17, "StableJoeStaking: max deposit fee can't be greater than 50%");
joe = _joe;
depositFeePercent = _depositFeePercent;
feeCollector = _feeCollector;
isRewardToken[_rewardToken] = true;
rewardTokens.push(_rewardToken);
DEPOSIT_FEE_PERCENT_PRECISION = 1e18;
ACC_REWARD_PER_SHARE_PRECISION = 1e24;
}
/**
* @notice Deposit JOE for reward token allocation
* @param _amount The amount of JOE to deposit
*/
function deposit(uint256 _amount) external {
UserInfo storage user = userInfo[_msgSender()];
uint256 _fee = _amount.mul(depositFeePercent).div(DEPOSIT_FEE_PERCENT_PRECISION);
uint256 _amountMinusFee = _amount.sub(_fee);
uint256 _previousAmount = user.amount;
uint256 _newAmount = user.amount.add(_amountMinusFee);
user.amount = _newAmount;
uint256 _len = rewardTokens.length;
for (uint256 i; i < _len; i++) {
IERC20Upgradeable _token = rewardTokens[i];
updateReward(_token);
uint256 _previousRewardDebt = user.rewardDebt[_token];
user.rewardDebt[_token] = _newAmount.mul(accRewardPerShare[_token]).div(ACC_REWARD_PER_SHARE_PRECISION);
if (_previousAmount != 0) {
uint256 _pending = _previousAmount
.mul(accRewardPerShare[_token])
.div(ACC_REWARD_PER_SHARE_PRECISION)
.sub(_previousRewardDebt);
if (_pending != 0) {
safeTokenTransfer(_token, _msgSender(), _pending);
emit ClaimReward(_msgSender(), address(_token), _pending);
}
}
}
internalJoeBalance = internalJoeBalance.add(_amountMinusFee);
joe.safeTransferFrom(_msgSender(), feeCollector, _fee);
joe.safeTransferFrom(_msgSender(), address(this), _amountMinusFee);
emit Deposit(_msgSender(), _amountMinusFee, _fee);
}
/**
* @notice Get user info
* @param _user The address of the user
* @param _rewardToken The address of the reward token
* @return The amount of JOE user has deposited
* @return The reward debt for the chosen token
*/
function getUserInfo(address _user, IERC20Upgradeable _rewardToken) external view returns (uint256, uint256) {
UserInfo storage user = userInfo[_user];
return (user.amount, user.rewardDebt[_rewardToken]);
}
/**
* @notice Get the number of reward tokens
* @return The length of the array
*/
function rewardTokensLength() external view returns (uint256) {
return rewardTokens.length;
}
/**
* @notice Add a reward token
* @param _rewardToken The address of the reward token
*/
function addRewardToken(IERC20Upgradeable _rewardToken) external onlyOwner {
require(
!isRewardToken[_rewardToken] && address(_rewardToken) != address(0),
"StableJoeStaking: token can't be added"
);
require(rewardTokens.length < 25, "StableJoeStaking: list of token too big");
rewardTokens.push(_rewardToken);
isRewardToken[_rewardToken] = true;
updateReward(_rewardToken);
emit RewardTokenAdded(address(_rewardToken));
}
/**
* @notice Remove a reward token
* @param _rewardToken The address of the reward token
*/
function removeRewardToken(IERC20Upgradeable _rewardToken) external onlyOwner {
require(isRewardToken[_rewardToken], "StableJoeStaking: token can't be removed");
updateReward(_rewardToken);
isRewardToken[_rewardToken] = false;
uint256 _len = rewardTokens.length;
for (uint256 i; i < _len; i++) {
if (rewardTokens[i] == _rewardToken) {
rewardTokens[i] = rewardTokens[_len - 1];
rewardTokens.pop();
break;
}
}
emit RewardTokenRemoved(address(_rewardToken));
}
/**
* @notice Set the deposit fee percent
* @param _depositFeePercent The new deposit fee percent
*/
function setDepositFeePercent(uint256 _depositFeePercent) external onlyOwner {
require(_depositFeePercent <= 5e17, "StableJoeStaking: deposit fee can't be greater than 50%");
uint256 oldFee = depositFeePercent;
depositFeePercent = _depositFeePercent;
emit DepositFeeChanged(_depositFeePercent, oldFee);
}
/**
* @notice View function to see pending reward token on frontend
* @param _user The address of the user
* @param _token The address of the token
* @return `_user`'s pending reward token
*/
function pendingReward(address _user, IERC20Upgradeable _token) external view returns (uint256) {
require(isRewardToken[_token], "StableJoeStaking: wrong reward token");
UserInfo storage user = userInfo[_user];
uint256 _totalJoe = internalJoeBalance;
uint256 _accRewardTokenPerShare = accRewardPerShare[_token];
uint256 _currRewardBalance = _token.balanceOf(address(this));
uint256 _rewardBalance = _token == joe ? _currRewardBalance.sub(_totalJoe) : _currRewardBalance;
if (_rewardBalance != lastRewardBalance[_token] && _totalJoe != 0) {
uint256 _accruedReward = _rewardBalance.sub(lastRewardBalance[_token]);
_accRewardTokenPerShare = _accRewardTokenPerShare.add(
_accruedReward.mul(ACC_REWARD_PER_SHARE_PRECISION).div(_totalJoe)
);
}
return
user.amount.mul(_accRewardTokenPerShare).div(ACC_REWARD_PER_SHARE_PRECISION).sub(user.rewardDebt[_token]);
}
/**
* @notice Withdraw JOE and harvest the rewards
* @param _amount The amount of JOE to withdraw
*/
function withdraw(uint256 _amount) external {
UserInfo storage user = userInfo[_msgSender()];
uint256 _previousAmount = user.amount;
require(_amount <= _previousAmount, "StableJoeStaking: withdraw amount exceeds balance");
uint256 _newAmount = user.amount.sub(_amount);
user.amount = _newAmount;
uint256 _len = rewardTokens.length;
if (_previousAmount != 0) {
for (uint256 i; i < _len; i++) {
IERC20Upgradeable _token = rewardTokens[i];
updateReward(_token);
uint256 _pending = _previousAmount
.mul(accRewardPerShare[_token])
.div(ACC_REWARD_PER_SHARE_PRECISION)
.sub(user.rewardDebt[_token]);
user.rewardDebt[_token] = _newAmount.mul(accRewardPerShare[_token]).div(ACC_REWARD_PER_SHARE_PRECISION);
if (_pending != 0) {
safeTokenTransfer(_token, _msgSender(), _pending);
emit ClaimReward(_msgSender(), address(_token), _pending);
}
}
}
internalJoeBalance = internalJoeBalance.sub(_amount);
joe.safeTransfer(_msgSender(), _amount);
emit Withdraw(_msgSender(), _amount);
}
/**
* @notice Withdraw without caring about rewards. EMERGENCY ONLY
*/
function emergencyWithdraw() external {
UserInfo storage user = userInfo[_msgSender()];
uint256 _amount = user.amount;
user.amount = 0;
uint256 _len = rewardTokens.length;
for (uint256 i; i < _len; i++) {
IERC20Upgradeable _token = rewardTokens[i];
user.rewardDebt[_token] = 0;
}
joe.safeTransfer(_msgSender(), _amount);
emit EmergencyWithdraw(_msgSender(), _amount);
}
/**
* @notice Update reward variables
* @param _token The address of the reward token
* @dev Needs to be called before any deposit or withdrawal
*/
function updateReward(IERC20Upgradeable _token) public {
require(isRewardToken[_token], "StableJoeStaking: wrong reward token");
uint256 _totalJoe = internalJoeBalance;
uint256 _currRewardBalance = _token.balanceOf(address(this));
uint256 _rewardBalance = _token == joe ? _currRewardBalance.sub(_totalJoe) : _currRewardBalance;
// Did StableJoeStaking receive any token
if (_rewardBalance == lastRewardBalance[_token] || _totalJoe == 0) {
return;
}
uint256 _accruedReward = _rewardBalance.sub(lastRewardBalance[_token]);
accRewardPerShare[_token] = accRewardPerShare[_token].add(
_accruedReward.mul(ACC_REWARD_PER_SHARE_PRECISION).div(_totalJoe)
);
lastRewardBalance[_token] = _rewardBalance;
}
/**
* @notice Safe token transfer function, just in case if rounding error
* causes pool to not have enough reward tokens
* @param _token The address of then token to transfer
* @param _to The address that will receive `_amount` `rewardToken`
* @param _amount The amount to send to `_to`
*/
function safeTokenTransfer(
IERC20Upgradeable _token,
address _to,
uint256 _amount
) internal {
uint256 _currRewardBalance = _token.balanceOf(address(this));
uint256 _rewardBalance = _token == joe ? _currRewardBalance.sub(internalJoeBalance) : _currRewardBalance;
if (_amount > _rewardBalance) {
lastRewardBalance[_token] = lastRewardBalance[_token].sub(_rewardBalance);
_token.safeTransfer(_to, _rewardBalance);
} else {
lastRewardBalance[_token] = lastRewardBalance[_token].sub(_amount);
_token.safeTransfer(_to, _amount);
}
}
}