diff --git a/foundry.toml b/foundry.toml index f595e04..c8b66cc 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,6 @@ src = "src" out = "out" libs = ["lib"] -remappings = ["@openzeppelin=lib/openzeppelin-contracts/contracts"] +remappings = ["@openzeppelin=lib/openzeppelin-contracts/contracts", +"@chainlink=lib/chainlink-brownie-contracts/contracts/src/v0.8",] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/chainlink-brownie-contracts b/lib/chainlink-brownie-contracts index b0591b8..c6d0ca5 160000 --- a/lib/chainlink-brownie-contracts +++ b/lib/chainlink-brownie-contracts @@ -1 +1 @@ -Subproject commit b0591b8790171392db81549f809177f0679b04a4 +Subproject commit c6d0ca512f1b09d93bc81415d246dbee7e5c6894 diff --git a/script/DeployDSC.s.sol b/script/DeployDSC.s.sol index 1707229..4f5eb24 100644 --- a/script/DeployDSC.s.sol +++ b/script/DeployDSC.s.sol @@ -12,10 +12,10 @@ contract DeployDSC is Script { address[] public priceFeedAddresses; function run() public returns (DecentralizedStableCoin, DSCEngine, HelperConfig) { - HelperConfig config = new HelperConfig(); - (address wethUsdPriceFeed, address wbtcUsdPriceFeed, address weth, address wbtc, uint256 deployerKey) = config.activeNetworkConfig(); + (address wethUsdPriceFeed, address wbtcUsdPriceFeed, address weth, address wbtc, uint256 deployerKey) = + config.activeNetworkConfig(); priceFeedAddresses = [wethUsdPriceFeed, wbtcUsdPriceFeed]; tokenAddresses = [weth, wbtc]; @@ -26,6 +26,6 @@ contract DeployDSC is Script { DSCEngine engine = new DSCEngine(tokenAddresses, priceFeedAddresses, address(dsc)); dsc.transferOwnership(address(engine)); vm.stopBroadcast(); - return(dsc, engine, config); + return (dsc, engine, config); } -} \ No newline at end of file +} diff --git a/script/HelperConfig.s.sol b/script/HelperConfig.s.sol index 774d589..fa72b3a 100644 --- a/script/HelperConfig.s.sol +++ b/script/HelperConfig.s.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.18; import {Script} from "forge-std/Script.sol"; import {MockV3Aggregator} from "../test/mocks/MockV3Aggregator.t.sol"; -import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; +import {ERC20Mock} from "@openzeppelin/mocks/token/ERC20Mock.sol"; contract HelperConfig is Script { struct NetworkConfig { @@ -18,7 +18,7 @@ contract HelperConfig is Script { // constants uint8 public constant WETH_DECIMALS = 8; uint8 public constant WBTC_DECIMALS = 8; - int256 public constant WETH_USD_PRICE = 2800 * 10 ** 8; + int256 public constant WETH_USD_PRICE = 2000 * 10 ** 8; int256 public constant WBTC_USD_PRICE = 60000 * 10 ** 8; uint256 public constant ANVIL_DEPLOYER_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; @@ -27,8 +27,7 @@ contract HelperConfig is Script { constructor() { if (block.chainid == 11155111) { activeNetworkConfig = getSepoliaEthConfig(); - } - else { + } else { activeNetworkConfig = getOrCreateAnvilConfig(); } } @@ -43,7 +42,7 @@ contract HelperConfig is Script { }); } - function getOrCreateAnvilConfig() public returns (NetworkConfig memory){ + function getOrCreateAnvilConfig() public returns (NetworkConfig memory) { // check to see if we have an existing config if (activeNetworkConfig.wethUsdPriceFeed != address(0)) { return activeNetworkConfig; @@ -53,11 +52,11 @@ contract HelperConfig is Script { vm.startBroadcast(); //weth mock $ PriceFeeds MockV3Aggregator wethUsdPriceFeed = new MockV3Aggregator(WETH_DECIMALS, WETH_USD_PRICE); - ERC20Mock wethMock = new ERC20Mock( ); + ERC20Mock wethMock = new ERC20Mock(); // wbtc mock and PriceFeeds MockV3Aggregator wbtcUsdPriceFeed = new MockV3Aggregator(WETH_DECIMALS, WBTC_USD_PRICE); - ERC20Mock wbtcMock = new ERC20Mock( ); + ERC20Mock wbtcMock = new ERC20Mock(); vm.stopBroadcast(); return NetworkConfig({ @@ -68,4 +67,4 @@ contract HelperConfig is Script { deployerKey: ANVIL_DEPLOYER_KEY }); } -} \ No newline at end of file +} diff --git a/src/DSCEngine.sol b/src/DSCEngine.sol index 060800f..5678c5b 100644 --- a/src/DSCEngine.sol +++ b/src/DSCEngine.sol @@ -29,8 +29,9 @@ pragma solidity ^0.8.19; //////////////////////////////////////////////////////////////*/ import {DecentralizedStableCoin} from "./DecntralizedStableCoin.sol"; -import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/utils/ReentrancyGuard.sol"; +import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; +import {AggregatorV3Interface} from "@chainlink/interfaces/AggregatorV3Interface.sol"; /*////////////////////////////////////////////////////////////// CONTRACT @@ -59,6 +60,10 @@ contract DSCEngine is ReentrancyGuard { error DSCEngine__TokenAddressesAndPriceFeedAddressesMustMatch(); error DSCEngine__TokenNotSupported(); error DSCEngine__TransferFailed(); + error DSCEngine__BrokenHealthFactor(uint256 healthFactor); + error DSCEngine__MintFailed(); + error DSCEngine__HealthFactorOk(); + error DSCEngine__HealthFactorNotImproved(); /*////////////////////////////////////////////////////////////// TYPE DECLARATION @@ -67,8 +72,18 @@ contract DSCEngine is ReentrancyGuard { /*////////////////////////////////////////////////////////////// STATE VARRIABLES //////////////////////////////////////////////////////////////*/ + uint256 private constant ADDITIONAL_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; + mapping(address token => address priceFeed) private s_priceFeeds; // tokenToPriceFeed mapping(address user => mapping(address token => uint256 amount)) private s_collateralDeposited; // userToTokenToAmount + mapping(address user => uint256) private s_dscMinted; // userToAmount + + address[] private s_collateralTokens; DecentralizedStableCoin private immutable i_dsc; @@ -76,6 +91,9 @@ contract DSCEngine is ReentrancyGuard { 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 @@ -95,9 +113,9 @@ contract DSCEngine is ReentrancyGuard { } /*////////////////////////////////////////////////////////////// - FUNCTIONS - //////////////////////////////////////////////////////////////*/ - + FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ------------------------------------------------- CONSTRUCTOR ------------------------------------------------- */ constructor(address[] memory tokenAddresses, address[] memory priceFeedAddresses, address dscAddress) { if (tokenAddresses.length != priceFeedAddresses.length) { @@ -105,12 +123,25 @@ contract DSCEngine is ReentrancyGuard { } 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 ------------------------------------------------- */ - function depositCollateralAndMintDSC() external {} + + /// @notice This Function allow users to deposit collateral and mint the StableCoin (DSC) + /// @param tokenCollateralAddress: The address of the token to dpeosit as collateral + /// @param amountCollateral: The amount of collateral to deposit + /// @param amountDscToMint: the amount of decentralizedstablecoin to mint + function depositCollateralAndMintDSC( + address tokenCollateralAddress, + uint256 amountCollateral, + uint256 amountDscToMint + ) external { + depositCollateral(tokenCollateralAddress, amountCollateral); + mintDsc(amountDscToMint); + } /** * @dev Deposit collateral to mint DSC @@ -119,7 +150,7 @@ contract DSCEngine is ReentrancyGuard { * @notice This functions follows CEI (Checks-Effects-Interactions) pattern */ function depositCollateral(address tokenCollateralAddress, uint256 amountCollateral) - external + public moreThanZero(amountCollateral) isAllowedToken(tokenCollateralAddress) nonReentrant @@ -127,20 +158,208 @@ contract DSCEngine is ReentrancyGuard { // Transfer the collateral from the user to this contract s_collateralDeposited[msg.sender][tokenCollateralAddress] += amountCollateral; emit CollateralDeposited(msg.sender, tokenCollateralAddress, amountCollateral); - + // Interact with the DSC contract to mint DSC bool success = IERC20(tokenCollateralAddress).transferFrom(msg.sender, address(this), amountCollateral); if (!success) { - revert DSCEngine__TransferFailed(); + revert DSCEngine__TransferFailed(); + } + } + + function redeemCollateralForDSC(address tokenCollateralAddress, uint256 amountCollateral, uint256 amountDscToBurn) + external + { + burnDsc(amountDscToBurn); + reedemCollateral(tokenCollateralAddress, amountCollateral); + // redeemed collateral already checks health factor. + } + + /** + * + * @param tokenCollateralAddress Address of the collateral token + * @param amountCollateral amount of the collateral to be redeemed. + */ + + /** + * in order to redeem collateral: + * 1. Health factor must be over 1 AFTER collateral is pulled + * DRY: Don't Repeat Yourself: + * CEI: Checks Effects and Interactions. + */ + function reedemCollateral(address tokenCollateralAddress, uint256 amountCollateral) + public + moreThanZero(amountCollateral) + nonReentrant + { + _redeemCollateral(tokenCollateralAddress, amountCollateral, msg.sender, msg.sender); + _revertIfHealthFactorIsBroken(msg.sender); + } + + // Burn the Decentralized Stable coin minted. + function burnDsc(uint256 amount) public moreThanZero(amount) { + _burnDsc(amount, msg.sender, msg.sender); + _revertIfHealthFactorIsBroken(msg.sender); // I don'th think this would ever hit.. + } + + /** + * + * @notice This functions follows CEI (Checks-Effects-Interactions) pattern + * @param amountDscToMint Amount of DSC to mint + * @dev Mint DSC by depositing collateral + * @notice they must have more collateral value than the minimum threshold + */ + function mintDsc(uint256 amountDscToMint) public moreThanZero(amountDscToMint) { + s_dscMinted[msg.sender] += amountDscToMint; + // if they minted more than the minimum threshold, they can't mint DSC + _revertIfHealthFactorIsBroken(msg.sender); + bool minted = i_dsc.mint(msg.sender, amountDscToMint); + if (!minted) { + revert DSCEngine__MintFailed(); } + } + /** + * @param collateral: Address of the collateral token to liquidate from the user. + * @param user: Address of the user to liquidate (User who has broken the _healthFactor. + * @param debtToCover: Amount of DSC to cover. + * @notice you can partially liquidate a user's debt + * @notice you'll get a liquidation bonous if you liquidate a user's debt + * @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 incentivize liquidators. + * - For example, if the price of the collateral plummeted before anyone could be liquidated, then the protocol would be insolvent. + * - Follow CEI (Checks-Effects-Interactions) pattern + */ + function liquidate(address collateral, address user, uint256 debtToCover) + external + moreThanZero(debtToCover) + nonReentrant + { + // check: health factor of the user + uint256 userHealthFactor = _healthFactor(user); + if (userHealthFactor >= MIN_HEALTH_FACTOR) { + revert DSCEngine__HealthFactorOk(); + } + // we want to burn their DSC "debt" + // take their collateral + // Bad User: + // - $140 ETH, $100 DSC + // - debtToCover = $100 + // $100 DSC -> ?? ETH + uint256 tokenAmountFromDebtCovered = getTokenAmountFromUsd(collateral, debtToCover); + // give the liquidator a bonous of 10% + uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / PRECISION; + uint256 totalCollateralToRedeem = tokenAmountFromDebtCovered + bonusCollateral; + _redeemCollateral(collateral, totalCollateralToRedeem, user, msg.sender); + // burn the DSC + _burnDsc(debtToCover, user, msg.sender); + + uint256 endingUserHealthFactor = _healthFactor(user); + if (endingUserHealthFactor <= userHealthFactor) { + revert DSCEngine__HealthFactorNotImproved(); + } + _revertIfHealthFactorIsBroken(msg.sender); } - function redeemCollateralForDSC() external {} - function reedemCollateral() external {} - function burnDsc() external {} - function mintDsc() external {} - function liquidate() external {} + function gethealthFactor() external view {} - function healthFactor() external view {} + /* ------------------------------------------------- PRIVATE & INTERNAL VIEW FUNCTIONS ------------------------------------------------- */ + + function _getAccountInformation(address user) + private + view + returns (uint256 totalDscMinted, uint256 totalCollateralValueInUsd) + { + totalDscMinted = s_dscMinted[user]; + totalCollateralValueInUsd = getAccountCollateralValueInUsd(user); + } + /** + * @dev Calculate the health factor of a user, and returns how close to liquidation they are + * @param user: User address to check health factor for. + * + */ + + function _healthFactor(address user) private view returns (uint256) { + // total Dsc Minited + // total collateral value + (uint256 totalDscMinted, uint256 totalCollateralValueInUsd) = _getAccountInformation(user); + uint256 collateralAdjustedForThreshold = + (totalCollateralValueInUsd * LIQUIDATION_THRESHOLD) / LIQUIDATION_PRECISION; + return collateralAdjustedForThreshold * PRECISION / totalDscMinted; + } + + /** + * + * @param user : User's address to check health factor if broken. + */ + function _revertIfHealthFactorIsBroken(address user) internal view { + // if the health factor is below the threshold, revert + + uint256 userHealthFactor = _healthFactor(user); + if (userHealthFactor < MIN_HEALTH_FACTOR) { + revert DSCEngine__BrokenHealthFactor(userHealthFactor); + } + + // Check: health factor (do they have more collateral value than the minimum threshold) + // Revert: if the health factor is below the threshold + } + + function _redeemCollateral(address tokenCollateralAddress, uint256 amountCollateral, address from, address to) + internal + { + s_collateralDeposited[from][tokenCollateralAddress] -= amountCollateral; + emit CollateralRedeemed(from, to, tokenCollateralAddress, amountCollateral); + + bool success = IERC20(tokenCollateralAddress).transfer(msg.sender, amountCollateral); + if (!success) { + revert DSCEngine__TransferFailed(); + } + } + + /** + * @dev Low-level internal function, do now call unless the function calling it has already checked the health factor + * @param amountDscToBurn: Amount of DSC to burn + * @param onBehalf : Address of the user who is burning the DSC + * @param dscFrom : Address of the user who is burning the DSC + */ + function _burnDsc(uint256 amountDscToBurn, address onBehalf, address dscFrom) internal { + s_dscMinted[onBehalf] -= amountDscToBurn; + bool success = i_dsc.transferFrom(dscFrom, address(this), amountDscToBurn); + if (!success) { + revert DSCEngine__TransferFailed(); + } + i_dsc.burn(amountDscToBurn); + } + + /* ------------------------------------------------- Public & External View Functions ------------------------------------------------- */ + function getAccountCollateralValueInUsd(address user) public view returns (uint256 totalCollateralValueInUsd) { + // Get the total value of all the collateral deposited by the user + // For each token deposited as collateral, get the value of the token in USD + // Multiply the amount of the token by the value of the token in US + // Add all the values together + for (uint256 i = 0; i < s_collateralTokens.length; i++) { + address token = s_collateralTokens[i]; + uint256 amount = s_collateralDeposited[user][token]; + totalCollateralValueInUsd += getUsdValue(token, amount); + } + } + + function getUsdValue(address token, uint256 amount) public view returns (uint256) { + AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]); + (, int256 price,,,) = priceFeed.latestRoundData(); + return (uint256(price) * ADDITIONAL_PRECISION * amount) / PRECISION; + } + + 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_PRECISION); + } + + function getAccountInformation(address user) + external + view + returns (uint256 totalDscMinted, uint256 totalCollateralValueInUsd) + { + (totalDscMinted, totalCollateralValueInUsd) = _getAccountInformation(user); + } } diff --git a/test/mocks/MockV3Aggregator.t.sol b/test/mocks/MockV3Aggregator.t.sol index cd6b901..49a8243 100644 --- a/test/mocks/MockV3Aggregator.t.sol +++ b/test/mocks/MockV3Aggregator.t.sol @@ -69,4 +69,4 @@ contract MockV3Aggregator { function description() external pure returns (string memory) { return "v0.6/tests/MockV3Aggregator.sol"; } -} \ No newline at end of file +} diff --git a/test/unit/DSCEngineTest.t.sol b/test/unit/DSCEngineTest.t.sol index 101d3c4..35eebdf 100644 --- a/test/unit/DSCEngineTest.t.sol +++ b/test/unit/DSCEngineTest.t.sol @@ -7,32 +7,101 @@ import {DeployDSC} from "../../script/DeployDSC.s.sol"; import {DecentralizedStableCoin} from "../../src/DecntralizedStableCoin.sol"; import {DSCEngine} from "../../src/DSCEngine.sol"; import {HelperConfig} from "../../script/HelperConfig.s.sol"; +import {ERC20Mock} from "@openzeppelin/mocks/token/ERC20Mock.sol"; contract DSCEngineTest is Test { - DeployDSC deployer; - DecentralizedStableCoin dsc; - DSCEngine engine; - HelperConfig config; - address weth; - address ethUsdPriceFeed; - - function setUp() public { + DeployDSC deployer; + DecentralizedStableCoin dsc; + DSCEngine engine; + HelperConfig config; + address weth; + address wbtc; + address ethUsdPriceFeed; + address btcUsdPriceFeed; + + address public USER = makeAddr("user"); + uint256 public constant AMOUNT_COLLATERAL = 10e18; + uint256 public constant STARTING_BALANCE = 10e18; + + function setUp() public { deployer = new DeployDSC(); (dsc, engine, config) = deployer.run(); - (ethUsdPriceFeed,, weth,,) = config.activeNetworkConfig(); - } + (ethUsdPriceFeed, btcUsdPriceFeed, weth, wbtc,) = config.activeNetworkConfig(); + ERC20Mock(weth).mint(USER, STARTING_BALANCE); + } + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR TEST + //////////////////////////////////////////////////////////////*/ + address[] public tokenAddresses; + address[] public priceFeedAddresses; + + function testRevertsIfTokenLengthDoesNotMatchPriceFeeds() public { + tokenAddresses.push(weth); + // tokenAddresses.push(wbtc); + priceFeedAddresses.push(ethUsdPriceFeed); + priceFeedAddresses.push(btcUsdPriceFeed); + vm.expectRevert(DSCEngine.DSCEngine__TokenAddressesAndPriceFeedAddressesMustMatch.selector); + new DSCEngine(tokenAddresses, priceFeedAddresses, address(dsc)); + } - /*////////////////////////////////////////////////////////////// - GETUSDVALUETEST + /*////////////////////////////////////////////////////////////// + PRICE FEED TEST //////////////////////////////////////////////////////////////*/ - function testGetUsdValue() view public { - uint256 ethAmount = 15e18; - uint256 expectedUsdValue = 42000e18; - uint256 actualUsdValue = engine.getUsdValue(weth, ethAmount); - assertEq(actualUsdValue, expectedUsdValue); + function testGetUsdValue() public view { + uint256 ethAmount = 15e18; + uint256 expectedUsdValue = 30000e18; + uint256 actualUsdValue = engine.getUsdValue(weth, ethAmount); + assertEq(actualUsdValue, expectedUsdValue); + } + + function testgetTokenAmountFromUsd() public view { + uint256 usdAmount = 100 ether; + uint256 expectedTokenAmount = 0.05 ether; + uint256 actualTokenAmount = engine.getTokenAmountFromUsd(weth, usdAmount); + assertEq(actualTokenAmount, expectedTokenAmount); + } + /*////////////////////////////////////////////////////////////// + DEPOST COLLATERAL TEST + //////////////////////////////////////////////////////////////*/ + + function testRevertsIfColateralZero() public { + vm.startPrank(USER); + ERC20Mock(weth).approve(address(engine), AMOUNT_COLLATERAL); + + vm.expectRevert(DSCEngine.DSCEngine__NeedsToBeMoreThanZero.selector); + engine.depositCollateral(weth, 0); + vm.stopPrank(); + } + + function testIsUnAllowedToken() public { + ERC20Mock testToken = new ERC20Mock(); + vm.expectRevert(DSCEngine.DSCEngine__TokenNotSupported.selector); + engine.depositCollateral(address(testToken), 10e18); + } + + function testRevertsWithUnapprovedCollateral() public { + ERC20Mock ranToken = new ERC20Mock(); + vm.startPrank(USER); + vm.expectRevert(DSCEngine.DSCEngine__TokenNotSupported.selector); + engine.depositCollateral(address(ranToken), AMOUNT_COLLATERAL); + vm.stopPrank(); + } + modifier depositedCollateral() { + vm.startPrank(USER); + ERC20Mock(weth).approve(address(engine), AMOUNT_COLLATERAL); + engine.depositCollateral(weth, AMOUNT_COLLATERAL); + _; + } - } -} \ No newline at end of file + function testCanDepoistCollateralAngGetAccountInfo() public depositedCollateral { + (uint256 totalDscMinted, uint256 collateralVauleInUsd) = engine.getAccountInformation(USER); + uint256 expectedTotalDscMinted = 0; + uint256 expectedDepositAmount = engine.getTokenAmountFromUsd(weth, collateralVauleInUsd); + assertEq(totalDscMinted, expectedTotalDscMinted); + assertEq(AMOUNT_COLLATERAL, expectedDepositAmount); + } +}