diff --git a/src/libraries/OracleLib.sol b/src/libraries/OracleLib.sol index e8f70e7..dc622fe 100644 --- a/src/libraries/OracleLib.sol +++ b/src/libraries/OracleLib.sol @@ -19,20 +19,17 @@ library OracleLib { 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(); + 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(); + if (secondsSince > TIMEOUT) revert OracleLib__StalePrice(); return (roundId, answer, startedAt, updatedAt, answeredInRound); } diff --git a/test/fuzz/Handler.t.sol b/test/fuzz/Handler.t.sol index 2516323..8c41291 100644 --- a/test/fuzz/Handler.t.sol +++ b/test/fuzz/Handler.t.sol @@ -92,4 +92,4 @@ contract Handler is Test { } return wbtc; } -} \ No newline at end of file +} diff --git a/test/fuzz/Invariants.t.sol b/test/fuzz/Invariants.t.sol index fe708d0..faf81b1 100644 --- a/test/fuzz/Invariants.t.sol +++ b/test/fuzz/Invariants.t.sol @@ -1,73 +1,73 @@ -// SPDX-License-Identifier: MIT +// // SPDX-License-Identifier: MIT -// Have our invariant aka properties +// // Have our invariant aka properties -// What are our invariants? +// // 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 +// // 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; +// 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"; +// 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; +// 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 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)); +// 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); +// 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()); +// 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); - } +// 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(); - } +// 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 +// // Should add more invariant tests for the rest of the getters... +// } diff --git a/test/fuzz/continueOnRevert/ContinueOnRevertHandler.t.sol b/test/fuzz/continueOnRevert/ContinueOnRevertHandler.t.sol new file mode 100644 index 0000000..0e3dc53 --- /dev/null +++ b/test/fuzz/continueOnRevert/ContinueOnRevertHandler.t.sol @@ -0,0 +1,111 @@ +// // SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { Test } from "forge-std/Test.sol"; +// import { ERC20Mock } from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; Updated mock location +import { ERC20Mock } from "../../mocks/ERC20Mock.sol"; + +import { MockV3Aggregator } from "../../mocks/MockV3Aggregator.sol"; +import { DSCEngine, AggregatorV3Interface } from "../../../src/DSCEngine.sol"; +import { DecentralizedStableCoin } from "../../../src/DecentralizedStableCoin.sol"; +// import {Randomish, EnumerableSet} from "../Randomish.sol"; // Randomish is not found in the codebase, EnumerableSet +// is imported from openzeppelin +import { MockV3Aggregator } from "../../mocks/MockV3Aggregator.sol"; +import { console } from "forge-std/console.sol"; + +contract ContinueOnRevertHandler is Test { + DSCEngine public dscEngine; + DecentralizedStableCoin public dsc; + MockV3Aggregator public ethUsdPriceFeed; + MockV3Aggregator public btcUsdPriceFeed; + ERC20Mock public weth; + ERC20Mock public wbtc; + + uint96 public constant MAX_DEPOSIT_SIZE = type(uint96).max; + + constructor(DSCEngine _dscEngine, DecentralizedStableCoin _dsc) { + dscEngine = _dscEngine; + dsc = _dsc; + + address[] memory collateralTokens = dscEngine.getCollateralTokens(); + weth = ERC20Mock(collateralTokens[0]); + wbtc = ERC20Mock(collateralTokens[1]); + + ethUsdPriceFeed = MockV3Aggregator(dscEngine.getCollateralTokenPriceFeed(address(weth))); + btcUsdPriceFeed = MockV3Aggregator(dscEngine.getCollateralTokenPriceFeed(address(wbtc))); + + } + + // FUNCTOINS TO INTERACT WITH + + /////////////// + // DSCEngine // + /////////////// + + function mintAndDepositCollateral(uint256 collateralSeed, uint256 amountCollateral) public { + amountCollateral = bound(amountCollateral,0,MAX_DEPOSIT_SIZE); + ERC20Mock collateral = _getCollateralFromSeed(collateralSeed); + collateral.mint(msg.sender, amountCollateral); + dscEngine.depositCollateral(address(collateral), amountCollateral); + } + + function redeemCollateral(uint256 collateralSeed, uint256 amountCollateral) public { + amountCollateral = bound(amountCollateral,0,MAX_DEPOSIT_SIZE); + ERC20Mock collateral = _getCollateralFromSeed(collateralSeed); + dscEngine.redeemCollateral(address(collateral), amountCollateral); + } + + function burnDsc(uint256 amountDsc) public { + amountDsc = bound(amountDsc, 0, dsc.balanceOf(msg.sender)); + dsc.burn(amountDsc); + } + + function mintDsc(uint256 amountDsc) public { + amountDsc = bound(amountDsc,0 ,MAX_DEPOSIT_SIZE); + dsc.mint(msg.sender, amountDsc); + } + + function liquidate(uint256 collateralSeed, address userToBeLiquidated, uint256 debtToCover) public { + ERC20Mock collateral = _getCollateralFromSeed(collateralSeed); + dscEngine.liquidate(address(collateral), userToBeLiquidated, debtToCover); + } + + ///////////////////////////// + // DecentralizedStableCoin // + ///////////////////////////// + function transferDsc(uint256 amountDsc, address to) public { + amountDsc = bound(amountDsc, 0, dsc.balanceOf(msg.sender)); + vm.prank(msg.sender); + dsc.transfer(to, amountDsc); + } + + ///////////////////////////// + // Aggregator // + ///////////////////////////// + function updateCollateralPrice(uint128, /* newPrice */ uint256 collateralSeed) public { + // int256 intNewPrice = int256(uint256(newPrice)); + int256 intNewPrice = 0; + ERC20Mock collateral = _getCollateralFromSeed(collateralSeed); + MockV3Aggregator priceFeed = MockV3Aggregator(dscEngine.getCollateralTokenPriceFeed(address(collateral))); + + priceFeed.updateAnswer(intNewPrice); + } + + // Helper functions + function _getCollateralFromSeed(uint256 collateralSeed) private view returns (ERC20Mock) { + if(collateralSeed % 2 == 0){ + return weth; + } + else{ + return wbtc; + } + } + + function callSummary() external view { + console.log("Weth total deposited", weth.balanceOf(address(dscEngine))); + console.log("Wbtc total deposited", wbtc.balanceOf(address(dscEngine))); + console.log("Total supply of DSC", dsc.totalSupply()); + } +} \ No newline at end of file diff --git a/test/fuzz/continueOnRevert/ContinueOnRevertInvariants.t.sol b/test/fuzz/continueOnRevert/ContinueOnRevertInvariants.t.sol new file mode 100644 index 0000000..237afd1 --- /dev/null +++ b/test/fuzz/continueOnRevert/ContinueOnRevertInvariants.t.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import { Test } 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 { ContinueOnRevertHandler } from "./ContinueOnRevertHandler.t.sol"; +import { console } from "forge-std/console.sol"; + +contract ContinueOnRevertInvariants is StdInvariant, Test { + DSCEngine public dsce; + DecentralizedStableCoin public dsc; + HelperConfig public helperConfig; + + address public ethUsdPriceFeed; + address public btcUsdPriceFeed; + address public weth; + address public wbtc; + + uint256 amountCollateral = 10 ether; + uint256 amountToMint = 100 ether; + + uint256 public constant STARTING_USER_BALANCE = 10 ether; + address public constant USER = address(1); + uint256 public constant MIN_HEALTH_FACTOR = 1e18; + uint256 public constant LIQUIDATION_THRESHOLD = 50; + + // Liquidation + address public liquidator = makeAddr("liquidator"); + uint256 public collateralToCover = 20 ether; + + ContinueOnRevertHandler public handler; + + function setUp() public { + DeployDSC deployer = new DeployDSC(); + (dsc,dsce,helperConfig) = deployer.run(); + (ethUsdPriceFeed,btcUsdPriceFeed,weth,wbtc,) = helperConfig.activeNetworkConfig(); + handler = new ContinueOnRevertHandler(dsce,dsc); + targetContract(address(handler)); + } + + // function invariant_protocolMustHaveMoreValueThatTotalSupplyDollars() public view { + // uint256 totalSupply = dsc.totalSupply(); + // uint256 wethDeposited = ERC20Mock(weth).balanceOf(address(dsce)); + // uint256 wbtcDeposited = ERC20Mock(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); + // } + + // function invariant_callSummary() public view { + // handler.callSummary(); + // } +} diff --git a/test/fuzz/failOnRevert/FailOnRevertHandler.t.sol b/test/fuzz/failOnRevert/FailOnRevertHandler.t.sol new file mode 100644 index 0000000..ae2ca80 --- /dev/null +++ b/test/fuzz/failOnRevert/FailOnRevertHandler.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { Test } from "forge-std/Test.sol"; +// import { ERC20Mock } from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; Updated mock location +import { ERC20Mock } from "../../mocks/ERC20Mock.sol"; + +import { MockV3Aggregator } from "../../mocks/MockV3Aggregator.sol"; +import { DSCEngine, AggregatorV3Interface } from "../../../src/DSCEngine.sol"; +import { DecentralizedStableCoin } from "../../../src/DecentralizedStableCoin.sol"; +import { MockV3Aggregator } from "../../mocks/MockV3Aggregator.sol"; +import { console } from "forge-std/console.sol"; + +contract FailOnRevertHandler is Test { + using EnumerableSet for EnumerableSet.AddressSet; + + // Deployed contracts to interact with + DSCEngine public dscEngine; + DecentralizedStableCoin public dsc; + MockV3Aggregator public ethUsdPriceFeed; + MockV3Aggregator public btcUsdPriceFeed; + ERC20Mock public weth; + ERC20Mock public wbtc; + + // Ghost Variables + uint96 public constant MAX_DEPOSIT_SIZE = type(uint96).max; + + constructor(DSCEngine _dscEngine, DecentralizedStableCoin _dsc) { + dscEngine = _dscEngine; + dsc = _dsc; + + address[] memory collateralTokens = dscEngine.getCollateralTokens(); + weth = ERC20Mock(collateralTokens[0]); + wbtc = ERC20Mock(collateralTokens[1]); + + ethUsdPriceFeed = MockV3Aggregator(dscEngine.getCollateralTokenPriceFeed(address(weth))); + btcUsdPriceFeed = MockV3Aggregator(dscEngine.getCollateralTokenPriceFeed(address(wbtc))); + } + + + // FUNCTOINS TO INTERACT WITH + + /////////////// + // DSCEngine // + /////////////// + + function mintAndDepositCollateral(uint256 collateralSeed, uint256 amountCollateral) public { + // must be more than 0 + amountCollateral = bound(amountCollateral, 1, MAX_DEPOSIT_SIZE); + ERC20Mock collateral = _getCollateralFromSeed(collateralSeed); + + vm.startPrank(msg.sender); + collateral.mint(msg.sender, amountCollateral); + collateral.approve(address(dscEngine), amountCollateral); + dscEngine.depositCollateral(address(collateral), amountCollateral); + vm.stopPrank(); + } + + function redeemCollateral(uint256 collateralSeed, uint256 amountCollateral) public { + ERC20Mock collateral = _getCollateralFromSeed(collateralSeed); + uint256 maxCollateral = dscEngine.getCollateralBalanceOfUser(msg.sender, address(collateral)); + + amountCollateral = bound(amountCollateral, 0, maxCollateral); + + if(amountCollateral == 0){ + return; + } + vm.prank(msg.sender); + dscEngine.redeemCollateral(address(collateral), amountCollateral); + + + } + + function burnDsc(uint256 amountDsc) public { + amountDsc = bound(amountDsc, 0, dsc.balanceOf(msg.sender)); + if(amountDsc == 0) return; + + vm.startPrank(msg.sender); + dsc.approve(address(dscEngine), amountDsc); + dscEngine.burnDsc(amountDsc); + vm.stopPrank(); + } + + function liquidate(uint256 collateralSeed, address userToBeLiquidated, uint256 debtToCover) public { + uint256 minHealthFactor = dscEngine.getMinHealthFactor(); + uint256 userHealthfactor = dscEngine.getHealthFactor(userToBeLiquidated); + if(userHealthfactor >= minHealthFactor){ + return; + } + ERC20Mock collateral = _getCollateralFromSeed(collateralSeed); + debtToCover = bound(debtToCover, 1, uint256(type(uint96).max)); + dscEngine.liquidate(address(collateral), userToBeLiquidated, debtToCover); + } + + ///////////////////////////// + // DecentralizedStableCoin // + ///////////////////////////// + function transferDsc(uint256 amountDsc, address to) public { + if(to == address(0)){ + to = address(1); + } + amountDsc = bound(amountDsc, 0, dsc.balanceOf(msg.sender)); + vm.prank(msg.sender); + dsc.transfer(to, amountDsc); + } + + ///////////////////////////// + // Aggregator // + ///////////////////////////// + + function updateCollateralPrice(uint96 newPrice, uint256 collateralSeed) public { + int256 intNewPrice = int256(uint256(newPrice)); + ERC20Mock collateral = _getCollateralFromSeed(collateralSeed); + MockV3Aggregator priceFeed = MockV3Aggregator(dscEngine.getCollateralTokenPriceFeed(address(collateral))); + + priceFeed.updateAnswer(intNewPrice); + } + + /// Helper Functions + function _getCollateralFromSeed(uint256 collateralSeed) private view returns (ERC20Mock) { + if (collateralSeed % 2 == 0) { + return weth; + } else { + return wbtc; + } + } +} \ No newline at end of file diff --git a/test/fuzz/failOnRevert/FailOnRevertInvariants.t.sol b/test/fuzz/failOnRevert/FailOnRevertInvariants.t.sol new file mode 100644 index 0000000..820535f --- /dev/null +++ b/test/fuzz/failOnRevert/FailOnRevertInvariants.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +// Invariants: +// protocol must never be insolvent / undercollateralized +// TODO: users cant create stablecoins with a bad health factor +// TODO: a user should only be able to be liquidated if they have a bad health factor + +import { Test } 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 "@openzeppelin/contracts/mocks/ERC20Mock.sol"; Updated mock location +import { ERC20Mock } from "../../mocks/ERC20Mock.sol"; +import { FailOnRevertHandler } from "./FailOnRevertHandler.t.sol"; +import { console } from "forge-std/console.sol"; + +contract FailOnRevertInvariants is StdInvariant, Test { + DSCEngine public dsce; + DecentralizedStableCoin public dsc; + HelperConfig public helperConfig; + + address public ethUsdPriceFeed; + address public btcUsdPriceFeed; + address public weth; + address public wbtc; + + uint256 amountCollateral = 10 ether; + uint256 amountToMint = 100 ether; + + uint256 public constant STARTING_USER_BALANCE = 10 ether; + address public constant USER = address(1); + uint256 public constant MIN_HEALTH_FACTOR = 1e18; + uint256 public constant LIQUIDATION_THRESHOLD = 50; + + // Liquidation + address public liquidator = makeAddr("liquidator"); + uint256 public collateralToCover = 20 ether; + + FailOnRevertHandler public handler; + + function setUp() external { + DeployDSC deployer = new DeployDSC(); + (dsc, dsce, helperConfig) = deployer.run(); + (ethUsdPriceFeed, btcUsdPriceFeed, weth, wbtc,) = helperConfig.activeNetworkConfig(); + handler = new FailOnRevertHandler(dsce, dsc); + targetContract(address(handler)); + // targetContract(address(ethUsdPriceFeed)); Why can't we just do this? + } + + function invariant_protocolMustHaveMoreValueThatTotalSupplyDollars() public view { + uint256 totalSupply = dsc.totalSupply(); + uint256 wethDeposted = ERC20Mock(weth).balanceOf(address(dsce)); + uint256 wbtcDeposited = ERC20Mock(wbtc).balanceOf(address(dsce)); + + uint256 wethValue = dsce.getUsdValue(weth, wethDeposted); + uint256 wbtcValue = dsce.getUsdValue(wbtc, wbtcDeposited); + + console.log("wethValue: %s", wethValue); + console.log("wbtcValue: %s", wbtcValue); + + assert(wethValue + wbtcValue >= totalSupply); + + } + + function invariant_gettersCantRevert() public view { + dsce.getAdditionalFeedPrecision(); + dsce.getCollateralTokens(); + dsce.getLiquidationBonus(); + dsce.getLiquidationThreshold(); + dsce.getMinHealthFactor(); + dsce.getPrecision(); + dsce.getDsc(); + // dsce.getTokenAmountFromUsd(); + // dsce.getCollateralTokenPriceFeed(); + // dsce.getCollateralBalanceOfUser(); + // getAccountCollateralValue(); + } +} + + diff --git a/test/mockTest/MockFailedMintDSCTest.t.sol b/test/mockTest/MockFailedMintDSCTest.t.sol new file mode 100644 index 0000000..675d49f --- /dev/null +++ b/test/mockTest/MockFailedMintDSCTest.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import { MockFailedMintDSC } from "../mocks/MockFailedMintDSC.sol"; +import { Test, console } from "forge-std/Test.sol"; +import { StdCheats } from "forge-std/StdCheats.sol"; + +contract MockFailedMintDSCTest is Test { + MockFailedMintDSC public mockDsc; + uint256 public BURN_AMOUNT = 1 ether; + + function setUp() public { + mockDsc = new MockFailedMintDSC(); + } + + function testRevertWhenAmountIsLessThanZero() public { + vm.prank(mockDsc.owner()); + vm.expectRevert(MockFailedMintDSC.DecentralizedStableCoin__AmountMustBeMoreThanZero.selector); + mockDsc.mint(address(this), 0); + } + + function testRevertWhenAmountIsMoreThanBalance() public { + vm.startPrank(mockDsc.owner()); + vm.expectRevert(MockFailedMintDSC.DecentralizedStableCoin__BurnAmountExceedsBalance.selector); + mockDsc.burn(1 ether); + } + + function testMintToAddressIsNotZero() public { + vm.startPrank(mockDsc.owner()); + vm.expectRevert(MockFailedMintDSC.DecentralizedStableCoin__NotZeroAddress.selector); + mockDsc.mint(address(0), 1 ether); + } + + function testMintAmountCannotbeZero() public { + vm.startPrank(mockDsc.owner()); + vm.expectRevert(MockFailedMintDSC.DecentralizedStableCoin__AmountMustBeMoreThanZero.selector); + mockDsc.mint(address(this), 0); + } + + function testMintFunctionAlwaysReturnFalse() public { + vm.startPrank(mockDsc.owner()); + assert(false == mockDsc.mint(address(this), 1 ether)); + } +} diff --git a/test/mocks/MockFailedMintDSC.sol b/test/mocks/MockFailedMintDSC.sol new file mode 100644 index 0000000..27e93ec --- /dev/null +++ b/test/mocks/MockFailedMintDSC.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import { ERC20Burnable, ERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +contract MockFailedMintDSC is ERC20Burnable, Ownable { + error DecentralizedStableCoin__AmountMustBeMoreThanZero(); + error DecentralizedStableCoin__BurnAmountExceedsBalance(); + error DecentralizedStableCoin__NotZeroAddress(); + + constructor() ERC20("DecentralizedStableCoin", "DSC") { } + + function burn(uint256 _amount) public override onlyOwner { + uint256 balance = balanceOf(msg.sender); + if (_amount <= 0) { + revert DecentralizedStableCoin__AmountMustBeMoreThanZero(); + } + 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__AmountMustBeMoreThanZero(); + } + _mint(_to, _amount); + return false; + } +} \ No newline at end of file diff --git a/test/mocks/MockFailedTransfer.sol b/test/mocks/MockFailedTransfer.sol new file mode 100644 index 0000000..167187f --- /dev/null +++ b/test/mocks/MockFailedTransfer.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import { ERC20Burnable, ERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +contract MockFailedTransfer is ERC20Burnable, Ownable { + error DecentralizedStableCoin__AmountMustBeMoreThanZero(); + error DecentralizedStableCoin__BurnAmountExceedsBalance(); + error DecentralizedStableCoin__NotZeroAddress(); + + /* + In future versions of OpenZeppelin contracts package, Ownable must be declared with an address of the contract owner + as a parameter. + For example: + constructor() ERC20("DecentralizedStableCoin", "DSC") Ownable(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) {} + Related code changes can be viewed in this commit: + https://github.com/OpenZeppelin/openzeppelin-contracts/commit/13d5e0466a9855e9305119ed383e54fc913fdc60 + */ + constructor() ERC20("DecentralizedStableCoin", "DSC") { } + + function burn(uint256 _amount) public override onlyOwner { + uint256 balance = balanceOf(msg.sender); + if (_amount <= 0) { + revert DecentralizedStableCoin__AmountMustBeMoreThanZero(); + } + if (balance < _amount) { + revert DecentralizedStableCoin__BurnAmountExceedsBalance(); + } + super.burn(_amount); + } + + function mint(address account, uint256 amount) public { + _mint(account, amount); + } + + function transfer(address, /*recipient*/ uint256 /*amount*/ ) public pure override returns (bool) { + return false; + } +} \ No newline at end of file diff --git a/test/mocks/MockFailedTransferFrom.sol b/test/mocks/MockFailedTransferFrom.sol new file mode 100644 index 0000000..6e2d43b --- /dev/null +++ b/test/mocks/MockFailedTransferFrom.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ERC20Burnable, ERC20} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract MockFailedTransferFrom is ERC20Burnable, Ownable { + error DecentralizedStableCoin__AmountMustBeMoreThanZero(); + error DecentralizedStableCoin__BurnAmountExceedsBalance(); + error DecentralizedStableCoin__NotZeroAddress(); + + constructor() ERC20("DecentralizedStableCoin", "DSC") {} + + function burn(uint256 _amount) public override onlyOwner { + uint256 balance = balanceOf(msg.sender); + if (_amount <= 0) { + revert DecentralizedStableCoin__AmountMustBeMoreThanZero(); + } + if (balance < _amount) { + revert DecentralizedStableCoin__BurnAmountExceedsBalance(); + } + super.burn(_amount); + } + + function mint(address account, uint256 amount) public { + _mint(account, amount); + } + + function transferFrom(address, address, uint256) public pure override returns (bool) { + return false; + } +} diff --git a/test/mocks/MockMoreDebtDSC.sol b/test/mocks/MockMoreDebtDSC.sol new file mode 100644 index 0000000..a0f5588 --- /dev/null +++ b/test/mocks/MockMoreDebtDSC.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT + +// This is considered an Exogenous, Decentralized, Anchored (pegged), Crypto Collateralized low volitility coin + +// 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 +// 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"; +import { MockV3Aggregator } from "./MockV3Aggregator.sol"; + +/* + * @title DecentralizedStableCoin + * @author Patrick Collins + * 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 MockMoreDebtDSC is ERC20Burnable, Ownable { + error DecentralizedStableCoin__AmountMustBeMoreThanZero(); + error DecentralizedStableCoin__BurnAmountExceedsBalance(); + error DecentralizedStableCoin__NotZeroAddress(); + + address mockAggregator; + + /* + In future versions of OpenZeppelin contracts package, Ownable must be declared with an address of the contract owner + as a parameter. + For example: + constructor() ERC20("DecentralizedStableCoin", "DSC") Ownable(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) {} + Related code changes can be viewed in this commit: + https://github.com/OpenZeppelin/openzeppelin-contracts/commit/13d5e0466a9855e9305119ed383e54fc913fdc60 + */ + constructor(address _mockAggregator) ERC20("DecentralizedStableCoin", "DSC") { + mockAggregator = _mockAggregator; + } + + function burn(uint256 _amount) public override onlyOwner { + // We crash the price + MockV3Aggregator(mockAggregator).updateAnswer(0); + uint256 balance = balanceOf(msg.sender); + if (_amount <= 0) { + revert DecentralizedStableCoin__AmountMustBeMoreThanZero(); + } + 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__AmountMustBeMoreThanZero(); + } + _mint(_to, _amount); + return true; + } +} \ No newline at end of file diff --git a/test/unit/DSCEngineTest.t.sol b/test/unit/DSCEngineTest.t.sol index b36d9e7..a14f2d5 100644 --- a/test/unit/DSCEngineTest.t.sol +++ b/test/unit/DSCEngineTest.t.sol @@ -8,9 +8,15 @@ 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"; +import {MockFailedTransferFrom} from "../mocks/MockFailedTransferFrom.sol"; +import { MockFailedMintDSC } from "../mocks/MockFailedMintDSC.sol"; +import { MockFailedTransfer } from "../mocks/MockFailedTransfer.sol"; +import { MockMoreDebtDSC } from "../mocks/MockMoreDebtDSC.sol"; contract DSCEngineTest is Test { - event CollateralRedeemed(address indexed redeemFrom, address indexed redeemTo, address token, uint256 amount); // if + event CollateralRedeemed( + address indexed redeemedFrom, address indexed redeemedTo, address collateralTokenAddress, uint256 indexed amount + ); // if // redeemFrom != redeemedTo, then it was liquidated DeployDSC deployer; @@ -78,6 +84,31 @@ contract DSCEngineTest is Test { // DepositCollateral Tests //////////////////////////// + // This test needs it's own setup + function testRevertsIfTransferFromFails() public { + // Arrange - setup + address owner = msg.sender; + vm.prank(owner); + MockFailedTransferFrom mockDsc = new MockFailedTransferFrom(); + tokenAddresses = [address(mockDsc)]; + feedAddresses = [ethUsdPriceFeed]; + vm.prank(owner); + DSCEngine mockDsce = new DSCEngine(tokenAddresses, feedAddresses, address(mockDsc)); + mockDsc.mint(USER, AMOUNT_COLLATERAL); + + vm.prank(owner); + mockDsc.transferOwnership(address(mockDsce)); + + // Arrange - User + vm.startPrank(USER); + ERC20Mock(address(mockDsc)).approve(address(mockDsce), AMOUNT_COLLATERAL); + + //Act/Assert + vm.expectRevert(DSCEngine.DSCEngine__TransferFailed.selector); + mockDsce.depositCollateral(address(mockDsc), AMOUNT_COLLATERAL); + vm.stopPrank(); + } + function testRevertsIfCollateralZero() public { vm.startPrank(USER); ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); @@ -150,6 +181,27 @@ contract DSCEngineTest is Test { // mintDsc Tests // /////////////////////////////////// + // This test needs it's own custom setup + function testRevertsIfMintFails() public { + // Arrange - setup + MockFailedMintDSC mockDsc = new MockFailedMintDSC(); + tokenAddresses = [weth]; + feedAddresses = [ethUsdPriceFeed]; + address owner = msg.sender; + vm.prank(owner); + DSCEngine mockDsce = new DSCEngine(tokenAddresses,feedAddresses,address(mockDsc)); + mockDsc.transferOwnership(address(mockDsce)); + + // Arrange - User + vm.startPrank(USER); + ERC20Mock(weth).approve(address(mockDsce), AMOUNT_COLLATERAL); + + vm.expectRevert(DSCEngine.DSCEngine__MintFailed.selector); + mockDsce.depositCollateralAndMintDsc(weth, AMOUNT_COLLATERAL, AMOUNT_TO_MINT); + vm.stopPrank(); + + } + function testRevertsIfMintAmountIsZero() public { vm.startPrank(USER); ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); @@ -204,6 +256,31 @@ contract DSCEngineTest is Test { // redeemCollateral Tests // ////////////////////////////////// + function testRevertsIfTransferFails() public { + // Arrange - setup + address owner = msg.sender; + vm.prank(owner); + MockFailedTransfer mockDsc = new MockFailedTransfer(); + tokenAddresses = [address(mockDsc)]; + feedAddresses = [ethUsdPriceFeed]; + vm.prank(owner); + DSCEngine mockDsce = new DSCEngine(tokenAddresses, feedAddresses, address(mockDsc)); + mockDsc.mint(USER, AMOUNT_COLLATERAL); + + vm.prank(owner); + mockDsc.transferOwnership(address(mockDsce)); + + //Arrange - USER + vm.startPrank(USER); + ERC20Mock(address(mockDsc)).approve(address(mockDsce), AMOUNT_COLLATERAL); + + // Act / Assert + mockDsce.depositCollateral(address(mockDsc), AMOUNT_COLLATERAL); + vm.expectRevert(DSCEngine.DSCEngine__TransferFailed.selector); + mockDsce.redeemCollateral(address(mockDsc), AMOUNT_COLLATERAL); + vm.stopPrank(); + } + function testRevertsIfRedeemAmountIsZero() public { vm.startPrank(USER); ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL); @@ -220,13 +297,13 @@ contract DSCEngineTest is Test { 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(); - // } + 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 // @@ -281,6 +358,41 @@ contract DSCEngineTest is Test { // Liquidation Tests // /////////////////////// + function testMustImproveHealthFactorOnLiquidation() public { + // Arrange - setup + MockMoreDebtDSC mockDsc = new MockMoreDebtDSC(ethUsdPriceFeed); + tokenAddresses = [weth]; + feedAddresses = [ethUsdPriceFeed]; + address owner = msg.sender; + vm.prank(owner); + DSCEngine mockDsce = new DSCEngine(tokenAddresses, feedAddresses, address(mockDsc)); + mockDsc.transferOwnership(address(mockDsce)); + + //Arrange - USER + vm.startPrank(USER); + ERC20Mock(weth).approve(address(mockDsce), AMOUNT_COLLATERAL); + mockDsce.depositCollateralAndMintDsc(weth, AMOUNT_COLLATERAL, AMOUNT_TO_MINT); + vm.stopPrank(); + + //Arrange - liquidator + collateralToCover = 1 ether; + ERC20Mock(weth).mint(liquidator, collateralToCover); + + vm.startPrank(liquidator); + ERC20Mock(weth).approve(address(mockDsce), collateralToCover); + uint256 debtToCover = 10 ether; + mockDsce.depositCollateralAndMintDsc(weth, collateralToCover, AMOUNT_TO_MINT); + mockDsc.approve(address(mockDsce), debtToCover); + + //Act + int256 ethUsdUpdatedPrice = 18e8; + MockV3Aggregator(ethUsdPriceFeed).updateAnswer(ethUsdUpdatedPrice); + //Act/Assert + vm.expectRevert(DSCEngine.DSCEngine__HealthFactorNotImproved.selector); + mockDsce.liquidate(weth, USER, debtToCover); + vm.stopPrank(); + } + function testCantLiquidateGoodHealthFactor() public depositedCollateralAndMintedDsc { ERC20Mock(weth).mint(liquidator, collateralToCover); diff --git a/test/unit/DecntralizedStableCoinTest.t.sol b/test/unit/DecntralizedStableCoinTest.t.sol new file mode 100644 index 0000000..ecdb14c --- /dev/null +++ b/test/unit/DecntralizedStableCoinTest.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import { DecentralizedStableCoin } from "../../src/DecentralizedStableCoin.sol"; +import { Test, console } from "forge-std/Test.sol"; +import { StdCheats } from "forge-std/StdCheats.sol"; + +contract DecentralizedStableCoinTest is Test { + DecentralizedStableCoin dsc; + + function setUp() public { + dsc = new DecentralizedStableCoin(); + } + + function testMustMintMoreThanZero() public { + vm.prank(dsc.owner()); + vm.expectRevert(); + dsc.mint(address(this), 0); + } + + function testMustBurnMoreThanZero() public { + vm.prank(dsc.owner()); + dsc.mint(address(this), 100); + vm.expectRevert(); + dsc.burn(0); + vm.stopPrank(); + } + + function testCantBurnMoreThanYouHave() public { + vm.startPrank(dsc.owner()); + dsc.mint(address(this), 100); + vm.expectRevert(); + dsc.burn(101); + vm.stopPrank(); + } + + function testCantMintToZeroAddress() public { + vm.startPrank(dsc.owner()); + vm.expectRevert(); + dsc.mint(address(0), 100); + vm.stopPrank(); + } +} \ No newline at end of file