From eef9bc1eeb23aa8879c48d0add8a06a6cbfc11a1 Mon Sep 17 00:00:00 2001 From: shivangrawat30 Date: Fri, 6 Sep 2024 18:08:52 +0530 Subject: [PATCH] First Commit Signed-off-by: shivangrawat30 --- .gitmodules | 9 + .vscode/settings.json | 3 + Makefile | 46 +++ foundry.toml | 10 + remappings.txt | 8 + script/Counter.s.sol | 19 -- script/DeployDSC.s.sol | 29 ++ script/HelperConfig.s.sol | 61 ++++ src/Counter.sol | 14 - src/DSCEngine.sol | 431 +++++++++++++++++++++++++++++ src/DecentralizedStableCoin.sol | 71 +++++ src/libraries/OracleLib.sol | 39 +++ test/Counter.t.sol | 24 -- test/fuzz/Handler.t.sol | 95 +++++++ test/fuzz/Invariants.t.sol | 73 +++++ test/fuzz/OpenInvariantsTest.t.sol | 48 ++++ test/mocks/ERC20Mock.sol | 29 ++ test/mocks/MockV3Aggregator.sol | 72 +++++ test/unit/DSCEngineTest.t.sol | 411 +++++++++++++++++++++++++++ 19 files changed, 1435 insertions(+), 57 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 Makefile create mode 100644 remappings.txt delete mode 100644 script/Counter.s.sol create mode 100644 script/DeployDSC.s.sol create mode 100644 script/HelperConfig.s.sol delete mode 100644 src/Counter.sol create mode 100644 src/DSCEngine.sol create mode 100644 src/DecentralizedStableCoin.sol create mode 100644 src/libraries/OracleLib.sol delete mode 100644 test/Counter.t.sol create mode 100644 test/fuzz/Handler.t.sol create mode 100644 test/fuzz/Invariants.t.sol create mode 100644 test/fuzz/OpenInvariantsTest.t.sol create mode 100644 test/mocks/ERC20Mock.sol create mode 100644 test/mocks/MockV3Aggregator.sol create mode 100644 test/unit/DSCEngineTest.t.sol diff --git a/.gitmodules b/.gitmodules index 888d42d..1559941 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,12 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/foundry-devops"] + path = lib/foundry-devops + url = https://github.com/cyfrin/foundry-devops +[submodule "lib/chainlink-brownie-contracts"] + path = lib/chainlink-brownie-contracts + url = https://github.com/smartcontractkit/chainlink-brownie-contracts +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/openzeppelin/openzeppelin-contracts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b5666c3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "solidity.compileUsingRemoteVersion": "v0.8.19+commit.7dd6d404" +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3d3f86c --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +-include .env + +.PHONY: all test clean deploy fund help install snapshot format anvil + +DEFAULT_ANVIL_KEY := 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + +help: + @echo "Usage:" + @echo " make deploy [ARGS=...]\n example: make deploy ARGS=\"--network sepolia\"" + @echo "" + @echo " make fund [ARGS=...]\n example: make deploy ARGS=\"--network sepolia\"" + +all: clean remove install update build + +# Clean the repo +clean :; forge clean + +# Remove modules +remove :; rm -rf .gitmodules && rm -rf .git/modules/* && rm -rf lib && touch .gitmodules && git add . && git commit -m "modules" + +install :; forge install cyfrin/foundry-devops@0.1.0 --no-commit && forge install smartcontractkit/chainlink-brownie-contracts@0.6.1 --no-commit && forge install foundry-rs/forge-std@v1.5.3 --no-commit && forge install openzeppelin/openzeppelin-contracts@v4.8.3 --no-commit + +# Update Dependencies +update:; forge update + +build:; forge build + +test :; forge test + +coverage :; forge coverage --report debug > coverage-report.txt + +snapshot :; forge snapshot + +format :; forge fmt + +anvil :; anvil -m 'test test test test test test test test test test test junk' --steps-tracing --block-time 1 + +NETWORK_ARGS := --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast + +ifeq ($(findstring --network sepolia,$(ARGS)),--network sepolia) + NETWORK_ARGS := --rpc-url $(SEPOLIA_RPC_URL) --private-key $(PRIVATE_KEY) --broadcast --verify --etherscan-api-key $(ETHERSCAN_API_KEY) -vvvv +endif + +deploy: + @forge script script/DeployDSC.s.sol:DeployDSC $(NETWORK_ARGS) + diff --git a/foundry.toml b/foundry.toml index 25b918f..3587142 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,15 @@ src = "src" out = "out" libs = ["lib"] +remappings = [ + '@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/', + '@openzeppelin/contracts=lib/openzeppelin-contracts/contracts', +] +[invariant] +runs = 128 +depth = 64 +fail_on_revert = true # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +[dependencies] +openzeppelin = "4.7.3" \ No newline at end of file diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..fe42e8a --- /dev/null +++ b/remappings.txt @@ -0,0 +1,8 @@ +@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/ +@solmate/=lib/solmate/src/ +chainlink-brownie-contracts/=lib/chainlink-brownie-contracts/contracts/src/v0.6/vendor/@arbitrum/nitro-contracts/src/ +ds-test/=lib/solmate/lib/ds-test/src/ +forge-std/=lib/forge-std/src/ +solmate/=lib/solmate/src/ +weird-erc20/=lib/solmate/lib/weird-erc20/src/ +@openzeppelin/contracts=lib/openzeppelin-contracts/contracts \ No newline at end of file diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index cdc1fe9..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/script/DeployDSC.s.sol b/script/DeployDSC.s.sol new file mode 100644 index 0000000..ac845b0 --- /dev/null +++ b/script/DeployDSC.s.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {Script} from "forge-std/Script.sol"; +import {HelperConfig} from "./HelperConfig.s.sol"; +import {DecentralizedStableCoin} from "../src/DecentralizedStableCoin.sol"; +import {DSCEngine} from "../src/DSCEngine.sol"; + +contract DeployDSC is Script { + address[] public tokenAddresses; + address[] public priceFeedAddresses; + + function run() external returns (DecentralizedStableCoin, DSCEngine, HelperConfig) { + HelperConfig helperConfig = new HelperConfig(); + + (address wethUsdPriceFeed, address wbtcUsdPriceFeed, address weth, address wbtc, uint256 deployerKey) = + helperConfig.activeNetworkConfig(); + + tokenAddresses = [weth, wbtc]; + priceFeedAddresses = [wethUsdPriceFeed, wbtcUsdPriceFeed]; + vm.startBroadcast(deployerKey); + DecentralizedStableCoin dsc = new DecentralizedStableCoin(); + DSCEngine dscEngine = new DSCEngine(tokenAddresses, priceFeedAddresses, address(dsc)); + dsc.transferOwnership(address(dscEngine)); + vm.stopBroadcast(); + return (dsc, dscEngine, helperConfig); + } +} diff --git a/script/HelperConfig.s.sol b/script/HelperConfig.s.sol new file mode 100644 index 0000000..20a8eb4 --- /dev/null +++ b/script/HelperConfig.s.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {MockV3Aggregator} from "../test/mocks/MockV3Aggregator.sol"; +import {Script} from "forge-std/Script.sol"; +import {ERC20Mock} from "../test/mocks/ERC20Mock.sol"; + +contract HelperConfig is Script { + struct NetworkConfig { + address wethUsdPriceFeed; + address wbtcUsdPriceFeed; + address weth; + address wbtc; + uint256 deployerKey; + } + + NetworkConfig public activeNetworkConfig; + uint8 public constant DECIMALS = 8; + int256 public constant ETH_USD_PRICE = 2000e8; + int256 public constant BTC_USD_PRICE = 1000e8; + uint256 public DEFAULT_ANVIL_PRIVATE_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + + constructor() { + if (block.chainid == 11_155_111) { + activeNetworkConfig = getSepoliaEthConfig(); + } else { + activeNetworkConfig = getOrCreateAnvilEthConfig(); + } + } + + function getSepoliaEthConfig() public view returns (NetworkConfig memory) { + return NetworkConfig({ + wethUsdPriceFeed: 0x694AA1769357215DE4FAC081bf1f309aDC325306, + wbtcUsdPriceFeed: 0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43, + weth: 0xdd13E55209Fd76AfE204dBda4007C227904f0a81, + wbtc: 0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063, + deployerKey: vm.envUint("PRIVATE_KEY") + }); + } + + function getOrCreateAnvilEthConfig() public returns (NetworkConfig memory anvilNetworkConfig) { + if (activeNetworkConfig.wethUsdPriceFeed != address(0)) { + return activeNetworkConfig; + } + + vm.startBroadcast(); + MockV3Aggregator ethUsdPriceFeeds = new MockV3Aggregator(DECIMALS, ETH_USD_PRICE); + ERC20Mock wethMock = new ERC20Mock("WETH", "WETH", msg.sender, 1000e8); + MockV3Aggregator btcUsdPriceFeed = new MockV3Aggregator(DECIMALS, BTC_USD_PRICE); + ERC20Mock wbtcMock = new ERC20Mock("WBTC", "WBTC", msg.sender, 1000e8); + vm.stopBroadcast(); + + anvilNetworkConfig = NetworkConfig({ + wethUsdPriceFeed: address(ethUsdPriceFeeds), + wbtcUsdPriceFeed: address(btcUsdPriceFeed), + weth: address(wethMock), + wbtc: address(wbtcMock), + deployerKey: DEFAULT_ANVIL_PRIVATE_KEY + }); + } +} diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/DSCEngine.sol b/src/DSCEngine.sol new file mode 100644 index 0000000..30e9d9b --- /dev/null +++ b/src/DSCEngine.sol @@ -0,0 +1,431 @@ +// 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 {DecentralizedStableCoin} from "./DecentralizedStableCoin.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {OracleLib} from "./libraries/OracleLib.sol"; +import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; + +/* + * @title DSCEngine + * @author Shivang Rawat + * + * 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__NeedsMoreThanZero(); + error DSCEngine__NotAllowedToken(); + error DSCEngine__TokenAddresesAndPriceFeedAddressesMustBeSameLength(); + error DSCEngine__TransferFailed(); + error DSCEngine__BreaksHealthFactor(uint256 healthFactorValue); + error DSCEngine__MintFailed(); + error DSCEngine__HealthFactorOk(); + error DSCEngine__HealthFactorNotImproved(); + + ///////////////////// + // TYPES + ///////////////////// + + using OracleLib for AggregatorV3Interface; + + ///////////////////// + // state variables + ///////////////////// + 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; // This means you get assets at a 10% discount when liquidating + + mapping(address token => address priceFeed) private s_priceFeeds; + 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 amount); + event CollateralRedeemed( + address indexed redeemedFrom, address indexed redeemedTo, address collateralTokenAddress, 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__NotAllowedToken(); + } + _; + } + + ///////////////////// + // functions + ///////////////////// + + constructor(address[] memory tokenAddresses, address[] memory priceFeedAddresses, address dscAddress) { + if (tokenAddresses.length != priceFeedAddresses.length) { + revert DSCEngine__TokenAddresesAndPriceFeedAddressesMustBeSameLength(); + } + + 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); + } + + /* + * @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(); + } + } + + function redeemCollateral(address tokenCollateralAddress, uint256 amountCollateral) + public + moreThanZero(amountCollateral) + nonReentrant + { + _redeemCollateral(tokenCollateralAddress, amountCollateral, msg.sender, msg.sender); + _revertIfHealthFactorIsBroken(msg.sender); + } + + /* + * @notice follows CEI + * @params 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) { + s_DSCMinted[msg.sender] += amountDscToMint; + _revertIfHealthFactorIsBroken(msg.sender); + bool minted = i_dsc.mint(msg.sender, amountDscToMint); + if (!minted) { + revert DSCEngine__MintFailed(); + } + } + + /* + * @param tokenCollateralAddress: The ERC20 token address of the collateral you're depositing + * @param amountCollateral: The amount of collateral you're depositing + * @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); + redeemCollateral(tokenCollateralAddress, amountCollateral); + _revertIfHealthFactorIsBroken(msg.sender); + // redeemCollateral already checks health factor + } + + function burnDsc(uint256 amount) public moreThanZero(amount) { + _burnDsc(amount, msg.sender, msg.sender); + _revertIfHealthFactorIsBroken(msg.sender); + } + + /* + * @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(); + } + // We want to burn their DSC "debt" + // And take their collateral + // Bad User: $140 ETH, $100 DSC + // debtToCover = $100? + // $100 of DSC = ??? + // 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 + uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / LIQUIDATION_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 <= startingUserHealthFactor) { + revert DSCEngine__HealthFactorNotImproved(); + } + _revertIfHealthFactorIsBroken(msg.sender); + } + + function getHealthFactor() external view {} + + function calculateHealthFactor(uint256 totalDscMinted, uint256 collateralValueInUsd) + external + pure + returns (uint256) + { + return _calculateHealthFactor(totalDscMinted, collateralValueInUsd); + } + + /////////////////////////////////////// + // Private and Internal View 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); + if (!success) { + revert DSCEngine__TransferFailed(); + } + i_dsc.burn(amountDscToBurn); + } + + 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 _getAccountInformation(address user) + private + view + returns (uint256 totalDscMinted, uint256 collateralValueToUsd) + { + totalDscMinted = s_DSCMinted[user]; + collateralValueToUsd = getAccountCollateralValueInUSD(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); + } + + 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 * PRECISION) / totalDscMinted; + // return collateralValueInUSD / totalDscMinted; + } + + /////////////////////////////////////// + // Public and External View Functions + ////////////////////////////////////// + + 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; + return (usdAmountInWei * PRECISION) / (uint256(price) * ADDITIONAL_FEED_PRECISION); + } + + function getAccountCollateralValueInUSD(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) private view returns (uint256) { + AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]); + (, int256 price,,,) = priceFeed.staleCheckLatestRoundData(); + return ((uint256(price) * ADDITIONAL_FEED_PRECISION) * amount) / PRECISION; + } + + function getUsdValue( + address token, + uint256 amount // in WEI + ) external view returns (uint256) { + return _getUsdValue(token, amount); + } + + function getAccountInformation(address user) + external + view + returns (uint256 totalDscMinted, uint256 collateralValueInUsd) + { + (totalDscMinted, collateralValueInUsd) = _getAccountInformation(user); + } + + 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 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); + } +} diff --git a/src/DecentralizedStableCoin.sol b/src/DecentralizedStableCoin.sol new file mode 100644 index 0000000..7d8bb95 --- /dev/null +++ b/src/DecentralizedStableCoin.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT + +// This is considered an Exogenous, Decentralized, Anchored (pegged), Crypto Collateralized low volitility coin + +// 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.19; + +import {ERC20Burnable, ERC20} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/* + * @title DecentralizedStableCoin + * @author Shivang Rawat + * Collateral: Exogenous + * Minting (Stability Mechanism): Decentralized (Algorithmic) + * Value (Relative Stability): Anchored (Pegged to USD) + * Collateral Type: Crypto + * +* This is the contract meant to be owned by DSCEngine. It is a ERC20 token that can be minted and burned by the +DSCEngine smart contract. + */ + +contract DecentralizedStableCoin is ERC20Burnable, Ownable { + constructor() ERC20("DecentralizedStableCoin", "DSC") {} + + error DecentralizedStableCoin__MustBeMoreThanZero(); + error DecentralizedStableCoin__BurnAmountExceedsBalance(); + error DecentralizedStableCoin__NotZeroAddress(); + + function burn(uint256 _amount) public override onlyOwner { + uint256 balance = balanceOf(msg.sender); + if (_amount <= 0) { + revert DecentralizedStableCoin__MustBeMoreThanZero(); + } + if (balance < _amount) { + revert DecentralizedStableCoin__BurnAmountExceedsBalance(); + } + super.burn(_amount); + } + + function mint(address _to, uint256 _amount) external onlyOwner returns (bool) { + if (_to == address(0)) { + revert DecentralizedStableCoin__NotZeroAddress(); + } + if (_amount <= 0) { + revert DecentralizedStableCoin__MustBeMoreThanZero(); + } + _mint(_to, _amount); + return true; + } +} diff --git a/src/libraries/OracleLib.sol b/src/libraries/OracleLib.sol new file mode 100644 index 0000000..e8f70e7 --- /dev/null +++ b/src/libraries/OracleLib.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; + +/* + * @title OracleLib + * @author Shivang Rawat + * @notice This library is used to check the Chainlink Oracle for stale data. + * If a price is stale, functions will revert, and render the DSCEngine unusable - this is by design. + * We want the DSCEngine to freeze if prices become stale. + * + * So if the Chainlink network explodes and you have a lot of money locked in the protocol... too bad. + */ + +library OracleLib { + error OracleLib__StalePrice(); + + uint256 private constant TIMEOUT = 3 hours; + + function staleCheckLatestRoundData( + AggregatorV3Interface chainlinkFeed + ) public view returns (uint80, int256, uint256, uint256, uint80) { + ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) = chainlinkFeed.latestRoundData(); + + uint256 secondsSince = block.timestamp - updatedAt; + + if(secondsSince > TIMEOUT) revert OracleLib__StalePrice(); + + return (roundId, answer, startedAt, updatedAt, answeredInRound); + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/fuzz/Handler.t.sol b/test/fuzz/Handler.t.sol new file mode 100644 index 0000000..2516323 --- /dev/null +++ b/test/fuzz/Handler.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +// Handler is going to narrow down the way we call function + +pragma solidity ^0.8.18; + +import {Test, console} from "forge-std/Test.sol"; +import {DSCEngine} from "../../src/DSCEngine.sol"; +import {DecentralizedStableCoin} from "../../src/DecentralizedStableCoin.sol"; +import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; +import {MockV3Aggregator} from "../mocks/MockV3Aggregator.sol"; + +// Price Feed + +contract Handler is Test { + DSCEngine dsce; + DecentralizedStableCoin dsc; + + ERC20Mock weth; + ERC20Mock wbtc; + + uint256 public timesMintIsCalled; + address[] public usersWithCollateralDeposited; + MockV3Aggregator public ethUsdPriceFeed; + + uint256 MAX_DEPOSIT_SIZE = type(uint96).max; // the max uint96 value + + constructor(DSCEngine _dscEngine, DecentralizedStableCoin _dsc) { + dsce = _dscEngine; + dsc = _dsc; + + address[] memory collateralTokens = dsce.getCollateralTokens(); + weth = ERC20Mock(collateralTokens[0]); + wbtc = ERC20Mock(collateralTokens[1]); + + ethUsdPriceFeed = MockV3Aggregator(dsce.getCollateralTokenPriceFeed(address(weth))); + } + + function mintDsc(uint256 amount, uint256 addressSeed) public { + if (usersWithCollateralDeposited.length == 0) { + return; + } + address sender = usersWithCollateralDeposited[addressSeed % usersWithCollateralDeposited.length]; + (uint256 totalDscMinted, uint256 collateralValueInUsd) = dsce.getAccountInformation(sender); + + int256 maxDscToMint = (int256(collateralValueInUsd) / 2) - int256(totalDscMinted); + + if (maxDscToMint < 0) { + return; + } + amount = bound(amount, 0, uint256(maxDscToMint)); + if (amount == 0) { + return; + } + vm.startPrank(sender); + dsce.mintDsc(amount); + vm.stopPrank(); + timesMintIsCalled++; + } + + function depositCollateral(uint256 collateralSeed, uint256 amountCollateral) public { + ERC20Mock collateral = _getCollateralFromSeed(collateralSeed); + amountCollateral = bound(amountCollateral, 1, MAX_DEPOSIT_SIZE); + + vm.startPrank(msg.sender); + collateral.mint(msg.sender, amountCollateral); + collateral.approve(address(dsce), amountCollateral); + dsce.depositCollateral(address(collateral), amountCollateral); + vm.stopPrank(); + usersWithCollateralDeposited.push(msg.sender); + } + + function redeemCollateral(uint256 collateralSeed, uint256 amountCollateral) public { + ERC20Mock collateral = _getCollateralFromSeed(collateralSeed); + uint256 maxCollateralToRedeem = dsce.getCollateralBalanceOfUser(msg.sender, address(collateral)); + amountCollateral = bound(amountCollateral, 0, maxCollateralToRedeem); + if (amountCollateral == 0) { + return; + } + dsce.redeemCollateral(address(collateral), amountCollateral); + } + + // This breaks our invariant test suite!!!! + // function updateCollateralPrice(uint96 newPrice) public { + // int256 newPriceInt = int256(uint256(newPrice)); + // ethUsdPriceFeed.updateAnswer(newPriceInt); + // } + + // Helper Functions + function _getCollateralFromSeed(uint256 collateralSeed) private view returns (ERC20Mock) { + if (collateralSeed % 2 == 0) { + return weth; + } + return wbtc; + } +} \ No newline at end of file diff --git a/test/fuzz/Invariants.t.sol b/test/fuzz/Invariants.t.sol new file mode 100644 index 0000000..fe708d0 --- /dev/null +++ b/test/fuzz/Invariants.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT + +// Have our invariant aka properties + +// What are our invariants? + +// 1. The total supply of DSC should be less than the total value of collateral +// 2. Getter view functions should never revert <- evergreen invariant + +pragma solidity ^0.8.18; + +import {Test, console} from "forge-std/Test.sol"; +import {StdInvariant} from "forge-std/StdInvariant.sol"; +import {DeployDSC} from "../../script/DeployDSC.s.sol"; +import {DSCEngine} from "../../src/DSCEngine.sol"; +import {DecentralizedStableCoin} from "../../src/DecentralizedStableCoin.sol"; +import {HelperConfig} from "../../script/HelperConfig.s.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Handler} from "./Handler.t.sol"; + +contract Invariants is StdInvariant, Test { + DeployDSC deployer; + DSCEngine dsce; + DecentralizedStableCoin dsc; + HelperConfig config; + address weth; + address wbtc; + Handler handler; + + function setUp() external { + deployer = new DeployDSC(); + (dsc, dsce, config) = deployer.run(); + (,, weth, wbtc,) = config.activeNetworkConfig(); + // targetContract(address(dsce)); + handler = new Handler(dsce, dsc); + targetContract(address(handler)); + } + + function invariant_protocolMustHaveMoreValueThanTotalSupply() public view { + // get the value of all the collateral in the protocol + // compare it to all the debt (dsc) + uint256 totalSupply = dsc.totalSupply(); + uint256 totalWethDeposited = IERC20(weth).balanceOf(address(dsce)); + uint256 totalBtcDeposited = IERC20(wbtc).balanceOf(address(dsce)); + + uint256 wethValue = dsce.getUsdValue(weth, totalWethDeposited); + uint256 wbtcValue = dsce.getUsdValue(wbtc, totalBtcDeposited); + + console.log("weth value: ", wethValue); + console.log("wbtc value: ", wbtcValue); + console.log("total supply: ", totalSupply); + console.log("Times mint called: ", handler.timesMintIsCalled()); + + assert(wethValue + wbtcValue >= totalSupply); + } + + function invariant_gettersCantRevert() public view { + dsce.getAdditionalFeedPrecision(); + dsce.getCollateralTokens(); + dsce.getLiquidationBonus(); + dsce.getLiquidationBonus(); + dsce.getLiquidationThreshold(); + dsce.getMinHealthFactor(); + dsce.getPrecision(); + dsce.getDsc(); + // dsce.getTokenAmountFromUsd(); + // dsce.getCollateralTokenPriceFeed(); + // dsce.getCollateralBalanceOfUser(); + // getAccountCollateralValue(); + } + + // Should add more invariant tests for the rest of the getters... +} \ No newline at end of file diff --git a/test/fuzz/OpenInvariantsTest.t.sol b/test/fuzz/OpenInvariantsTest.t.sol new file mode 100644 index 0000000..a6dd17b --- /dev/null +++ b/test/fuzz/OpenInvariantsTest.t.sol @@ -0,0 +1,48 @@ +// // What are our Invariants? +// //1. Total suppy of DSC should be less than total value of collateral +// //2. Getter view functions should never revert <- evengreen invariant + +// // SPDX-License-Identifier: MIT + +// pragma solidity 0.8.19; + +// import { Test,console } from "forge-std/Test.sol"; +// import { StdInvariant } from "forge-std/StdInvariant.sol"; +// import { DSCEngine } from "../../src/DSCEngine.sol"; +// import { DecentralizedStableCoin } from "../../src/DecentralizedStableCoin.sol"; +// import { HelperConfig } from "../../script/HelperConfig.s.sol"; +// import { DeployDSC } from "../../script/DeployDSC.s.sol"; +// import { ERC20Mock } from "../mocks/ERC20Mock.sol"; +// import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// contract OpenInvariantsTest is StdInvariant, Test{ +// DeployDSC deployer; +// DSCEngine dsce; +// DecentralizedStableCoin dsc; +// HelperConfig config; +// address weth; +// address wbtc; + +// function setUp() external { +// deployer = new DeployDSC(); +// (dsc, dsce, config) = deployer.run(); +// (,,weth,wbtc,) = config.activeNetworkConfig(); +// targetContract(address(dsce)); +// } + +// function invariant_protocolMustHaveMoreValueThanTotalSupply() public view { +// // get the value of all the collateral in the protocol +// // compare it to all the debt (dsc) +// uint256 totalSupply = dsc.totalSupply(); +// uint256 wethDeposited = IERC20(weth).balanceOf(address(dsce)); +// uint256 wbtcDeposited = IERC20(wbtc).balanceOf(address(dsce)); + +// uint256 wethValue = dsce.getUsdValue(weth, wethDeposited); +// uint256 wbtcValue = dsce.getUsdValue(wbtc, wbtcDeposited); + +// console.log("wethValue: %s", wethValue); +// console.log("wbtcValue: %s", wbtcValue); + +// assert(wethValue + wbtcValue >= totalSupply); +// } +// } diff --git a/test/mocks/ERC20Mock.sol b/test/mocks/ERC20Mock.sol new file mode 100644 index 0000000..15c2432 --- /dev/null +++ b/test/mocks/ERC20Mock.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ERC20Mock is ERC20 { + constructor(string memory name, string memory symbol, address initialAccount, uint256 initialBalance) + payable + ERC20(name, symbol) + { + _mint(initialAccount, initialBalance); + } + + function mint(address account, uint256 amount) public { + _mint(account, amount); + } + + function burn(address account, uint256 amount) public { + _burn(account, amount); + } + + function transferInternal(address from, address to, uint256 value) public { + _transfer(from, to, value); + } + + function approveInternal(address owner, address spender, uint256 value) public { + _approve(owner, spender, value); + } +} diff --git a/test/mocks/MockV3Aggregator.sol b/test/mocks/MockV3Aggregator.sol new file mode 100644 index 0000000..2969363 --- /dev/null +++ b/test/mocks/MockV3Aggregator.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title MockV3Aggregator + * @notice Based on the FluxAggregator contract + * @notice Use this contract when you need to test + * other contract's ability to read data from an + * aggregator contract, but how the aggregator got + * its answer is unimportant + */ +contract MockV3Aggregator { + uint256 public constant version = 0; + + uint8 public decimals; + int256 public latestAnswer; + uint256 public latestTimestamp; + uint256 public latestRound; + + mapping(uint256 => int256) public getAnswer; + mapping(uint256 => uint256) public getTimestamp; + mapping(uint256 => uint256) private getStartedAt; + + constructor(uint8 _decimals, int256 _initialAnswer) { + decimals = _decimals; + updateAnswer(_initialAnswer); + } + + function updateAnswer(int256 _answer) public { + latestAnswer = _answer; + latestTimestamp = block.timestamp; + latestRound++; + getAnswer[latestRound] = _answer; + getTimestamp[latestRound] = block.timestamp; + getStartedAt[latestRound] = block.timestamp; + } + + function updateRoundData(uint80 _roundId, int256 _answer, uint256 _timestamp, uint256 _startedAt) public { + latestRound = _roundId; + latestAnswer = _answer; + latestTimestamp = _timestamp; + getAnswer[latestRound] = _answer; + getTimestamp[latestRound] = _timestamp; + getStartedAt[latestRound] = _startedAt; + } + + function getRoundData(uint80 _roundId) + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId); + } + + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + return ( + uint80(latestRound), + getAnswer[latestRound], + getStartedAt[latestRound], + getTimestamp[latestRound], + uint80(latestRound) + ); + } + + function description() external pure returns (string memory) { + return "v0.6/tests/MockV3Aggregator.sol"; + } +} diff --git a/test/unit/DSCEngineTest.t.sol b/test/unit/DSCEngineTest.t.sol new file mode 100644 index 0000000..b36d9e7 --- /dev/null +++ b/test/unit/DSCEngineTest.t.sol @@ -0,0 +1,411 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {DeployDSC} from "../../script/DeployDSC.s.sol"; +import {DSCEngine} from "../../src/DSCEngine.sol"; +import {DecentralizedStableCoin} from "../../src/DecentralizedStableCoin.sol"; +import {HelperConfig} from "../../script/HelperConfig.s.sol"; +import {Test, console} from "forge-std/Test.sol"; +import {ERC20Mock} from "../mocks/ERC20Mock.sol"; +import {MockV3Aggregator} from "../mocks/MockV3Aggregator.sol"; + +contract DSCEngineTest is Test { + event CollateralRedeemed(address indexed redeemFrom, address indexed redeemTo, address token, uint256 amount); // if + // redeemFrom != redeemedTo, then it was liquidated + + DeployDSC deployer; + DecentralizedStableCoin dsc; + DSCEngine dsce; + HelperConfig config; + address ethUsdPriceFeed; + address weth; + + address public USER = makeAddr("user"); + uint256 AMOUNT_COLLATERAL = 10 ether; + uint256 AMOUNT_TO_MINT = 100 ether; + uint256 STARTING_ERC20_BALANCE = 10 ether; + + uint256 public constant STARTING_USER_BALANCE = 10 ether; + uint256 public constant MIN_HEALTH_FACTOR = 1e18; + uint256 public constant LIQUIDATION_THRESHOLD = 50; + + // Liquidation + address public liquidator = makeAddr("liquidator"); + uint256 public collateralToCover = 20 ether; + + function setUp() public { + deployer = new DeployDSC(); + (dsc, dsce, config) = deployer.run(); + (ethUsdPriceFeed,, weth,,) = config.activeNetworkConfig(); + + ERC20Mock(weth).mint(USER, STARTING_ERC20_BALANCE); + } + + ///////////////////// + // Constructor Tests + ///////////////////// + address[] public tokenAddresses; + address[] public feedAddresses; + + function testRevertsIfTokenLengthDoesntMatchPriceFeeds() public { + tokenAddresses.push(weth); + feedAddresses.push(ethUsdPriceFeed); + feedAddresses.push(ethUsdPriceFeed); + + vm.expectRevert(DSCEngine.DSCEngine__TokenAddresesAndPriceFeedAddressesMustBeSameLength.selector); + new DSCEngine(tokenAddresses, feedAddresses, address(dsc)); + } + + /////////////////// + // Price Tests + /////////////////// + + function testGetTokenAmountFromUsd() public { + // If we want $100 of WETH @$2000/WETH, that would be 0.05 WETH + uint256 expectedWeth = 0.05 ether; + uint256 amountWeth = dsce.getTokenAmountFromUsd(weth, 100 ether); + assertEq(amountWeth, expectedWeth); + } + + function testGetUsdValue() public { + uint256 ethAmount = 15e18; + uint256 expectedUsd = 30000e18; + uint256 actualUsd = dsce.getUsdValue(weth, ethAmount); + assertEq(expectedUsd, actualUsd); + } + + //////////////////////////// + // DepositCollateral Tests + //////////////////////////// + + function testRevertsIfCollateralZero() public { + vm.startPrank(USER); + ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); + + vm.expectRevert(DSCEngine.DSCEngine__NeedsMoreThanZero.selector); + dsce.depositCollateral(weth, 0); + vm.stopPrank(); + } + + function testRevertsWithUnapprovedCollateral() public { + ERC20Mock randToken = new ERC20Mock("RAN", "RAN", USER, 100e18); + vm.startPrank(USER); + vm.expectRevert(DSCEngine.DSCEngine__NotAllowedToken.selector); + dsce.depositCollateral(address(randToken), 100e18); + vm.stopPrank(); + } + + modifier depositCollateral() { + vm.startPrank(USER); + ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); + dsce.depositCollateral(weth, AMOUNT_COLLATERAL); + vm.stopPrank(); + _; + } + + function testCanDepositCollateralWithoutMinting() public depositCollateral { + uint256 balanceOf = dsc.balanceOf(USER); + assertEq(balanceOf, 0); + } + + function testCanDepositCollateralAndGetAccountInfo() public depositCollateral { + (uint256 totalDscMinted, uint256 collateralValueInUsd) = dsce.getAccountInformation(USER); + uint256 expectedDepositAmount = dsce.getTokenAmountFromUsd(weth, collateralValueInUsd); + assertEq(totalDscMinted, 0); + assertEq(expectedDepositAmount, AMOUNT_COLLATERAL); + } + + /////////////////////////////////////// + // depositCollateralAndMintDsc Tests // + /////////////////////////////////////// + + function testRevertsIfMintedDscBreaksHealthFactor() public { + (, int256 price,,,) = MockV3Aggregator(ethUsdPriceFeed).latestRoundData(); + AMOUNT_TO_MINT = + (AMOUNT_COLLATERAL * (uint256(price) * dsce.getAdditionalFeedPrecision())) / dsce.getPrecision(); + vm.startPrank(USER); + ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); + + uint256 expectedHealthFactor = + dsce.calculateHealthFactor(AMOUNT_TO_MINT, dsce.getUsdValue(weth, AMOUNT_COLLATERAL)); + vm.expectRevert(abi.encodeWithSelector(DSCEngine.DSCEngine__BreaksHealthFactor.selector, expectedHealthFactor)); + dsce.depositCollateralAndMintDsc(weth, AMOUNT_COLLATERAL, AMOUNT_TO_MINT); + vm.stopPrank(); + } + + modifier depositedCollateralAndMintedDsc() { + vm.startPrank(USER); + ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); + dsce.depositCollateralAndMintDsc(weth, AMOUNT_COLLATERAL, AMOUNT_TO_MINT); + vm.stopPrank(); + _; + } + + function testCanMintWithDepositedCollateral() public depositedCollateralAndMintedDsc { + uint256 userBalance = dsc.balanceOf(USER); + assertEq(userBalance, AMOUNT_TO_MINT); + } + + /////////////////////////////////// + // mintDsc Tests // + /////////////////////////////////// + + function testRevertsIfMintAmountIsZero() public { + vm.startPrank(USER); + ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); + dsce.depositCollateralAndMintDsc(weth, AMOUNT_COLLATERAL, AMOUNT_TO_MINT); + vm.expectRevert(DSCEngine.DSCEngine__NeedsMoreThanZero.selector); + dsce.mintDsc(0); + vm.stopPrank(); + } + + function testRevertsIfMintAmountBreaksHealthFactor() public depositCollateral { + (, int256 price,,,) = MockV3Aggregator(ethUsdPriceFeed).latestRoundData(); + AMOUNT_TO_MINT = + (AMOUNT_COLLATERAL * (uint256(price) * dsce.getAdditionalFeedPrecision())) / dsce.getPrecision(); + vm.startPrank(USER); + uint256 expectedHealthFactor = + dsce.calculateHealthFactor(AMOUNT_TO_MINT, dsce.getUsdValue(weth, AMOUNT_COLLATERAL)); + vm.expectRevert(abi.encodeWithSelector(DSCEngine.DSCEngine__BreaksHealthFactor.selector, expectedHealthFactor)); + dsce.mintDsc(AMOUNT_TO_MINT); + vm.stopPrank(); + } + + /////////////////////////////////// + // burnDsc Tests // + /////////////////////////////////// + + function testRevertsIfBurnAmountIsZero() public { + vm.startPrank(USER); + ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); + dsce.depositCollateralAndMintDsc(weth, AMOUNT_COLLATERAL, AMOUNT_TO_MINT); + vm.expectRevert(DSCEngine.DSCEngine__NeedsMoreThanZero.selector); + dsce.burnDsc(0); + vm.stopPrank(); + } + + function testCantBurnMoreThanUserHas() public { + vm.prank(USER); + vm.expectRevert(); + dsce.burnDsc(1); + } + + function testCanBurnDsc() public depositedCollateralAndMintedDsc { + vm.startPrank(USER); + dsc.approve(address(dsce), AMOUNT_TO_MINT); + dsce.burnDsc(AMOUNT_TO_MINT); + vm.stopPrank(); + + uint256 userBalance = dsc.balanceOf(USER); + assertEq(userBalance, 0); + } + + /////////////////////////////////// + // redeemCollateral Tests // + ////////////////////////////////// + + function testRevertsIfRedeemAmountIsZero() public { + vm.startPrank(USER); + ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); + dsce.depositCollateralAndMintDsc(weth, AMOUNT_COLLATERAL, AMOUNT_TO_MINT); + vm.expectRevert(DSCEngine.DSCEngine__NeedsMoreThanZero.selector); + dsce.redeemCollateral(weth, 0); + vm.stopPrank(); + } + + function testCanRedeemCollateral() public depositCollateral { + vm.startPrank(USER); + dsce.redeemCollateral(weth, AMOUNT_COLLATERAL); + uint256 userBalance = ERC20Mock(weth).balanceOf(USER); + assertEq(userBalance, AMOUNT_COLLATERAL); + } + + // function testEmitCollateralRedeemedWithCorrectArgs() public depositCollateral { + // vm.expectEmit(true, true, true, true, address(dsce)); + // emit CollateralRedeemed(USER, USER, weth, AMOUNT_COLLATERAL); + // vm.startPrank(USER); + // dsce.redeemCollateral(weth, AMOUNT_COLLATERAL); + // vm.stopPrank(); + // } + + /////////////////////////////////// + // redeemCollateralForDsc Tests // + ////////////////////////////////// + + function testMustRedeemMoreThanZero() public depositedCollateralAndMintedDsc { + vm.startPrank(USER); + // dsce.redeemCollateral(weth, AMOUNT_COLLATERAL); + vm.expectRevert(DSCEngine.DSCEngine__NeedsMoreThanZero.selector); + dsce.redeemCollateralForDsc(weth, 0, AMOUNT_TO_MINT); + vm.stopPrank(); + } + + function testCanRedeemDepositedCollateral() public { + vm.startPrank(USER); + ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); + dsce.depositCollateralAndMintDsc(weth, AMOUNT_COLLATERAL, AMOUNT_TO_MINT); + dsc.approve(address(dsce), AMOUNT_TO_MINT); + dsce.redeemCollateralForDsc(weth, AMOUNT_COLLATERAL, AMOUNT_TO_MINT); + vm.stopPrank(); + + uint256 userBalance = dsc.balanceOf(USER); + assertEq(userBalance, 0); + } + + //////////////////////// + // healthFactor Tests // + //////////////////////// + + function testProperlyReportsHealthFactor() public depositedCollateralAndMintedDsc { + uint256 expectedHealthFactor = 100 ether; + uint256 healthFactor = dsce.getHealthFactor(USER); + // $100 minted with $20,000 collateral at 50% liquidation threshold + // means that we must have $200 collatareral at all times. + // 20,000 * 0.5 = 10,000 + // 10,000 / 100 = 100 health factor + assertEq(healthFactor, expectedHealthFactor); + } + + function testHealthFactorCanGoBelowOne() public depositedCollateralAndMintedDsc { + int256 ethUsdUpdatedPrice = 18e8; + // Rememeber, we need $200 at all times if we have $100 of debt + + MockV3Aggregator(ethUsdPriceFeed).updateAnswer(ethUsdUpdatedPrice); + + uint256 userHealthFactor = dsce.getHealthFactor(USER); + // 180*50 /100 / 100 == 90/100 == 0.9 + assert(userHealthFactor == 0.9 ether); + } + + /////////////////////// + // Liquidation Tests // + /////////////////////// + + function testCantLiquidateGoodHealthFactor() public depositedCollateralAndMintedDsc { + ERC20Mock(weth).mint(liquidator, collateralToCover); + + vm.startPrank(liquidator); + ERC20Mock(weth).approve(address(dsce), collateralToCover); + dsce.depositCollateralAndMintDsc(weth, AMOUNT_COLLATERAL, AMOUNT_TO_MINT); + dsc.approve(address(dsce), AMOUNT_TO_MINT); + + vm.expectRevert(DSCEngine.DSCEngine__HealthFactorOk.selector); + dsce.liquidate(weth, USER, AMOUNT_TO_MINT); + vm.stopPrank(); + } + + modifier liquidated() { + vm.startPrank(USER); + ERC20Mock(weth).approve(address(dsce), collateralToCover); + dsce.depositCollateralAndMintDsc(weth, AMOUNT_COLLATERAL, AMOUNT_TO_MINT); + vm.stopPrank(); + int256 ethUsdUpdatedPrice = 18e8; + + MockV3Aggregator(ethUsdPriceFeed).updateAnswer(ethUsdUpdatedPrice); + uint256 userHealthFactor = dsce.getHealthFactor(USER); + + ERC20Mock(weth).mint(liquidator, collateralToCover); + + vm.startPrank(liquidator); + ERC20Mock(weth).approve(address(dsce), collateralToCover); + dsce.depositCollateralAndMintDsc(weth, collateralToCover, AMOUNT_TO_MINT); + dsc.approve(address(dsce), AMOUNT_TO_MINT); + dsce.liquidate(weth, USER, AMOUNT_TO_MINT); + vm.stopPrank(); + _; + } + + function testLiquidationPayoutIsCorrect() public liquidated { + uint256 liquidatorWethBalance = ERC20Mock(weth).balanceOf(liquidator); + uint256 expectedWeth = dsce.getTokenAmountFromUsd(weth, AMOUNT_TO_MINT) + + (dsce.getTokenAmountFromUsd(weth, AMOUNT_TO_MINT) / dsce.getLiquidationBonus()); + assertEq(liquidatorWethBalance, expectedWeth); + } + + function testUserHasSomeEthAfterLiquidation() public liquidated { + uint256 amountLiquidated = dsce.getTokenAmountFromUsd(weth, AMOUNT_TO_MINT) + + (dsce.getTokenAmountFromUsd(weth, AMOUNT_TO_MINT) / dsce.getLiquidationBonus()); + uint256 usdAmountLiquidated = dsce.getUsdValue(weth, amountLiquidated); + uint256 expectedUserCollateralValueInUsd = dsce.getUsdValue(weth, AMOUNT_COLLATERAL) - usdAmountLiquidated; + (, uint256 userCollateralValueInUsd) = dsce.getAccountInformation(USER); + assertEq(userCollateralValueInUsd, expectedUserCollateralValueInUsd); + } + + function testLiquidatorTakesOnUsersDebt() public liquidated { + (uint256 liquidatorDscMinted,) = dsce.getAccountInformation(liquidator); + assertEq(liquidatorDscMinted, AMOUNT_TO_MINT); + } + + function testUserHasNoMoreDebt() public liquidated { + (uint256 userDscMinted,) = dsce.getAccountInformation(USER); + assertEq(userDscMinted, 0); + } + + /////////////////////////////////// + // View & Pure Function Tests // + ////////////////////////////////// + + function testGetCollateralTokenPriceFeed() public { + address priceFeed = dsce.getCollateralTokenPriceFeed(weth); + assertEq(priceFeed, ethUsdPriceFeed); + } + + function testGetCollateralTokens() public { + address[] memory collateralTokens = dsce.getCollateralTokens(); + assertEq(collateralTokens[0], weth); + } + + function testGetMinHealthFactor() public { + uint256 minHealthFactor = dsce.getMinHealthFactor(); + assertEq(minHealthFactor, MIN_HEALTH_FACTOR); + } + + function testGetLiquidationThreshold() public { + uint256 liquidationThreshold = dsce.getLiquidationThreshold(); + assertEq(liquidationThreshold, LIQUIDATION_THRESHOLD); + } + + modifier depositedCollateral() { + vm.startPrank(USER); + ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); + dsce.depositCollateral(weth, AMOUNT_COLLATERAL); + vm.stopPrank(); + _; + } + + function testGetAccountCollateralValueFromInformation() public depositedCollateral { + (, uint256 collateralValue) = dsce.getAccountInformation(USER); + uint256 expectedCollateralValue = dsce.getUsdValue(weth, AMOUNT_COLLATERAL); + assertEq(collateralValue, expectedCollateralValue); + } + + function testGetCollateralBalanceOfUser() public { + vm.startPrank(USER); + ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); + dsce.depositCollateral(weth, AMOUNT_COLLATERAL); + vm.stopPrank(); + uint256 collateralBalance = dsce.getCollateralBalanceOfUser(USER, weth); + assertEq(collateralBalance, AMOUNT_COLLATERAL); + } + + function testGetAccountCollateralValue() public { + vm.startPrank(USER); + ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); + dsce.depositCollateral(weth, AMOUNT_COLLATERAL); + vm.stopPrank(); + uint256 collateralValue = dsce.getAccountCollateralValue(USER); + uint256 expectedCollateralValue = dsce.getUsdValue(weth, AMOUNT_COLLATERAL); + assertEq(collateralValue, expectedCollateralValue); + } + + function testGetDsc() public { + address dscAddress = dsce.getDsc(); + assertEq(dscAddress, address(dsc)); + } + + function testLiquidationPrecision() public { + uint256 expectedLiquidatedPrecision = 100; + uint256 actualLiquidationPrecision = dsce.getLiquidationPrecision(); + assertEq(expectedLiquidatedPrecision, actualLiquidationPrecision); + } +}