-
Notifications
You must be signed in to change notification settings - Fork 35
/
Copy pathDSCEngine.sol
424 lines (378 loc) · 16.5 KB
/
DSCEngine.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
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
// SPDX-License-Identifier: MIT
// Layout of Contract:
// version
// imports
// interfaces, libraries, contracts
// errors
// Type declarations
// State variables
// Events
// Modifiers
// Functions
// Layout of Functions:
// constructor
// receive function (if exists)
// fallback function (if exists)
// external
// public
// internal
// private
// view & pure functions
pragma solidity ^0.8.18;
import {DecentralizedStableCoin} from "./DecentralizedStableCoin.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import {OracleLib} from "./libraries/OracleLib.sol";
/**
* @title DSCEngine
* @author Patrick Collins
*
* The system is designed to be as minimal as possible, and have the tokens maintain a 1 token == $1 peg.
* This stablecoin has the properties:
* - Exogenous Collateral
* - Dollar Pegged
* - Algoritmically Stable
*
* It is similar to DAI if DAI had no governance, no fees, and was only backed by WETH and WBTC.
*
* Our DSC system should always be "overcollateralized". At no point, should the value of all collateral <= the $ backed value of all the DSC.
*
* @notice This contract is the core of the DSC System. It handles all the logic for mining and redeeming DSC, as well as depositing & withdrawing collateral.
* @notice This contract is VERY loosely based on the MakerDAO DSS (DAI) system.
*/
contract DSCEngine is ReentrancyGuard {
//////////////////
// Errors //
//////////////////
error DSCEngine__NeedsMoreThanZero();
error DSCEngine__TokenAddressesAndPriceFeedAddressesMustBeSameLength();
error DSCEngine__NotAllowedToken();
error DSCEngine__TransferFailed();
error DSCEngine__BreaksHealthFactor(uint256 healthFactor);
error DSCEngine__MintFailed();
error DSCEngine__HealthFactorOk();
error DSCEngine__HealthFactorNotImproved();
//////////////////
// Type //
//////////////////
using OracleLib for AggregatorV3Interface;
/////////////////////
// State Variables //
/////////////////////
uint256 private constant ADDITIONAL_FEED_PRECISION = 1e10;
uint256 private constant PRECISION = 1e18;
uint256 private constant LIQUIDATION_THRESHOLD = 50; // 200% overcollateralized
uint256 private constant LIQUIDATION_PRECISION = 100;
uint256 private constant MIN_HEALTH_FACTOR = 1e18;
uint256 private constant LIQUIDATION_BONUS = 10; // this means a 10% bonus
mapping(address token => address priceFeed) private s_priceFeeds; // tokenToPriceFeed
mapping(address user => mapping(address token => uint256 amount)) private s_collateralDeposited;
mapping(address user => uint256 amountDscMinted) private s_DSCMinted;
address[] private s_collateralTokens;
DecentralizedStableCoin private immutable i_dsc;
/////////////////////
// Events //
/////////////////////
event CollateralDeposited(address indexed user, address indexed token, uint256 indexed amount);
event CollateralRedeemed(
address indexed redeemedFrom, address indexed redeemedTo, address indexed token, uint256 amount
);
//////////////////
// Modifiers //
//////////////////
modifier moreThanZero(uint256 amount) {
if (amount == 0) {
revert DSCEngine__NeedsMoreThanZero();
}
_;
}
modifier isAllowedToken(address token) {
if (s_priceFeeds[token] == address(0)) {
revert DSCEngine__NotAllowedToken();
}
_;
}
//////////////////
// Functions //
//////////////////
constructor(address[] memory tokenAddresses, address[] memory priceFeedAddresses, address dscAddress) {
// USD Price Feeds
if (tokenAddresses.length != priceFeedAddresses.length) {
revert DSCEngine__TokenAddressesAndPriceFeedAddressesMustBeSameLength();
}
// For example ETH / USD, BTC / USD, MKR / USD, etc
for (uint256 i = 0; i < tokenAddresses.length; i++) {
s_priceFeeds[tokenAddresses[i]] = priceFeedAddresses[i];
s_collateralTokens.push(tokenAddresses[i]);
}
i_dsc = DecentralizedStableCoin(dscAddress);
}
///////////////////////////
// External Functions //
///////////////////////////
/*
* @param tokenCollateralAddress The address of the token to deposit as collateral
* @param amountCollateral The amount of collateral to deposit
* @param amountDscToMint The amount of decentralized stablecoin to mint
* @notice this function will deposit your collateral and mint DSC in one transaction
*/
function depositCollateralAndMintDsc(
address tokenCollateralAddress,
uint256 amountCollateral,
uint256 amountDscToMint
) external {
depositCollateral(tokenCollateralAddress, amountCollateral);
mintDsc(amountDscToMint);
}
/*
* @notice follows CEI
* @param tokenCollateralAddress The address of the token to deposit as collateral
* @param amountCollateral The amount of collateral to deposit
*/
function depositCollateral(address tokenCollateralAddress, uint256 amountCollateral)
public
moreThanZero(amountCollateral)
isAllowedToken(tokenCollateralAddress)
nonReentrant
{
s_collateralDeposited[msg.sender][tokenCollateralAddress] += amountCollateral;
emit CollateralDeposited(msg.sender, tokenCollateralAddress, amountCollateral);
bool success = IERC20(tokenCollateralAddress).transferFrom(msg.sender, address(this), amountCollateral);
if (!success) {
revert DSCEngine__TransferFailed();
}
}
/*
* @param tokenCollateralAddress The collateral address to redeem
* @param amountCollateral The amount of collateral to redeem
* @param amountDscToBurn The amount of DSC to burn
* This function burns DSC and redeems underlying collateral in one transaction
*/
function redeemCollateralForDsc(address tokenCollateralAddress, uint256 amountCollateral, uint256 amountDscToBurn)
external
{
burnDsc(amountDscToBurn);
redeemCollateral(tokenCollateralAddress, amountCollateral);
// redeemCollateral already checks health factor
}
/*
* @param tokenCollateralAddress: The ERC20 token address of the collateral you're redeeming
* @param amountCollateral: The amount of collateral you're redeeming
* @notice This function will redeem your collateral.
* @notice If you have DSC minted, you will not be able to redeem until you burn your DSC
*/
function redeemCollateral(address tokenCollateralAddress, uint256 amountCollateral)
public
moreThanZero(amountCollateral)
nonReentrant
{
_redeemCollateral(msg.sender, msg.sender, tokenCollateralAddress, amountCollateral);
_revertIfHealthFactorIsBroken(msg.sender);
}
/*
* @notice follows CEI
* @param amountDscToMint The amount of decentralized stablecoin to mint
* @notice they must have more collateral value than the minimum threshold
*/
function mintDsc(uint256 amountDscToMint) public moreThanZero(amountDscToMint) nonReentrant {
s_DSCMinted[msg.sender] += amountDscToMint;
// if they minted too much ($150 DSC, $100 ETH)
_revertIfHealthFactorIsBroken(msg.sender);
bool minted = i_dsc.mint(msg.sender, amountDscToMint);
if (!minted) {
revert DSCEngine__MintFailed();
}
}
/*
* @notice careful! You'll burn your DSC here! Make sure you want to do this...
* @dev you might want to use this if you're nervous you might get liquidated and want to just burn
* you DSC but keep your collateral in.
*/
function burnDsc(uint256 amount) public moreThanZero(amount) {
_burnDsc(amount, msg.sender, msg.sender);
_revertIfHealthFactorIsBroken(msg.sender); // I don't think this would ever hit...
}
/*
* @param collateral The erc20 collateral address to liquidate from the user
* @param user The user who has broken the health factor. Their _healthFactor should be below MIN_HEALTH_FACTOR
* @param debtToCover The amount of DSC you want to burn to improve the users health factor
* @notice You can partially liquidate a user.
* @notice You will get a liquidation bonus for taking the users funds
* @notice This function working assumes the protocol will be roughly 200% overcollateralized in order for this to work.
* @notice A known bug would be if the protocol were 100% or less collateralized, then we wouldn't be able to incentive the liquidators.
* For example, if the price of the collateral plummeted before anyone could be liquidated.
*
* Follows CEI: Checks, Effects, Interactions
*/
function liquidate(address collateral, address user, uint256 debtToCover)
external
moreThanZero(debtToCover)
nonReentrant
{
// need to check health factor of the user
uint256 startingUserHealthFactor = _healthFactor(user);
if (startingUserHealthFactor >= MIN_HEALTH_FACTOR) {
revert DSCEngine__HealthFactorOk();
}
// We want to burn their DSC "debt"
// And take their collateral
// Bad User: $140 ETH, $100 DSC
// debtToCover = $100
// $100 of DSC == ??? ETH?
// 0.05 ETH
uint256 tokenAmountFromDebtCovered = getTokenAmountFromUsd(collateral, debtToCover);
// And give them a 10% bonus
// So we are giving the liquidator $110 of WETH for 100 DSC
// We should implement a feature to liquidate in the event the protocol is insolvent
// And sweep extra amounts into a treasury
// 0.05 * 0.1 = 0.005. Getting 0.055
uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / LIQUIDATION_PRECISION;
uint256 totalCollateralToRedeem = tokenAmountFromDebtCovered + bonusCollateral;
_redeemCollateral(user, msg.sender, collateral, totalCollateralToRedeem);
// We need to burn the DSC
_burnDsc(debtToCover, user, msg.sender);
uint256 endingUserHealthFactor = _healthFactor(user);
if (endingUserHealthFactor <= startingUserHealthFactor) {
revert DSCEngine__HealthFactorNotImproved();
}
_revertIfHealthFactorIsBroken(msg.sender);
}
/////////////////////////////////////
// Private & Internal Functions //
/////////////////////////////////////
/*
* @dev Low-level internal function, do not call unless the function calling it is
* checking for health factors being broken
*/
function _burnDsc(uint256 amountDscToBurn, address onBehalfOf, address dscFrom) private {
s_DSCMinted[onBehalfOf] -= amountDscToBurn;
bool success = i_dsc.transferFrom(dscFrom, address(this), amountDscToBurn);
// This conditional is hypothtically unreachable
if (!success) {
revert DSCEngine__TransferFailed();
}
i_dsc.burn(amountDscToBurn);
}
function _redeemCollateral(address from, address to, address tokenCollateralAddress, uint256 amountCollateral)
private
{
s_collateralDeposited[from][tokenCollateralAddress] -= amountCollateral;
emit CollateralRedeemed(from, to, tokenCollateralAddress, amountCollateral);
bool success = IERC20(tokenCollateralAddress).transfer(to, amountCollateral);
if (!success) {
revert DSCEngine__TransferFailed();
}
}
/////////////////////////////////////
// Private & Internal View/Pure Functions //
/////////////////////////////////////
function _getAccountInformation(address user)
private
view
returns (uint256 totalDscMinted, uint256 collateralValueInUsd)
{
totalDscMinted = s_DSCMinted[user];
collateralValueInUsd = getAccountCollateralValue(user);
}
/*
* Returns how close to liquidation a user is
* If a user goes below 1, then they can get liquidated
*/
function _healthFactor(address user) private view returns (uint256) {
(uint256 totalDscMinted, uint256 collateralValueInUsd) = _getAccountInformation(user);
return _calculateHealthFactor(totalDscMinted, collateralValueInUsd);
}
// 1. Check health factor (do they have enough collateral?)
// 2. Revert if they don't
function _revertIfHealthFactorIsBroken(address user) internal view {
uint256 userHealthFactor = _healthFactor(user);
if (userHealthFactor < MIN_HEALTH_FACTOR) {
revert DSCEngine__BreaksHealthFactor(userHealthFactor);
}
}
function _calculateHealthFactor(uint256 totalDscMinted, uint256 collateralValueInUsd)
internal
pure
returns (uint256)
{
if (totalDscMinted == 0) return type(uint256).max;
uint256 collateralAdjustedForThreshold = (collateralValueInUsd * LIQUIDATION_THRESHOLD) / LIQUIDATION_PRECISION;
return (collateralAdjustedForThreshold * 1e18) / totalDscMinted;
}
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
// External & Public View & Pure Functions
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
function getTokenAmountFromUsd(address token, uint256 usdAmountInWei) public view returns (uint256) {
// price of ETH (token)
// $/ETH ETH ??
// $2000 / ETH. $1000 = 0.5 ETH
AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]);
(, int256 price,,,) = priceFeed.staleCheckLatestRoundData();
// ($10e18 * 1e18) / ($2000e8 * 1e10)
return (usdAmountInWei * PRECISION) / (uint256(price) * ADDITIONAL_FEED_PRECISION);
}
function getAccountCollateralValue(address user) public view returns (uint256 totalCollateralValueInUsd) {
// loop through each collateral token, get the amount they have deposited, and map it to
// the price, to get the USD value
for (uint256 i = 0; i < s_collateralTokens.length; i++) {
address token = s_collateralTokens[i];
uint256 amount = s_collateralDeposited[user][token];
totalCollateralValueInUsd += getUsdValue(token, amount);
}
return totalCollateralValueInUsd;
}
function getUsdValue(address token, uint256 amount) public view returns (uint256) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]);
(, int256 price,,,) = priceFeed.staleCheckLatestRoundData();
// 1 ETH = $1000
// The returned value from CL will be 1000 * 1e8
return ((uint256(price) * ADDITIONAL_FEED_PRECISION) * amount) / PRECISION;
}
function getAccountInformation(address user)
external
view
returns (uint256 totalDscMinted, uint256 collateralValueInUsd)
{
(totalDscMinted, collateralValueInUsd) = _getAccountInformation(user);
}
function getAdditionalFeedPrecision() external pure returns (uint256) {
return ADDITIONAL_FEED_PRECISION;
}
function getPrecision() external pure returns (uint256) {
return PRECISION;
}
function calculateHealthFactor(uint256 totalDscMinted, uint256 collateralValueInUsd)
external
pure
returns (uint256)
{
return _calculateHealthFactor(totalDscMinted, collateralValueInUsd);
}
function getHealthFactor(address user) external view returns (uint256) {
return _healthFactor(user);
}
function getLiquidationBonus() external pure returns (uint256) {
return LIQUIDATION_BONUS;
}
function getCollateralTokenPriceFeed(address token) external view returns (address) {
return s_priceFeeds[token];
}
function getCollateralTokens() external view returns (address[] memory) {
return s_collateralTokens;
}
function getMinHealthFactor() external pure returns (uint256) {
return MIN_HEALTH_FACTOR;
}
function getLiquidationThreshold() external pure returns (uint256) {
return LIQUIDATION_THRESHOLD;
}
function getCollateralBalanceOfUser(address user, address token) external view returns (uint256) {
return s_collateralDeposited[user][token];
}
function getDsc() external view returns (address) {
return address(i_dsc);
}
}