-
Notifications
You must be signed in to change notification settings - Fork 5
/
StakingRewardsManager.sol
279 lines (250 loc) · 11.4 KB
/
StakingRewardsManager.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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./StakingRewardsFactory.sol";
import "./StakingRewards.sol";
/**
* @title StakingRewardsManager
* @notice A Telcoin Contract
* @dev Implements Openzeppelin Audited Contracts
*
* @notice This contract can manage multiple Synthetix StakingRewards contracts.
* Staking contracts managed my multisigs can avoid having to coordinate to top up contracts every staking period.
* Instead, add staking contracts to this manager contract, approve this contract to spend the rewardToken and then topUp() can be called permissionlessly.
*/
contract StakingRewardsManager is AccessControlUpgradeable {
using SafeERC20 for IERC20;
/// @notice This role grants the ability to rescue ERC20 tokens that do not rightfully belong to this contract
bytes32 public constant BUILDER_ROLE = keccak256("BUILDER_ROLE");
bytes32 public constant MAINTAINER_ROLE = keccak256("MAINTAINER_ROLE");
bytes32 public constant SUPPORT_ROLE = keccak256("SUPPORT_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
/// @dev StakingRewards config
struct StakingConfig {
uint256 rewardsDuration;
uint256 rewardAmount;
}
/// @dev Reward token for all StakingRewards contracts managed by this contract
IERC20 public rewardToken;
/// @dev Optional factory contract for creating new StakingRewards contracts
StakingRewardsFactory public stakingRewardsFactory;
/// @dev Array of managed StakingRewards contracts
StakingRewards[] public stakingContracts;
/// @dev Maps a StakingReward contract to boolean indicating its existence in the stakingContracts array
mapping(StakingRewards => bool) public stakingExists;
/// @dev Maps a StakingReward contract to its configuration (rewardsDuration and rewardAmount)
mapping(StakingRewards => StakingConfig) public stakingConfigs;
/// @dev Emitted when an existing StakingRewards contract is added to the stakingContracts array
event StakingAdded(StakingRewards indexed staking, StakingConfig config);
/// @dev Emitted when a StakingRewards contract is removed from the stakingContracts array
event StakingRemoved(StakingRewards indexed staking);
/// @dev Emitted when configuration for a StakingRewards contract is changed
event StakingConfigChanged(
StakingRewards indexed staking,
StakingConfig config
);
/// @dev Emitted when the StakingRewards Factory contract is changed
event StakingRewardsFactoryChanged(
StakingRewardsFactory indexed stakingFactory
);
/// @dev Emitted when updatePeriodFinish is called on a StakingRewards contract
event PeriodFinishUpdated(
StakingRewards indexed staking,
uint256 newPeriodFinish
);
/// @dev Emitted when a StakingRewards contract is topped up
event ToppedUp(StakingRewards indexed staking, StakingConfig config);
/// @notice initialize the contract
/// @param reward The reward token of all the managed staking contracts
function initialize(
IERC20 reward,
StakingRewardsFactory factory
) external initializer {
//check for zero values
require(
address(factory) != address(0) && address(reward) != address(0),
"StakingRewardsManager: cannot intialize to zero"
);
// set up default
_grantRole(DEFAULT_ADMIN_ROLE, _msgSender());
// set values
rewardToken = reward;
stakingRewardsFactory = factory;
emit StakingRewardsFactoryChanged(factory);
}
/// @return length uint256 of stakingContracts array
function stakingContractsLength() external view returns (uint256) {
return stakingContracts.length;
}
/// @return length uint256 of stakingContracts array
function getStakingContract(
uint256 i
) external view returns (StakingRewards) {
return stakingContracts[i];
}
/// @notice Create a new StakingRewards contract via the factory and add it to the stakingContracts array of managed contracts
/// @param stakingToken Staking token for the new StakingRewards contract
/// @param config Staking configuration
function createNewStakingRewardsContract(
IERC20 stakingToken,
StakingConfig calldata config
) external onlyRole(BUILDER_ROLE) {
// create the new staking contract
// new staking will have owner and rewardsDistribution set to address(this)
StakingRewards staking = StakingRewards(
address(
stakingRewardsFactory.createStakingRewards(
address(this),
IERC20(address(rewardToken)),
IERC20(stakingToken)
)
)
);
//internal call to add new contract
_addStakingRewardsContract(staking, config);
}
/// @notice Add a StakingRewards contract
/// @dev This contract must be nominated for ownership before the staking contract can be added
/// If this contract cannot acceptOwnership of the staking contract this function will revert
/// This function WILL NOT REVERT if `staking` does not have the right rewardToken.
/// Do not add staking contracts with rewardToken other than the one passed to initialize this contract.
/// @param staking Address of the StakingRewards contract to add
/// @param config Configuration of the staking contracts
function addStakingRewardsContract(
StakingRewards staking,
StakingConfig calldata config
) external onlyRole(BUILDER_ROLE) {
//checking if already exists
require(
!stakingExists[staking],
"StakingRewardsManager: Staking contract already exists"
);
//internal call to add new contract
_addStakingRewardsContract(staking, config);
}
/// @notice Add a StakingRewards contract
/// @param staking Address of the StakingRewards contract to add
/// @param config Configuration of the staking contracts
function _addStakingRewardsContract(
StakingRewards staking,
StakingConfig calldata config
) internal {
// in order to manage this contract we have to own it
// staking.acceptOwnership();
// in order to top up rewards, we have to be rewardsDistribution. this is an onlyOwner function
staking.setRewardsDistribution(address(this));
// push staking onto stakingContracts array
stakingContracts.push(staking);
// set staking config
stakingConfigs[staking] = config;
// mark inclusion in the stakingContracts array
stakingExists[staking] = true;
emit StakingAdded(staking, config);
}
/// @notice Remove a StakingRewards contract from the stakingContracts array. This will remove this contract's ability to manage it
/// @dev This function WILL NOT transfer ownership of the staking contract. To do this, call `nominateOwnerForStaking`
/// @param i Index of staking contract to remove
function removeStakingRewardsContract(
uint256 i
) external onlyRole(BUILDER_ROLE) {
StakingRewards staking = stakingContracts[i];
// un-mark this staking contract as included in stakingContracts
stakingExists[staking] = false;
// replace the removed staking contract with the last item in the stakingContracts array
stakingContracts[i] = stakingContracts[stakingContracts.length - 1];
// pop the last staking contract off the array
stakingContracts.pop();
emit StakingRemoved(staking);
}
/// @notice Set the configuration for a StakingRewards contract
/// @dev `staking` does not need to be included in `stakingContracts` for this function to succeed
/// @param staking Address of StakingRewards contract
/// @param config Staking config
function setStakingConfig(
StakingRewards staking,
StakingConfig calldata config
) external onlyRole(MAINTAINER_ROLE) {
// replacing old value
stakingConfigs[staking] = config;
emit StakingConfigChanged(staking, config);
}
/// @notice Set the StakingRewards Factory contract
/// @dev Factory AND StakingRewards contracts must maintain their ABI
/// @param factory Address of StakingRewards Factory contract
function setStakingRewardsFactory(
StakingRewardsFactory factory
) external onlyRole(MAINTAINER_ROLE) {
//check for zero values
require(
address(factory) != address(0),
"StakingRewardsManager: Factory cannot be set to zero"
);
//set new value
stakingRewardsFactory = factory;
emit StakingRewardsFactoryChanged(factory);
}
/// @notice Recover ERC20 tokens from a StakingRewards contract
/// @dev This contract must own the staking contract
/// @param staking The staking contract to recover tokens from
/// @param tokenAddress Address of the ERC20 token contract
/// @param tokenAmount Amount of tokens to recover
/// @param to The account to send the recovered tokens to
function recoverERC20FromStaking(
StakingRewards staking,
IERC20 tokenAddress,
uint256 tokenAmount,
address to
) external onlyRole(SUPPORT_ROLE) {
// grab the tokens from the staking contract
staking.recoverERC20(to, tokenAddress, tokenAmount);
}
/// @notice Recover ERC20 tokens from THIS contract
/// @param tokenAddress Address of the ERC20 token contract
/// @param tokenAmount Amount of tokens to recover
/// @param to The account to send the recovered tokens to
function recoverERC20(
IERC20 tokenAddress,
uint256 tokenAmount,
address to
) external onlyRole(SUPPORT_ROLE) {
//move funds
tokenAddress.safeTransfer(to, tokenAmount);
}
/// @notice change ownership for a staking contract
/// @dev This contract must currently own the staking contract
/// @param staking The staking contract to transfer ownership of
/// @param newOwner Account of new owner
function transferStakingOwnership(
StakingRewards staking,
address newOwner
) external onlyRole(ADMIN_ROLE) {
//internal emit is called
staking.transferOwnership(newOwner);
}
/// @notice Top up multiple staking contracts
/// @param source address from which tokens are taken
/// @param indices array of staking contract indices
function topUp(
address source,
uint256[] memory indices
) external onlyRole(EXECUTOR_ROLE) {
for (uint i = 0; i < indices.length; i++) {
// get staking contract and config
StakingRewards staking = stakingContracts[i];
StakingConfig memory config = stakingConfigs[staking];
// will revert if block.timestamp <= periodFinish
staking.setRewardsDuration(config.rewardsDuration);
// pull tokens from owner of this contract to fund the staking contract
rewardToken.transferFrom(
source,
address(staking),
config.rewardAmount
);
// start periods
staking.notifyRewardAmount(config.rewardAmount);
emit ToppedUp(staking, config);
}
}
}