-
Notifications
You must be signed in to change notification settings - Fork 124
/
DSCEngine.sol
464 lines (417 loc) · 17.4 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
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
// Layout of Contract:
// version
// imports
// errors
// interfaces, libraries, contracts
// Type declarations
// State variables
// Events
// Modifiers
// Functions
// Layout of Functions:
// constructor
// receive function (if exists)
// fallback function (if exists)
// external
// public
// internal
// private
// internal & private view & pure functions
// external & public view & pure functions
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import { OracleLib, AggregatorV3Interface } from "./libraries/OracleLib.sol";
// The correct path for ReentrancyGuard in latest Openzeppelin contracts is
//"import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { DecentralizedStableCoin } from "./DecentralizedStableCoin.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 at all times.
* This is a stablecoin with the properties:
* - Exogenously Collateralized
* - Dollar Pegged
* - Algorithmically Stable
*
* It is similar to DAI if DAI had no governance, no fees, and was backed by only 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 Decentralized Stablecoin system. It handles all the logic
* for minting and redeeming DSC, as well as depositing and withdrawing collateral.
* @notice This contract is based on the MakerDAO DSS system
*/
contract DSCEngine is ReentrancyGuard {
///////////////////
// Errors
///////////////////
error DSCEngine__TokenAddressesAndPriceFeedAddressesAmountsDontMatch();
error DSCEngine__NeedsMoreThanZero();
error DSCEngine__TokenNotAllowed(address token);
error DSCEngine__TransferFailed();
error DSCEngine__BreaksHealthFactor(uint256 healthFactorValue);
error DSCEngine__MintFailed();
error DSCEngine__HealthFactorOk();
error DSCEngine__HealthFactorNotImproved();
///////////////////
// Types
///////////////////
using OracleLib for AggregatorV3Interface;
///////////////////
// State Variables
///////////////////
DecentralizedStableCoin private immutable i_dsc;
uint256 private constant LIQUIDATION_THRESHOLD = 50; // This means you need to be 200% over-collateralized
uint256 private constant LIQUIDATION_BONUS = 10; // This means you get assets at a 10% discount when liquidating
uint256 private constant LIQUIDATION_PRECISION = 100;
uint256 private constant MIN_HEALTH_FACTOR = 1e18;
uint256 private constant PRECISION = 1e18;
uint256 private constant ADDITIONAL_FEED_PRECISION = 1e10;
uint256 private constant FEED_PRECISION = 1e8;
/// @dev Mapping of token address to price feed address
mapping(address collateralToken => address priceFeed) private s_priceFeeds;
/// @dev Amount of collateral deposited by user
mapping(address user => mapping(address collateralToken => uint256 amount)) private s_collateralDeposited;
/// @dev Amount of DSC minted by user
mapping(address user => uint256 amount) private s_DSCMinted;
/// @dev If we know exactly how many tokens we have, we could make this immutable!
address[] private s_collateralTokens;
///////////////////
// Events
///////////////////
event CollateralDeposited(address indexed user, address indexed token, uint256 indexed amount);
event CollateralRedeemed(address indexed redeemFrom, address indexed redeemTo, address token, uint256 amount); // if
// redeemFrom != redeemedTo, then it was liquidated
///////////////////
// Modifiers
///////////////////
modifier moreThanZero(uint256 amount) {
if (amount == 0) {
revert DSCEngine__NeedsMoreThanZero();
}
_;
}
modifier isAllowedToken(address token) {
if (s_priceFeeds[token] == address(0)) {
revert DSCEngine__TokenNotAllowed(token);
}
_;
}
///////////////////
// Functions
///////////////////
constructor(address[] memory tokenAddresses, address[] memory priceFeedAddresses, address dscAddress) {
if (tokenAddresses.length != priceFeedAddresses.length) {
revert DSCEngine__TokenAddressesAndPriceFeedAddressesAmountsDontMatch();
}
// These feeds will be the USD pairs
// For example ETH / USD or MKR / USD
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 ERC20 token address of the collateral you're depositing
* @param amountCollateral: The amount of collateral you're depositing
* @param amountDscToMint: The amount of DSC you want 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);
}
/*
* @param tokenCollateralAddress: The ERC20 token address of the collateral you're withdrawing
* @param amountCollateral: The amount of collateral you're withdrawing
* @param amountDscToBurn: The amount of DSC you want to burn
* @notice This function will withdraw your collateral and burn DSC in one transaction
*/
function redeemCollateralForDsc(
address tokenCollateralAddress,
uint256 amountCollateral,
uint256 amountDscToBurn
)
external
moreThanZero(amountCollateral)
isAllowedToken(tokenCollateralAddress)
{
_burnDsc(amountDscToBurn, msg.sender, msg.sender);
_redeemCollateral(tokenCollateralAddress, amountCollateral, msg.sender, msg.sender);
revertIfHealthFactorIsBroken(msg.sender);
}
/*
* @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
)
external
moreThanZero(amountCollateral)
nonReentrant
isAllowedToken(tokenCollateralAddress)
{
_redeemCollateral(tokenCollateralAddress, amountCollateral, msg.sender, msg.sender);
revertIfHealthFactorIsBroken(msg.sender);
}
/*
* @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
* your DSC but keep your collateral in.
*/
function burnDsc(uint256 amount) external moreThanZero(amount) {
_burnDsc(amount, msg.sender, msg.sender);
revertIfHealthFactorIsBroken(msg.sender); // I don't think this would ever hit...
}
/*
* @param collateral: The ERC20 token address of the collateral you're using to make the protocol solvent again.
* This is collateral that you're going to take from the user who is insolvent.
* In return, you have to burn your DSC to pay off their debt, but you don't pay off your own.
* @param user: The user who is insolvent. They have to have a _healthFactor below MIN_HEALTH_FACTOR
* @param debtToCover: The amount of DSC you want to burn to cover the user's debt.
*
* @notice: You can partially liquidate a user.
* @notice: You will get a 10% LIQUIDATION_BONUS for taking the users funds.
* @notice: This function working assumes that the protocol will be roughly 150% overcollateralized in order for this
to work.
* @notice: A known bug would be if the protocol was only 100% collateralized, we wouldn't be able to liquidate
anyone.
* For example, if the price of the collateral plummeted before anyone could be liquidated.
*/
function liquidate(
address collateral,
address user,
uint256 debtToCover
)
external
isAllowedToken(collateral)
moreThanZero(debtToCover)
nonReentrant
{
uint256 startingUserHealthFactor = _healthFactor(user);
if (startingUserHealthFactor >= MIN_HEALTH_FACTOR) {
revert DSCEngine__HealthFactorOk();
}
// If covering 100 DSC, we need to $100 of collateral
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
uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / LIQUIDATION_PRECISION;
// Burn DSC equal to debtToCover
// Figure out how much collateral to recover based on how much burnt
_redeemCollateral(collateral, tokenAmountFromDebtCovered + bonusCollateral, user, msg.sender);
_burnDsc(debtToCover, user, msg.sender);
uint256 endingUserHealthFactor = _healthFactor(user);
// This conditional should never hit, but just in case
if (endingUserHealthFactor <= startingUserHealthFactor) {
revert DSCEngine__HealthFactorNotImproved();
}
revertIfHealthFactorIsBroken(msg.sender);
}
///////////////////
// Public Functions
///////////////////
/*
* @param amountDscToMint: The amount of DSC you want to mint
* You can only mint DSC if you have enough collateral
*/
function mintDsc(uint256 amountDscToMint) public moreThanZero(amountDscToMint) nonReentrant {
s_DSCMinted[msg.sender] += amountDscToMint;
revertIfHealthFactorIsBroken(msg.sender);
bool minted = i_dsc.mint(msg.sender, amountDscToMint);
if (minted != true) {
revert DSCEngine__MintFailed();
}
}
/*
* @param tokenCollateralAddress: The ERC20 token address of the collateral you're depositing
* @param amountCollateral: The amount of collateral you're depositing
*/
function depositCollateral(
address tokenCollateralAddress,
uint256 amountCollateral
)
public
moreThanZero(amountCollateral)
nonReentrant
isAllowedToken(tokenCollateralAddress)
{
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();
}
}
///////////////////
// Private Functions
///////////////////
function _redeemCollateral(
address tokenCollateralAddress,
uint256 amountCollateral,
address from,
address to
)
private
{
s_collateralDeposited[from][tokenCollateralAddress] -= amountCollateral;
emit CollateralRedeemed(from, to, tokenCollateralAddress, amountCollateral);
bool success = IERC20(tokenCollateralAddress).transfer(to, amountCollateral);
if (!success) {
revert DSCEngine__TransferFailed();
}
}
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 hypothetically unreachable
if (!success) {
revert DSCEngine__TransferFailed();
}
i_dsc.burn(amountDscToBurn);
}
//////////////////////////////
// Private & Internal View & Pure Functions
//////////////////////////////
function _getAccountInformation(address user)
private
view
returns (uint256 totalDscMinted, uint256 collateralValueInUsd)
{
totalDscMinted = s_DSCMinted[user];
collateralValueInUsd = getAccountCollateralValue(user);
}
function _healthFactor(address user) private view returns (uint256) {
(uint256 totalDscMinted, uint256 collateralValueInUsd) = _getAccountInformation(user);
return _calculateHealthFactor(totalDscMinted, collateralValueInUsd);
}
function _getUsdValue(address token, uint256 amount) private view returns (uint256) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]);
(, int256 price,,,) = priceFeed.staleCheckLatestRoundData();
// 1 ETH = 1000 USD
// The returned value from Chainlink will be 1000 * 1e8
// Most USD pairs have 8 decimals, so we will just pretend they all do
// We want to have everything in terms of WEI, so we add 10 zeros at the end
return ((uint256(price) * ADDITIONAL_FEED_PRECISION) * amount) / PRECISION;
}
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 * PRECISION) / totalDscMinted;
}
function revertIfHealthFactorIsBroken(address user) internal view {
uint256 userHealthFactor = _healthFactor(user);
if (userHealthFactor < MIN_HEALTH_FACTOR) {
revert DSCEngine__BreaksHealthFactor(userHealthFactor);
}
}
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
// External & Public View & Pure Functions
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
function calculateHealthFactor(
uint256 totalDscMinted,
uint256 collateralValueInUsd
)
external
pure
returns (uint256)
{
return _calculateHealthFactor(totalDscMinted, collateralValueInUsd);
}
function getAccountInformation(address user)
external
view
returns (uint256 totalDscMinted, uint256 collateralValueInUsd)
{
return _getAccountInformation(user);
}
function getUsdValue(
address token,
uint256 amount // in WEI
)
external
view
returns (uint256)
{
return _getUsdValue(token, amount);
}
function getCollateralBalanceOfUser(address user, address token) external view returns (uint256) {
return s_collateralDeposited[user][token];
}
function getAccountCollateralValue(address user) public view returns (uint256 totalCollateralValueInUsd) {
for (uint256 index = 0; index < s_collateralTokens.length; index++) {
address token = s_collateralTokens[index];
uint256 amount = s_collateralDeposited[user][token];
totalCollateralValueInUsd += _getUsdValue(token, amount);
}
return totalCollateralValueInUsd;
}
function getTokenAmountFromUsd(address token, uint256 usdAmountInWei) public view returns (uint256) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]);
(, int256 price,,,) = priceFeed.staleCheckLatestRoundData();
// $100e18 USD Debt
// 1 ETH = 2000 USD
// The returned value from Chainlink will be 2000 * 1e8
// Most USD pairs have 8 decimals, so we will just pretend they all do
return ((usdAmountInWei * PRECISION) / (uint256(price) * ADDITIONAL_FEED_PRECISION));
}
function getPrecision() external pure returns (uint256) {
return PRECISION;
}
function getAdditionalFeedPrecision() external pure returns (uint256) {
return ADDITIONAL_FEED_PRECISION;
}
function getLiquidationThreshold() external pure returns (uint256) {
return LIQUIDATION_THRESHOLD;
}
function getLiquidationBonus() external pure returns (uint256) {
return LIQUIDATION_BONUS;
}
function getLiquidationPrecision() external pure returns (uint256) {
return LIQUIDATION_PRECISION;
}
function getMinHealthFactor() external pure returns (uint256) {
return MIN_HEALTH_FACTOR;
}
function getCollateralTokens() external view returns (address[] memory) {
return s_collateralTokens;
}
function getDsc() external view returns (address) {
return address(i_dsc);
}
function getCollateralTokenPriceFeed(address token) external view returns (address) {
return s_priceFeeds[token];
}
function getHealthFactor(address user) external view returns (uint256) {
return _healthFactor(user);
}
}