Skip to content

Commit f4485ee

Browse files
committed
feat: implement decentralized stablecoin engine
1 parent 5cd2830 commit f4485ee

7 files changed

Lines changed: 309 additions & 8 deletions

File tree

‎script/DeployDSC.s.sol‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.18;
4+
5+
import {Script} from "forge-std/Script.sol";
6+
import {DecentralizedStableCoin} from "../src/DecentralizedStableCoin.sol";
7+
import {DSCEngine} from "../src/DSCEngine.sol";
8+
import {HelperConfig} from "./HelperConfig.s.sol";
9+
10+
contract DeployDSC is Script {
11+
address[] public collateralTokens;
12+
address[] public priceFeeds;
13+
14+
function run() public returns (DecentralizedStableCoin, DSCEngine, HelperConfig) {
15+
HelperConfig config = new HelperConfig();
16+
17+
(address wethUsdPriceFeed, address wbtcUsdPriceFeed, address weth, address wbtc, uint256 deployerKey) =
18+
config.activeNetworkConfig();
19+
20+
collateralTokens = [weth, wbtc];
21+
priceFeeds = [wethUsdPriceFeed, wbtcUsdPriceFeed];
22+
vm.startBroadcast(deployerKey);
23+
24+
DecentralizedStableCoin dsc = new DecentralizedStableCoin();
25+
DSCEngine engine = new DSCEngine(collateralTokens, priceFeeds, address(dsc));
26+
27+
dsc.transferOwnership(address(engine));
28+
vm.stopBroadcast();
29+
return (dsc, engine, config);
30+
}
31+
}

‎script/HelperConfig.s.sol‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.18;
4+
5+
import {Script} from "forge-std/Script.sol";
6+
import {MockV3Aggregator} from "../test/mocks/MockV3Aggregator.sol";
7+
import {ERC20Mock} from "../test/mocks/ERC20Mock.sol";
8+
9+
contract HelperConfig is Script {
10+
struct NetworkConfig {
11+
address wethUsdPriceFeed;
12+
address wbtcUsdPriceFeed;
13+
address weth;
14+
address wbtc;
15+
uint256 deployerKey;
16+
}
17+
18+
uint8 public constant DEFAULT_DECIMAL = 8;
19+
int256 public constant ETH_PRICE = 2000e8;
20+
int256 public constant BTC_PRICE = 1000e8;
21+
uint256 public constant DEFAULT_ANVIL_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;
22+
23+
NetworkConfig public activeNetworkConfig;
24+
25+
constructor() {
26+
if (block.chainid == 11155111) {
27+
activeNetworkConfig = getSepoliaEthConfig();
28+
} else if (block.chainid == 1) {
29+
activeNetworkConfig = getMainnetEthConfig();
30+
} else {
31+
activeNetworkConfig = getOrCreateAnvilEthConfig();
32+
}
33+
}
34+
35+
function getSepoliaEthConfig() public view returns (NetworkConfig memory) {
36+
return NetworkConfig({
37+
wethUsdPriceFeed: 0x694AA1769357215DE4FAC081bf1f309aDC325306,
38+
wbtcUsdPriceFeed: 0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43,
39+
weth: 0xdd13E55209Fd76AfE204dBda4007C227904f0a81,
40+
wbtc: 0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063,
41+
deployerKey: vm.envUint("PRIVATE_KEY")
42+
});
43+
}
44+
45+
function getMainnetEthConfig() public view returns (NetworkConfig memory) {
46+
return NetworkConfig({
47+
wethUsdPriceFeed: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419,
48+
wbtcUsdPriceFeed: 0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c,
49+
weth: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2,
50+
wbtc: 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599,
51+
deployerKey: vm.envUint("PRIVATE_KEY")
52+
});
53+
}
54+
55+
function getOrCreateAnvilEthConfig() public returns (NetworkConfig memory) {
56+
if (activeNetworkConfig.wethUsdPriceFeed != address(0)) {
57+
return activeNetworkConfig;
58+
}
59+
vm.startBroadcast();
60+
MockV3Aggregator feedEthUsd = new MockV3Aggregator(DEFAULT_DECIMAL, ETH_PRICE);
61+
MockV3Aggregator feedBtcUsd = new MockV3Aggregator(DEFAULT_DECIMAL, BTC_PRICE);
62+
ERC20Mock weth = new ERC20Mock("WETH", "WETH", msg.sender, 1000e18);
63+
ERC20Mock wbtc = new ERC20Mock("WBTC", "WBTC", msg.sender, 1000e18);
64+
vm.stopBroadcast();
65+
return NetworkConfig({
66+
wethUsdPriceFeed: address(feedEthUsd),
67+
wbtcUsdPriceFeed: address(feedBtcUsd),
68+
weth: address(weth),
69+
wbtc: address(wbtc),
70+
deployerKey: DEFAULT_ANVIL_KEY
71+
});
72+
}
73+
}

‎src/DSCEngine.sol‎

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ contract DSCEngine is ReentrancyGuard {
7979
// Events //
8080
//////////////////////
8181
event CollateralDeposited(address indexed user, address indexed token, uint256 indexed amount);
82-
82+
event CollateralRedeemed(address indexed user, address indexed token, uint256 indexed amount);
8383
///////////////////////
8484
// Modifiers //
8585
//////////////////////
@@ -114,14 +114,23 @@ contract DSCEngine is ReentrancyGuard {
114114
///////////////////////
115115
// External Functions//
116116
///////////////////////
117-
function depositCollateralAndMintDSC() external {}
117+
/**
118+
* @notice - This function is used to deposit collateral and mint DSC in a single transaction.
119+
* @param collateralTokenAddress - The address of the collateral token to deposit
120+
* @param collateralAmount - The amount of collateral to deposit
121+
* @param dscAmountToMint - The amount of DSC to mint
122+
*/
123+
function depositCollateralAndMintDSC(address collateralTokenAddress, uint256 collateralAmount, uint256 dscAmountToMint) external {
124+
depositCollateral(collateralTokenAddress, collateralAmount);
125+
mintDSC(dscAmountToMint);
126+
}
118127

119128
/**
120129
* @param collateralTokenAddress - The address of the collateral token to deposit
121130
* @param collateralAmount - The amount of collateral to deposit
122131
*/
123132
function depositCollateral(address collateralTokenAddress, uint256 collateralAmount)
124-
external
133+
public
125134
moreThanZero(collateralAmount)
126135
isAllowedToken(collateralTokenAddress)
127136
nonReentrant
@@ -134,15 +143,38 @@ contract DSCEngine is ReentrancyGuard {
134143
}
135144
}
136145

137-
function redeemCollateralForDSC() external {}
146+
/**
147+
* @param collateralTokenToRedeem - The address of the collateral token to redeem
148+
* @param amountToRedeem - The amount of collateral to redeem
149+
* @param dscAmountToBurn - The amount of DSC to burn
150+
* @notice This function burns DSC and then redeems collateral in a single transaction.
151+
* Since the function redeemCollateral already if health factor is broken, we don't need to check it again.
152+
*/
153+
function redeemCollateralForDSC(address collateralTokenToRedeem, uint256 amountToRedeem, uint256 dscAmountToBurn) external {
154+
burnDsc(dscAmountToBurn);
155+
redeemCollateral(collateralTokenToRedeem, amountToRedeem);
156+
}
138157

139-
function redeemCollateral() external {}
158+
/**
159+
* @param collateralTokenToRedeem - The address of the collateral token to redeem
160+
* @param amountToRedeem - The amount of collateral to redeem
161+
* @notice The function will revert if the health factor is not above 1 after the redemption of collateral.
162+
*/
163+
function redeemCollateral(address collateralTokenToRedeem, uint256 amountToRedeem) public nonReentrant moreThanZero(amountToRedeem) {
164+
s_collateralDeposited[msg.sender][collateralTokenToRedeem] -= amountToRedeem;
165+
emit CollateralRedeemed(msg.sender, collateralTokenToRedeem, amountToRedeem);
166+
bool success = IERC20(collateralTokenToRedeem).transfer(msg.sender, amountToRedeem);
167+
if (!success) {
168+
revert DSCEngine__TransferFailed();
169+
}
170+
_revertIfHealthIsBroken(msg.sender);
171+
}
140172

141173
/**
142174
* @param amount - The amount of DSC to mint
143175
* @notice The amount of DSC to mint is based on the amount of collateral deposited and the price feed of the collateral and depositedCollateralValue > desiredDscAmount
144176
*/
145-
function mintDSC(uint256 amount) external moreThanZero(amount) nonReentrant{
177+
function mintDSC(uint256 amount) public moreThanZero(amount) nonReentrant {
146178
s_DscMinted[msg.sender] += amount;
147179
revertIfHealthIsBroken(msg.sender);
148180
bool minted = i_dsc.mint(msg.sender, amount);
@@ -151,7 +183,14 @@ contract DSCEngine is ReentrancyGuard {
151183
}
152184
}
153185

154-
function burnDsc() external {}
186+
function burnDsc(uint256 amountToBurn) public moreThanZero(amountToBurn){
187+
s_DscMinted[msg.sender] -= amountToBurn;
188+
bool success = i_dsc.transferFrom(msg.sender, address(this), amountToBurn);
189+
if (!success) {
190+
revert DSCEngine__TransferFailed();
191+
}
192+
i_dsc.burn(amountToBurn);
193+
}
155194

156195
function liquidate() external {}
157196

‎src/DecentralizedStableCoin.sol‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
// SPDX-License-Identifier: MIT
2424

25-
pragma solidity ^0.8.18;
25+
pragma solidity ^0.8.19;
2626

2727
import {ERC20Burnable, ERC20} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
2828
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

‎test/mocks/ERC20Mock.sol‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.19;
3+
4+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
6+
contract ERC20Mock is ERC20 {
7+
constructor(string memory name, string memory symbol, address initialAccount, uint256 initialBalance)
8+
payable
9+
ERC20(name, symbol)
10+
{
11+
_mint(initialAccount, initialBalance);
12+
}
13+
14+
function mint(address account, uint256 amount) public {
15+
_mint(account, amount);
16+
}
17+
18+
function burn(address account, uint256 amount) public {
19+
_burn(account, amount);
20+
}
21+
22+
function transferInternal(address from, address to, uint256 value) public {
23+
_transfer(from, to, value);
24+
}
25+
26+
function approveInternal(address owner, address spender, uint256 value) public {
27+
_approve(owner, spender, value);
28+
}
29+
}

‎test/mocks/MockV3Aggregator.sol‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
/**
5+
* @title MockV3Aggregator
6+
* @notice Based on the FluxAggregator contract
7+
* @notice Use this contract when you need to test
8+
* other contract's ability to read data from an
9+
* aggregator contract, but how the aggregator got
10+
* its answer is unimportant
11+
*/
12+
contract MockV3Aggregator {
13+
uint256 public constant version = 0;
14+
15+
uint8 public decimals;
16+
int256 public latestAnswer;
17+
uint256 public latestTimestamp;
18+
uint256 public latestRound;
19+
20+
mapping(uint256 => int256) public getAnswer;
21+
mapping(uint256 => uint256) public getTimestamp;
22+
mapping(uint256 => uint256) private getStartedAt;
23+
24+
constructor(uint8 _decimals, int256 _initialAnswer) {
25+
decimals = _decimals;
26+
updateAnswer(_initialAnswer);
27+
}
28+
29+
function updateAnswer(int256 _answer) public {
30+
latestAnswer = _answer;
31+
latestTimestamp = block.timestamp;
32+
latestRound++;
33+
getAnswer[latestRound] = _answer;
34+
getTimestamp[latestRound] = block.timestamp;
35+
getStartedAt[latestRound] = block.timestamp;
36+
}
37+
38+
function updateRoundData(uint80 _roundId, int256 _answer, uint256 _timestamp, uint256 _startedAt) public {
39+
latestRound = _roundId;
40+
latestAnswer = _answer;
41+
latestTimestamp = _timestamp;
42+
getAnswer[latestRound] = _answer;
43+
getTimestamp[latestRound] = _timestamp;
44+
getStartedAt[latestRound] = _startedAt;
45+
}
46+
47+
function getRoundData(uint80 _roundId)
48+
external
49+
view
50+
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
51+
{
52+
return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId);
53+
}
54+
55+
function latestRoundData()
56+
external
57+
view
58+
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
59+
{
60+
return (
61+
uint80(latestRound),
62+
getAnswer[latestRound],
63+
getStartedAt[latestRound],
64+
getTimestamp[latestRound],
65+
uint80(latestRound)
66+
);
67+
}
68+
69+
function description() external pure returns (string memory) {
70+
return "v0.6/tests/MockV3Aggregator.sol";
71+
}
72+
}

‎test/unit/DSCEngineTest.t.sol‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.19;
4+
5+
import {Test, console} from "forge-std/Test.sol";
6+
import {DeployDSC} from "../../script/DeployDSC.s.sol";
7+
import {DSCEngine} from "../../src/DSCEngine.sol";
8+
import {DecentralizedStableCoin} from "../../src/DecentralizedStableCoin.sol";
9+
import {HelperConfig} from "../../script/HelperConfig.s.sol";
10+
import {ERC20Mock} from "../../test/mocks/ERC20Mock.sol";
11+
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
12+
13+
contract DSCEngineTest is Test {
14+
DeployDSC deployer;
15+
DecentralizedStableCoin dsc;
16+
DSCEngine dsce;
17+
HelperConfig config;
18+
address weth;
19+
address wbtc;
20+
address wethUsdPriceFeed;
21+
address wbtcUsdPriceFeed;
22+
uint256 deployerKey;
23+
24+
address public USER = makeAddr("user");
25+
uint256 public constant AMOUNT_COLLATERAL = 10 ether;
26+
uint256 public constant INITIAL_USER_BALANCE = 10 ether;
27+
28+
29+
function setUp() public {
30+
deployer = new DeployDSC();
31+
(dsc, dsce, config) = deployer.run();
32+
(wethUsdPriceFeed, wbtcUsdPriceFeed, weth, wbtc, deployerKey ) = config.activeNetworkConfig();
33+
ERC20Mock(weth).mint(USER, INITIAL_USER_BALANCE);
34+
}
35+
36+
/////////////////////////////
37+
///Price Tests ///
38+
/////////////////////////////
39+
function testGetUsdValue() public view {
40+
uint256 ethAmount = 15e18;
41+
AggregatorV3Interface priceFeed = AggregatorV3Interface(wethUsdPriceFeed);
42+
(, int256 price,,,) = priceFeed.latestRoundData();
43+
uint256 expectedUsd = uint256(ethAmount) * uint256(price) / 1e8;
44+
uint256 actualUsd = dsce.getUsdValue(weth, ethAmount);
45+
assertEq(actualUsd, expectedUsd);
46+
}
47+
48+
//////////////////////////////
49+
///Collateral Tests ///
50+
//////////////////////////////
51+
function testRevertIfCollateralZero() public {
52+
vm.prank(USER);
53+
ERC20Mock(weth).approve(address(dsce), AMOUNT_COLLATERAL);
54+
vm.expectRevert(DSCEngine.DSCEngine__MustBeMoreThanZero.selector);
55+
dsce.depositCollateral(weth, 0);
56+
}
57+
}

0 commit comments

Comments
 (0)