Created
April 4, 2024 11:47
-
-
Save yvesbou/c61a1cecf39c56fd660599c48d973905 to your computer and use it in GitHub Desktop.
Simple CDP Stablecoin Workshop
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
// This is considered an Exogenous, Decentralized, Anchored (pegged), Crypto Collateralized low volitility coin | |
pragma solidity 0.8.24; | |
import {ERC20Burnable, ERC20} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; | |
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; | |
contract DecentralizedStableCoin 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") Ownable(msg.sender) {} | |
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 true; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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"; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity 0.8.24; | |
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; | |
/* | |
* @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(); | |
if (updatedAt == 0 || answeredInRound < roundId) { | |
revert OracleLib__StalePrice(); | |
} | |
uint256 secondsSince = block.timestamp - updatedAt; | |
if (secondsSince > TIMEOUT) revert OracleLib__StalePrice(); | |
return (roundId, answer, startedAt, updatedAt, answeredInRound); | |
} | |
function getTimeout(AggregatorV3Interface /* chainlinkFeed */ ) public pure returns (uint256) { | |
return TIMEOUT; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity 0.8.24; | |
import {OracleLib, AggregatorV3Interface} from "./OracleLib.sol"; | |
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; | |
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | |
import {DecentralizedStableCoin} from "./DecentralizedStableCoin.sol"; | |
contract DSCEngine is ReentrancyGuard { | |
/////////////////// | |
// Errors | |
/////////////////// | |
error DSCEngine__TokenAddressesAndPriceFeedAddressesAmountsDontMatch(); | |
error DSCEngine__NeedsMoreThanZero(); | |
error DSCEngine__TokenNotAllowed(address token); | |
error DSCEngine__TransferFailed(); | |
error DSCEngine__BreaksHealthFactor(uint256 healthFactorValue); | |
error DSCEngine__MintFailed(); | |
error DSCEngine__HealthFactorOk(); | |
error DSCEngine__HealthFactorNotImproved(); | |
/////////////////// | |
// Types | |
/////////////////// | |
using OracleLib for AggregatorV3Interface; | |
/////////////////// | |
// State Variables | |
/////////////////// | |
DecentralizedStableCoin private immutable i_dsc; | |
uint256 private constant LIQUIDATION_THRESHOLD = 50; // This means you need to be 200% over-collateralized | |
uint256 private constant LIQUIDATION_BONUS = 10; // This means you get assets at a 10% discount when liquidating | |
uint256 private constant LIQUIDATION_PRECISION = 100; | |
uint256 private constant MIN_HEALTH_FACTOR = 1e18; | |
uint256 private constant PRECISION = 1e18; | |
uint256 private constant ADDITIONAL_FEED_PRECISION = 1e10; | |
uint256 private constant FEED_PRECISION = 1e8; | |
/// @dev Mapping of token address to price feed address | |
mapping(address collateralToken => address priceFeed) private s_priceFeeds; | |
/// @dev Amount of collateral deposited by user | |
mapping(address user => mapping(address collateralToken => uint256 amount)) private s_collateralDeposited; | |
/// @dev Amount of DSC minted by user | |
mapping(address user => uint256 amount) private s_DSCMinted; | |
/// @dev If we know exactly how many tokens we have, we could make this immutable! | |
address[] private s_collateralTokens; | |
/////////////////// | |
// Events | |
/////////////////// | |
event CollateralDeposited(address indexed user, address indexed token, uint256 indexed amount); | |
event CollateralRedeemed(address indexed redeemFrom, address indexed redeemTo, address token, uint256 amount); // if redeemFrom != redeemedTo, then it was liquidated | |
/////////////////// | |
// Modifiers | |
/////////////////// | |
modifier moreThanZero(uint256 amount) { | |
if (amount == 0) { | |
revert DSCEngine__NeedsMoreThanZero(); | |
} | |
_; | |
} | |
modifier isAllowedToken(address token) { | |
if (s_priceFeeds[token] == address(0)) { | |
revert DSCEngine__TokenNotAllowed(token); | |
} | |
_; | |
} | |
/////////////////// | |
// Functions | |
/////////////////// | |
constructor(address[] memory tokenAddresses, address[] memory priceFeedAddresses, address dscAddress) { | |
} | |
/////////////////// | |
// 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 { | |
// use depositCollateral | |
// use mintDsc | |
} | |
/* | |
* @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) | |
{ | |
// burn Dsc | |
// redeem Collateral | |
// revert if health factor is broken | |
} | |
/* | |
* @param tokenCollateralAddress: The ERC20 token address of the collateral you're redeeming | |
* @param amountCollateral: The amount of collateral you're redeeming | |
* @notice This function will redeem your collateral. | |
* @notice If you have DSC minted, you will not be able to redeem until you burn your DSC | |
*/ | |
function redeemCollateral(address tokenCollateralAddress, uint256 amountCollateral) | |
external | |
moreThanZero(amountCollateral) | |
nonReentrant | |
isAllowedToken(tokenCollateralAddress) | |
{ | |
// redeem collateral (look for a private or internal function) | |
// revert if health factor is broken | |
} | |
/* | |
* @notice careful! You'll burn your DSC here! Make sure you want to do this... | |
* @dev you might want to use this if you're nervous you might get liquidated and want to just burn | |
* you DSC but keep your collateral in. | |
*/ | |
function burnDsc(uint256 amount) external moreThanZero(amount) { | |
// burn Dsc (look for a private or internal function) | |
// revert if health factor is broken | |
} | |
/* | |
* @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 200% 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 | |
{ | |
// check health factor (only allow unhealthy positions to be liquidated) | |
// get the amount of collateral tokens that are equal in value to the debt to cover (DSC) | |
// uint256 tokenAmountFromDebtCovered = ... | |
// giving a 10% bonus of the collateral tokens | |
// uint256 bonusCollateral = ... | |
// redeem collateral to the liquidator (tokenAmountFromDebtCovered+bonusCollateral) | |
// burn DSC (to remove bad debt) | |
// revert if health factor of liquidator is broken | |
} | |
/////////////////// | |
// Public Functions | |
/////////////////// | |
/* | |
* @param amountDscToMint: The amount of DSC you want to mint | |
* You can only mint DSC if you hav enough collateral | |
*/ | |
function mintDsc(uint256 amountDscToMint) public moreThanZero(amountDscToMint) nonReentrant { | |
// increase our balance (how much Dsc is minted to each user of the protocol | |
// check the health factor (function should revert if unhealthy) | |
// mint dsc | |
// check if mint was successful (uncomment the thing below) | |
/* | |
if (minted != true) { | |
revert DSCEngine__MintFailed(); | |
} | |
*/ | |
} | |
/* | |
* @param tokenCollateralAddress: The ERC20 token address of the collateral you're depositing | |
* @param amountCollateral: The amount of collateral you're depositing | |
*/ | |
function depositCollateral(address tokenCollateralAddress, uint256 amountCollateral) | |
public | |
moreThanZero(amountCollateral) | |
nonReentrant | |
isAllowedToken(tokenCollateralAddress) | |
{ | |
// increase collateral balance | |
// emit event that collateral position increased | |
// transfer collateral to this contract | |
// if this transfer is not successful revert and throw error | |
/* | |
if (!success) { | |
revert DSCEngine__TransferFailed(); | |
} | |
*/ | |
} | |
/////////////////// | |
// Private Functions | |
/////////////////// | |
function _redeemCollateral(address tokenCollateralAddress, uint256 amountCollateral, address from, address to) private { | |
// remove collateral balance | |
// emit event that collateral is redeemed | |
// transfer collateral to redeemer ("to") | |
/* | |
if (!success) { | |
revert DSCEngine__TransferFailed(); | |
} | |
*/ | |
} | |
function _burnDsc(uint256 amountDscToBurn, address onBehalfOf, address dscFrom) private { | |
// reduce stored balance of onBehalfOf | |
// transfer from dscFrom | |
/* | |
if (!success) { | |
revert DSCEngine__TransferFailed(); | |
} | |
*/ | |
// burn | |
} | |
////////////////////////////// | |
// Private & Internal View & Pure Functions | |
////////////////////////////// | |
function _getAccountInformation(address user) | |
private | |
view | |
returns (uint256 totalDscMinted, uint256 collateralValueInUsd) | |
{ | |
totalDscMinted = s_DSCMinted[user]; | |
collateralValueInUsd = getAccountCollateralValue(user); | |
} | |
function _healthFactor(address user) private view returns (uint256) { | |
(uint256 totalDscMinted, uint256 collateralValueInUsd) = _getAccountInformation(user); | |
return _calculateHealthFactor(totalDscMinted, collateralValueInUsd); | |
} | |
function _getUsdValue(address token, uint256 amount) private view returns (uint256) { | |
AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]); | |
(, int256 price,,,) = priceFeed.staleCheckLatestRoundData(); | |
// 1 ETH = 1000 USD | |
// The returned value from Chainlink will be 1000 * 1e8 | |
// Most USD pairs have 8 decimals, so we will just pretend they all do | |
// We want to have everything in terms of WEI, so we add 10 zeros at the end | |
return ((uint256(price) * ADDITIONAL_FEED_PRECISION) * amount) / PRECISION; | |
} | |
function _calculateHealthFactor(uint256 totalDscMinted, uint256 collateralValueInUsd) | |
internal | |
pure | |
returns (uint256) | |
{ | |
if (totalDscMinted == 0) return type(uint256).max; // => "infinite number aka max health" | |
uint256 collateralAdjustedForThreshold = (collateralValueInUsd * LIQUIDATION_THRESHOLD) / LIQUIDATION_PRECISION; | |
return (collateralAdjustedForThreshold * PRECISION) / totalDscMinted; | |
} | |
function revertIfHealthFactorIsBroken(address user) internal view { | |
uint256 userHealthFactor = _healthFactor(user); | |
if (userHealthFactor < MIN_HEALTH_FACTOR) { | |
revert DSCEngine__BreaksHealthFactor(userHealthFactor); | |
} | |
} | |
//////////////////////////////////////////////////////////////////////////// | |
//////////////////////////////////////////////////////////////////////////// | |
// External & Public View & Pure Functions | |
//////////////////////////////////////////////////////////////////////////// | |
//////////////////////////////////////////////////////////////////////////// | |
function calculateHealthFactor(uint256 totalDscMinted, uint256 collateralValueInUsd) | |
external | |
pure | |
returns (uint256) | |
{ | |
return _calculateHealthFactor(totalDscMinted, collateralValueInUsd); | |
} | |
function getAccountInformation(address user) | |
external | |
view | |
returns (uint256 totalDscMinted, uint256 collateralValueInUsd) | |
{ | |
return _getAccountInformation(user); | |
} | |
function getUsdValue( | |
address token, | |
uint256 amount // in WEI | |
) external view returns (uint256) { | |
return _getUsdValue(token, amount); | |
} | |
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 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 | |
// Most USD pairs have 8 decimals, so we will just pretend they all do | |
return ((usdAmountInWei * PRECISION) / (uint256(price) * ADDITIONAL_FEED_PRECISION)); | |
} | |
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity ^0.8.24; | |
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | |
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; | |
contract WETH is ERC20, Ownable { | |
uint256 private _totalSupply; | |
string private _name; | |
string private _symbol; | |
constructor() ERC20("Wrapped ETH", "WETH") Ownable(msg.sender){ | |
_totalSupply = 1000000000*1e18; | |
} | |
function mint(address _to, uint256 _amount) external onlyOwner returns (bool) { | |
require(_amount > 0, "amount has to be bigger than 0"); | |
_mint(_to, _amount); | |
return true; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment