1/5
_Follow along the course with this video._ --- Our current DSCEngine.sol for reference: <details> <summary>DSCEngine.sol</summary> ```js // 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.18; import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { DecentralizedStableCoin } from "./DecentralizedStableCoin.sol"; import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.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 healthFactor); error DSCEngine__MintFailed(); error DSCEngine__HealthFactorOk(); ///////////////////////// // State Variables // ///////////////////////// mapping(address token => address priceFeed) private s_priceFeeds; DecentralizedStableCoin private immutable i_dsc; mapping(address user => mapping(address token => uint256 amount)) private s_collateralDeposited; mapping(address user => uint256 amountDscMinted) private s_DSCMinted; address[] private s_collateralTokens; uint256 private constant ADDITIONAL_FEED_PRECISION = 1e10; uint256 private constant PRECISION = 1e18; uint256 private constant LIQUIDATION_THRESHOLD = 50; uint256 private constant LIQUIDATION_PRECISION = 100; uint256 private constant MIN_HEALTH_FACTOR = 1e18; uint256 private constant LIQUIDATION_BONUS = 10; //////////////// // Events // //////////////// event CollateralDeposited(address indexed user, address indexed token, uint256 indexed amount); event CollateralRedeemed(address indexed user, address indexed token, uint256 indexed amount); /////////////////// // 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__TokenAddressesAndPriceFeedAddressesMustBeSameLength(); } 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 */ function depositCollateral( address tokenCollateralAddress, uint256 amountCollateral ) external 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(); } } /* * @param amountDscToMint: The amount of DSC you want to mint * You can only mint DSC if you hav enough collateral */ function mintDsc(uint256 amountDscToMint) external moreThanZero(amountDscToMint) nonReentrant { s_DSCMinted[msg.sender] += amountDscToMint; _revertIfHealthFactorIsBroken(msg.sender); bool minted = i_dsc.mint(msg.sender, amountDscToMint); if(!minted){ revert DSCEngine__MintFailed(); } } /* * @param tokenCollateralAddress: the collateral address to redeem * @param amountCollateral: amount of collateral to redeem * @param amountDscToBurn: 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); } /* * @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 moreThanZero(debtToCover) nonReentrant { uint256 startingUserHealthFactor = _healthFactor(user); if(startingUserHealthFactor > MIN_HEALTH_FACTOR){ revert DSCEngine__HealthFactorOk(); } uint256 tokenAmountFromDebtCovered = getTokenAmountFromUsd(collateral, debtToCover); uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / LIQUIDATION_PRECISION; uint256 totalCollateralRedeemed = tokenAmountFromDebtCovered + bonusCollateral; } ///////////////////////// // Public Functions // ///////////////////////// function redeemCollateral(address tokenCollateralAddress, uint256 amountCollateral) public moreThanZero(amountCollateral) nonReentrant{ s_collateralDeposited[msg.sender][tokenCollateralAddress] -= amountCollateral; emit CollateralRedeemed(msg.sender, tokenCollateralAddress, amountCollateral); bool success = IERC20(tokenCollateralAddress).transfer(msg.sender, amountCollateral); if(!success){ revert DSCEngine__TransferFailed(); } _revertIfHealthFactorIsBroken(msg.sender); } function burnDsc(uint256 amount) public moreThanZero(amount){ s_DSCMinted[msg.sender] -= amount; bool success = i_dsc.transferFrom(msg.sender, address(this), amount); if(!success){ revert DSCEngine__TransferFailed(); } i_dsc.burn(amount); _revertIfHealthFactorIsBroken(msg.sender); } /////////////////////////////////////////// // Private & Internal View Functions // /////////////////////////////////////////// /* * Returns how close to liquidation a user is * If a user goes below 1, then they can be liquidated. */ function _healthFactor(address user) private view returns(uint256){ (uint256 totalDscMinted, uint256 collateralValueInUsd) = _getAccountInformation(user); uint256 collateralAdjustedForThreshold = (collateralValueInUsd * LIQUIDATION_THRESHOLD) / LIQUIDATION_PRECISION; return (collateralAdjustedForThreshold * PRECISION) / totalDscMinted; } function _getAccountInformation(address user) private view returns(uint256 totalDscMinted,uint256 collateralValueInUsd){ totalDscMinted = s_DSCMinted[user]; collateralValueInUsd = getAccountCollateralValue(user); } function _revertIfHealthFactorIsBroken(address user) internal view { uint256 userHealthFactor = _healthFactor(user); if(userHealthFactor < MIN_HEALTH_FACTOR){ revert DSCEngine__BreaksHealthFactor(userHealthFactor); } } ////////////////////////////////////////// // Public & External View Functions // ////////////////////////////////////////// function getAccountCollateralValue(address user) public view returns (uint256 totalCollateralValueInUsd) { 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.latestRoundData(); return ((uint256(price) * ADDITIONAL_FEED_PRECISION) * amount) / PRECISION; } /* * @param tokenCollateralAddress: the address of the token to deposit as collateral * @param amountCollateral: The amount of collateral to deposit * @param amountDscToMint: The amount of DecentralizedStableCoin to mint * @notice: This function will deposit your collateral and mint DSC in one transaction */ function depositCollateralAndMintDsc(address tokenCollateralAddress, uint256 amountCollateral, uint256 amountDscToMint){ depositCollateral(tokenCollateralAddress, amountCollateral); mintDsc(amountDscToMint); } function getTokenAmountFromUsd(address token, uint256 usdAmountInWei) public view returns (uint256) { AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]); (, int256 price,,,) = priceFeed.latestRoundData(); return (usdAmountInWei * PRECISION) / (uint256(price) * ADDITIONAL_FEED_PRECISION); } function getHealthFactor() external view {} } ``` </details> ### Liquidation/Refactoring In the last lesson we left off with our `liquidate` function still needing to redeem the unhealthy position's collateral, and burn the `liquidator`'s `DSC`. If we look at the `redeemCollateral` function, we can see why achieving our goal won't be as simple as calling `redeemCollateral` and `burnDsc`. ```js function redeemCollateral(address tokenCollateralAddress, uint256 amountCollateral) public moreThanZero(amountCollateral) nonReentrant{ s_collateralDeposited[msg.sender][tokenCollateralAddress] -= amountCollateral; emit CollateralRedeemed(msg.sender, tokenCollateralAddress, amountCollateral); bool success = IERC20(tokenCollateralAddress).transfer(msg.sender, amountCollateral); if(!success){ revert DSCEngine__TransferFailed(); } _revertIfHealthFactorIsBroken(msg.sender); } ``` Currently this function has `msg.sender` hardcoded as the user for which collateral is redeemed _and_ sent to. This isn't the case when someone is being `liquidated`, the `msg.sender` is a third party. So, how do we adjust things to account for this? What we'll do is refactor the contract to include an _internal_ `_redeemCollateral` function which is only callable by permissioned methods within the protocol. This will allow our liquidate function to redeem the collateral of an arbitrary user when appropriate conditions are met. We'll add this new internal function under our `Private & Internal View Functions` header. ```js /////////////////////////////////////////// // Private & Internal View Functions // /////////////////////////////////////////// function _redeemCollateral(address tokenCollateralAddress, uint256 amountCollateral, address from, address to) private { s_collateralDeposited[from][tokenCollateralAddress] -= amountCollateral; emit CollateralRedeemed(msg.sender, tokenCollateralAddress, amountCollateral); bool success = IERC20(tokenCollateralAddress).transfer(to, amountCollateral); if(!success){ revert DSCEngine__TransferFailed(); } } ``` The above internal version of `redeemCollateral` contains the same logic as our public one currently, but we've changed the collateral balance change and transfer to reflect the `from` and `to` addresses respectively. At this point let's adjust our `CollateralRedeemed` event. We're going to adjust the emission and the declaration of the event to handle this new from/to structure. We'll adjust this in our public `redeemCollateral` function soon. ```js //////////////// // 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); ... function _redeemCollateral(address tokenCollateralAddress, uint256 amountCollateral, address from, address to){ ... emit CollateralRedeemed(from, to, tokenCollateralAddress, amountCollateral); ... } ``` Now, back in our public `redeemCollateral` function, we can simply call this internal version and hardcode the appropriate `msg.sender` values. ```js function redeemCollateral(address tokenCollateralAddress, uint256 amountCollateral) public moreThanZero(amountCollateral) nonReentrant{ _redeemCollateral(msg.sender, msg.sender, tokenCollateralAddress, amountCollateral); _revertIfHealthFactorIsBroken(msg.sender); } ``` ### Back to Liquidate Now that we've written this internal `_redeemCollateral` function, we can leverage this within our `liquidate` function. ```js function liquidate(address collateral, address user, uint256 debtToCover) external moreThanZero(debtToCover) nonReentrant { uint256 startingUserHealthFactor = _healthFactor(user); if(startingUserHealthFactor > MIN_HEALTH_FACTOR){ revert DSCEngine__HealthFactorOk(); } uint256 tokenAmountFromDebtCovered = getTokenAmountFromUsd(collateral, debtToCover); uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / LIQUIDATION_PRECISION; uint256 totalCollateralRedeemed = tokenAmountFromDebtCovered + bonusCollateral; _redeemCollateral(user, msg.sender, collateral, totalCollateralToRedeem); } ``` With the refactoring we've just done, we can be sure that the `liquidator` will be awarded the collateral (after some testing of course). We're going to need to do the same thing with our `burnDsc` function, which is currently public and hardcoded with `msg.sender` as well. ```js function burnDsc(uint256 amount) public moreThanZero(amount){ _burnDsc(amount, msg.sender, msg.sender) _revertIfHealthFactorIsBroken(msg.sender); } ... function _burnDsc(uint256 amountDscToBurn, address onBehalfOf, address dscFrom) private moreThanZero(amount){ s_DSCMinted[onBehalfOf] -= amount; bool success = i_dsc.transferFrom(dscFrom, address(this), amount); if(!success){ revert DSCEngine__TransferFailed(); } i_dsc.burn(amount); } ``` And, just like before, we can go back to our `liquidate` function and leverage this internal `_burnDsc`. ```js function liquidate(address collateral, address user, uint256 debtToCover) external moreThanZero(debtToCover) nonReentrant { uint256 startingUserHealthFactor = _healthFactor(user); if(startingUserHealthFactor > MIN_HEALTH_FACTOR){ revert DSCEngine__HealthFactorOk(); } uint256 tokenAmountFromDebtCovered = getTokenAmountFromUsd(collateral, debtToCover); uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / LIQUIDATION_PRECISION; uint256 totalCollateralRedeemed = tokenAmountFromDebtCovered + bonusCollateral; _redeemCollateral(user, msg.sender, collateral, totalCollateralToRedeem); _burnDsc(debtToCover, user, msg.sender); } ``` Importantly, we're calling these low level internal calls, so we've going to want to check some `Health Factors` here. If the `liquidation` somehow doesn't result in the user's `Health Factor` improving, we should revert. This will come with a new custom error. ```js uint256 endingUserHealthFactor = _healthFactor(user); if(endingUserHealthFactor <= startingUserHealthFactor){ revert DSCEngine__HealthFactorNotImproved(); } ``` Be sure to declare the custom error where appropriate. ```js /////////////////// // Errors // /////////////////// ... error DSCEngine__HealthFactorNotImproved(); ``` The last thing we'll want to do is also ensure that our `liquidator`'s `Health Factor` hasn't been broken. Our final `liquidate` function should look like this: ```js function liquidate(address collateral, address user, uint256 debtToCover) external moreThanZero(debtToCover) nonReentrant { uint256 startingUserHealthFactor = _healthFactor(user); if(startingUserHealthFactor > MIN_HEALTH_FACTOR){ revert DSCEngine__HealthFactorOk(); } uint256 tokenAmountFromDebtCovered = getTokenAmountFromUsd(collateral, debtToCover); uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / LIQUIDATION_PRECISION; uint256 totalCollateralRedeemed = tokenAmountFromDebtCovered + bonusCollateral; _redeemCollateral(user, msg.sender, collateral, totalCollateralToRedeem); _burnDsc(debtToCover, user, msg.sender); uint256 endingUserHealthFactor = _healthFactor(user); if(endingUserHealthFactor <= startingUserHealthFactor){ revert DSCEngine__HealthFactorNotImproved(); } _revertIfHealthFactorIsBroken(msg.sender); } ``` ### Wrap Up Run `forge build` at this point, make sure things are compiling, but if so... WOOOOOOOOO! We're (more or less) done with our `DSCEngine` contract. Let's recap a bit of what we've accomplished here and what our contract is capable of. Our code is starting to look very professional. We have NATSPEC documentation throughout. `DSCEngine` is able to `mintDsc`, `burnDsc`, `depositCollateral`, `redeemCollateral`, `liquidate` unhealthy positions, and assess the `Health Factor` of users. Users are able to mint as much `DSC` as their collateral and subsequently `Health Factor` will support. Currently a user must have `200%` collateralization of their `DSC` position. We've ensured that `liquidators` are incentivized to secure the value of our stablecoin by closing unhealthy positions and receiving collateral rewards in turn. It's through this mechanism that the `DecentralizedStableCoin` protocol will never be `under-collateralized`. Now's a great time to take a break. You've earned it. When you come back to the next lesson, we'll be diving deep into more advanced testing methodologies as we validate our code. See you there!
This lesson focuses on refining the DeFi protocol by refactoring the 'redeemCollateral()' function. It covers the importance of testing and refactoring for building a reliable DeFi protocol, enhancing security, and improving functionality.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Guest lecturers:
Juliette Chevalier
Lead Developer relations at Aragon
Nader Dabit
Director of developer relations at Avara
Ally Haire
Developer relations at Protocol Labs
Harrison
Founder at GasliteGG
Last updated on November 29, 2024
Solidity Developer
Advanced FoundryDuration: 36min
Duration: 3h 06min
Duration: 5h 02min
Duration: 2h 47min
Duration: 1h 23min
Duration: 4h 28min
Duration: 1h 19min
Duration: 58min
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Guest lecturers:
Juliette Chevalier
Lead Developer relations at Aragon
Nader Dabit
Director of developer relations at Avara
Ally Haire
Developer relations at Protocol Labs
Harrison
Founder at GasliteGG
Last updated on November 29, 2024
Testimonials
Read what our students have to say about this course.
Chainlink
Chainlink
Gustavo Gonzalez
Solutions Engineer at OpenZeppelin
Francesco Andreoli
Lead Devrel at Metamask
Albert Hu
DeForm Founding Engineer
Radek
Senior Developer Advocate at Ceramic
Boidushya
WalletConnect
Idris
Developer Relations Engineer at Axelar