-
Notifications
You must be signed in to change notification settings - Fork 0
/
AfEth.sol
302 lines (260 loc) · 10.7 KB
/
AfEth.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
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "contracts/strategies/votium/VotiumStrategy.sol";
import "contracts/external_interfaces/IVotiumStrategy.sol";
import "contracts/strategies/AbstractStrategy.sol";
// AfEth is the strategy manager for safEth and votium strategies
contract AfEth is Initializable, OwnableUpgradeable, ERC20Upgradeable {
uint256 public ratio;
uint256 public protocolFee;
address public feeAddress;
address public constant SAF_ETH_ADDRESS =
0x6732Efaf6f39926346BeF8b821a04B6361C4F3e5;
address public vEthAddress; // Votium Strategy Address
uint256 public latestWithdrawId;
struct WithdrawInfo {
address owner;
uint256 amount;
uint256 safEthWithdrawAmount;
uint256 vEthWithdrawId;
uint256 withdrawTime;
}
mapping(uint256 => WithdrawInfo) public withdrawIdInfo;
bool public pauseDeposit;
bool public pauseWithdraw;
error StrategyAlreadyAdded();
error StrategyNotFound();
error InsufficientBalance();
error InvalidStrategy();
error InvalidFee();
error CanNotWithdraw();
error NotOwner();
error FailedToSend();
error FailedToDeposit();
error Paused();
error BelowMinOut();
event WithdrawRequest(
address indexed account,
uint256 amount,
uint256 withdrawId,
uint256 withdrawTime
);
address private constant CVX_ADDRESS =
0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B;
address private constant VLCVX_ADDRESS =
0x72a19342e8F1838460eBFCCEf09F6585e32db86E;
uint256 public pendingSafEthWithdraws;
modifier onlyWithdrawIdOwner(uint256 withdrawId) {
if (withdrawIdInfo[withdrawId].owner != msg.sender) revert NotOwner();
_;
}
// As recommended by https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
/**
@notice - Initialize values for the contracts
@dev - This replaces the constructor for upgradeable contracts
*/
function initialize() external initializer {
_transferOwnership(msg.sender);
ratio = 5e17;
}
/**
* @notice - Sets the strategy addresses for safEth and votium
* @param _vEthAddress - vEth strategy address
*/
function setStrategyAddress(address _vEthAddress) external onlyOwner {
vEthAddress = _vEthAddress;
}
/**
@notice - Sets the target ratio of safEth to votium.
@notice target ratio is maintained by directing rewards into either safEth or votium strategy
@param _newRatio - New ratio of safEth to votium
*/
function setRatio(uint256 _newRatio) public onlyOwner {
ratio = _newRatio;
}
/**
@notice - Sets the protocol fee address which takes a percentage of the rewards.
@param _newFeeAddress - New protocol fee address to collect rewards
*/
function setFeeAddress(address _newFeeAddress) public onlyOwner {
feeAddress = _newFeeAddress;
}
/**
@notice - Sets the protocol fee which takes a percentage of the rewards.
@param _newFee - New protocol fee
*/
function setProtocolFee(uint256 _newFee) public onlyOwner {
if (_newFee > 1e18) revert InvalidFee();
protocolFee = _newFee;
}
/**
@notice - Enables/Disables depositing
@param _pauseDeposit - Bool to set pauseDeposit
*/
function setPauseDeposit(bool _pauseDeposit) external onlyOwner {
pauseDeposit = _pauseDeposit;
}
/**
@notice - Enables/Disables withdrawing & requesting to withdraw
@param _pauseWithdraw - Bool to set pauseWithdraw
*/
function setPauseWithdraw(bool _pauseWithdraw) external onlyOwner {
pauseWithdraw = _pauseWithdraw;
}
/**
@notice - Get's the price of afEth
@dev - Checks each strategy and calculates the total value in ETH divided by supply of afETH tokens
@return - Price of afEth
*/
function price() public view returns (uint256) {
if (totalSupply() == 0) return 1e18;
AbstractStrategy vEthStrategy = AbstractStrategy(vEthAddress);
uint256 safEthValueInEth = (ISafEth(SAF_ETH_ADDRESS).approxPrice(true) *
safEthBalanceMinusPending()) / 1e18;
uint256 vEthValueInEth = (vEthStrategy.price() *
vEthStrategy.balanceOf(address(this))) / 1e18;
return ((vEthValueInEth + safEthValueInEth) * 1e18) / totalSupply();
}
/**
@notice - Deposits into each strategy
@dev - This is the entry into the protocol
@param _minout - Minimum amount of afEth to mint
*/
function deposit(uint256 _minout) external payable virtual {
if (pauseDeposit) revert Paused();
uint256 amount = msg.value;
uint256 priceBeforeDeposit = price();
uint256 totalValue;
AbstractStrategy vStrategy = AbstractStrategy(vEthAddress);
uint256 sValue = (amount * ratio) / 1e18;
uint256 sMinted = sValue > 0
? ISafEth(SAF_ETH_ADDRESS).stake{value: sValue}(0)
: 0;
uint256 vValue = (amount * (1e18 - ratio)) / 1e18;
uint256 vMinted = vValue > 0 ? vStrategy.deposit{value: vValue}() : 0;
totalValue +=
(sMinted * ISafEth(SAF_ETH_ADDRESS).approxPrice(true)) +
(vMinted * vStrategy.price());
if (totalValue == 0) revert FailedToDeposit();
uint256 amountToMint = totalValue / priceBeforeDeposit;
if (amountToMint < _minout) revert BelowMinOut();
_mint(msg.sender, amountToMint);
}
/**
@notice - Request to close position
@param _amount - Amount of afEth to withdraw
*/
function requestWithdraw(uint256 _amount) external virtual {
uint256 withdrawTimeBefore = withdrawTime(_amount);
if (pauseWithdraw) revert Paused();
latestWithdrawId++;
// ratio of afEth being withdrawn to totalSupply
// we are transfering the afEth to the contract when we requestWithdraw
// we shouldn't include that in the withdrawRatio
uint256 afEthBalance = balanceOf(address(this));
uint256 withdrawRatio = (_amount * 1e18) /
(totalSupply() - afEthBalance);
_transfer(msg.sender, address(this), _amount);
uint256 votiumBalance = IERC20(vEthAddress).balanceOf(address(this));
uint256 votiumWithdrawAmount = (withdrawRatio * votiumBalance) / 1e18;
uint256 vEthWithdrawId = AbstractStrategy(vEthAddress).requestWithdraw(
votiumWithdrawAmount
);
uint256 safEthBalance = safEthBalanceMinusPending();
uint256 safEthWithdrawAmount = (withdrawRatio * safEthBalance) / 1e18;
pendingSafEthWithdraws += safEthWithdrawAmount;
withdrawIdInfo[latestWithdrawId]
.safEthWithdrawAmount = safEthWithdrawAmount;
withdrawIdInfo[latestWithdrawId].vEthWithdrawId = vEthWithdrawId;
withdrawIdInfo[latestWithdrawId].owner = msg.sender;
withdrawIdInfo[latestWithdrawId].amount = _amount;
withdrawIdInfo[latestWithdrawId].withdrawTime = withdrawTimeBefore;
emit WithdrawRequest(
msg.sender,
_amount,
latestWithdrawId,
withdrawTimeBefore
);
}
/**
@notice - Checks if withdraw can be executed from withdrawId
@param _withdrawId - Id of the withdraw request for SafEth
@return - Bool if withdraw can be executed
*/
function canWithdraw(uint256 _withdrawId) public view returns (bool) {
return
AbstractStrategy(vEthAddress).canWithdraw(
withdrawIdInfo[_withdrawId].vEthWithdrawId
);
}
/**
@notice - Get's the withdraw time for an amount of AfEth
@param _amount - Amount of afETH to withdraw
@return - Highest withdraw time of the strategies
*/
function withdrawTime(uint256 _amount) public view returns (uint256) {
return AbstractStrategy(vEthAddress).withdrawTime(_amount);
}
/**
@notice - Withdraw from each strategy
@param _withdrawId - Id of the withdraw request
@param _minout - Minimum amount of ETH to receive
*/
function withdraw(
uint256 _withdrawId,
uint256 _minout
) external virtual onlyWithdrawIdOwner(_withdrawId) {
if (pauseWithdraw) revert Paused();
uint256 ethBalanceBefore = address(this).balance;
WithdrawInfo memory withdrawInfo = withdrawIdInfo[_withdrawId];
if (!canWithdraw(_withdrawId)) revert CanNotWithdraw();
ISafEth(SAF_ETH_ADDRESS).unstake(withdrawInfo.safEthWithdrawAmount, 0);
AbstractStrategy(vEthAddress).withdraw(withdrawInfo.vEthWithdrawId);
_burn(address(this), withdrawIdInfo[_withdrawId].amount);
uint256 ethBalanceAfter = address(this).balance;
uint256 ethReceived = ethBalanceAfter - ethBalanceBefore;
pendingSafEthWithdraws -= withdrawInfo.safEthWithdrawAmount;
if (ethReceived < _minout) revert BelowMinOut();
// solhint-disable-next-line
(bool sent, ) = msg.sender.call{value: ethReceived}("");
if (!sent) revert FailedToSend();
}
/**
* @notice - sells _amount of eth from votium contract
* @dev - puts it into safEthStrategy or votiumStrategy, whichever is underweight.\
* @param _amount - amount of eth to sell
*/
function depositRewards(uint256 _amount) public payable {
IVotiumStrategy votiumStrategy = IVotiumStrategy(vEthAddress);
uint256 feeAmount = (_amount * protocolFee) / 1e18;
if (feeAmount > 0) {
// solhint-disable-next-line
(bool sent, ) = feeAddress.call{value: feeAmount}("");
if (!sent) revert FailedToSend();
}
uint256 amount = _amount - feeAmount;
uint256 safEthTvl = (ISafEth(SAF_ETH_ADDRESS).approxPrice(true) *
safEthBalanceMinusPending()) / 1e18;
uint256 votiumTvl = ((votiumStrategy.cvxPerVotium() *
votiumStrategy.ethPerCvx(true)) *
IERC20(vEthAddress).balanceOf(address(this))) / 1e36;
uint256 totalTvl = (safEthTvl + votiumTvl);
uint256 safEthRatio = (safEthTvl * 1e18) / totalTvl;
if (safEthRatio < ratio) {
ISafEth(SAF_ETH_ADDRESS).stake{value: amount}(0);
} else {
votiumStrategy.depositRewards{value: amount}(amount);
}
}
function safEthBalanceMinusPending() public view returns (uint256) {
return
IERC20(SAF_ETH_ADDRESS).balanceOf(address(this)) -
pendingSafEthWithdraws;
}
receive() external payable {}
}