前言
我们介绍下基于EVM的稳定币实现逻辑,通过下面两个合约我们可以实现一种和美元1:1兑换的稳定币DSC币
我们先创建tocken命名为DSC,用户不可以随意的创建或者销毁代币,只有拥有者可以;在部署过程中我们将代币合约的拥有权交给DSCEngine,用户只能通过DSCEngine实现对DSC代币的铸造,销毁,转移,清算,质押,取回等功能
DecentralizedStableCoin.sol
只有 DSCEngine(owner)能铸造和销毁 DSC,普通用户不能直接操作。
铸造:mint 给指定用户。
销毁:burn 掉 owner 自己的余额。
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 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 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") { }
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;
}
}
DSCEngine.sol
代币(DSC)只是一个稳定币账本,不能直接管理抵押逻辑。
DSCEngine 管理整个系统:
- 接收抵押物(WETH / WBTC)
- 根据抵押物价值铸造 DSC
- 允许赎回抵押物(必须先烧掉 DSC)
- 在抵押不足时清算用户
目标:保证 DSC 始终锚定 1 美元,并且系统 超额抵押。
核心机制
健康因子公式:
healthFactor = (collateralValueInUsd * LIQUIDATION_THRESHOLD / LIQUIDATION_PRECISION) * PRECISION / totalDscMinted;
其中 collateralValueInUsd是抵押品的美元价值,LIQUIDATION_THRESHOLD=50,LIQUIDATION_PRECISION=100,PRECISION是单位配平,totalDscMinted是DSC货币量
- 当 healthFactor < 1 时,用户处于危险状态,可以被清算。
- healthFactor 越大,越安全。
Mint & Burn
mintDsc: 用户在存入抵押品后,可以调用此函数铸造 DSC。
会更新用户的 s_DSCMinted。
立刻检查健康因子 _revertIfHealthFactorIsBroken。
调用 DecentralizedStableCoin.mint(),把 DSC 发给用户。
burnDsc: 用户销毁自己持有的 DSC,减少债务(常用于避免清算)。
Collateral Deposit & Redeem
depositCollateral
- 用户存入抵押品(如 WETH)。
- 更新 s_collateralDeposited[user][token]。
- IERC20.transferFrom() 把 token 转入合约。
redeemCollateral
- 用户取出抵押品。
- 要检查 healthFactor,保证取出后系统仍然安全。
depositCollateralAndMintDsc
存入抵押品 + 一次性 mint DSC。
Liquidation(清算逻辑)
当某个用户抵押不足(healthFactor < 1),别人可以调用 liquidate():
选择目标用户(被清算者 user)。
偿还其部分 DSC 债务(即 burn 他们的 DSC)。
获取对应抵押品 + 奖励(LIQUIDATION_BONUS = 10%)。
例子:
用户抵押 2 ETH(价值 $2000),mint 1500 DSC(抵押率 < 200% → 健康因子不足)。
Liquidator(清算人)用 100 DSC 偿还用户债务,系统会给他价值 110 USD 的 ETH(含 10% bonus)。
系统流程
用户 depositCollateral(WETH, 10) → 存入 10 ETH 抵押品。
用户调用 mintDsc(1000) → 得到 1000 DSC。
系统检查:10 ETH = $20,000
抵押率 = 2000% > 200%,安全。
如果 ETH 价格暴跌,导致用户健康因子 < 1:
任何人都能调用 liquidate(),清算用户。
// 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 { OracleLib, AggregatorV3Interface } from "./libraries/OracleLib.sol";
// The correct path for ReentrancyGuard in latest Openzeppelin contracts is
//"import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { DecentralizedStableCoin } from "./DecentralizedStableCoin.sol";
/*
* @title DSCEngine
* @author Patrick Collins
*
* 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__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) {
if (tokenAddresses.length != priceFeedAddresses.length) {
revert DSCEngine__TokenAddressesAndPriceFeedAddressesAmountsDontMatch();
}
// These feeds will be the USD pairs
// For example ETH / USD or MKR / USD
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);
}
/*
* @param tokenCollateralAddress: The ERC20 token address of the collateral you're withdrawing
* @param amountCollateral: The amount of collateral you're withdrawing
* @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, msg.sender, msg.sender);
_redeemCollateral(tokenCollateralAddress, amountCollateral, msg.sender, msg.sender);
_revertIfHealthFactorIsBroken(msg.sender);
}
/*
* @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)
{
_redeemCollateral(tokenCollateralAddress, amountCollateral, msg.sender, msg.sender);
_revertIfHealthFactorIsBroken(msg.sender);
}
/*
* @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
* your DSC but keep your collateral in.
*/
function burnDsc(uint256 amount) external moreThanZero(amount) {
_burnDsc(amount, msg.sender, msg.sender);
_revertIfHealthFactorIsBroken(msg.sender); // I don't think this would ever hit...
}
/*
* @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
isAllowedToken(collateral)
moreThanZero(debtToCover)
nonReentrant
{
uint256 startingUserHealthFactor = _healthFactor(user);
if (startingUserHealthFactor >= MIN_HEALTH_FACTOR) {
revert DSCEngine__HealthFactorOk();
}
// If covering 100 DSC, we need to $100 of collateral
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;
// Burn DSC equal to debtToCover
// Figure out how much collateral to recover based on how much burnt
_redeemCollateral(collateral, tokenAmountFromDebtCovered + bonusCollateral, user, msg.sender);
_burnDsc(debtToCover, user, msg.sender);
uint256 endingUserHealthFactor = _healthFactor(user);
// This conditional should never hit, but just in case
if (endingUserHealthFactor <= startingUserHealthFactor) {
revert DSCEngine__HealthFactorNotImproved();
}
_revertIfHealthFactorIsBroken(msg.sender);
}
///////////////////
// Public Functions
///////////////////
/*
* @param amountDscToMint: The amount of DSC you want to mint
* You can only mint DSC if you have enough collateral
*/
function mintDsc(uint256 amountDscToMint) public moreThanZero(amountDscToMint) nonReentrant {
s_DSCMinted[msg.sender] += amountDscToMint;
_revertIfHealthFactorIsBroken(msg.sender);
bool minted = i_dsc.mint(msg.sender, amountDscToMint);
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)
{
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();
}
}
///////////////////
// Private Functions
///////////////////
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 _burnDsc(uint256 amountDscToBurn, address onBehalfOf, address dscFrom) private {
s_DSCMinted[onBehalfOf] -= amountDscToBurn;
bool success = i_dsc.transferFrom(dscFrom, address(this), amountDscToBurn);
// This conditional is hypothetically unreachable
if (!success) {
revert DSCEngine__TransferFailed();
}
i_dsc.burn(amountDscToBurn);
}
//////////////////////////////
// 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;
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);
}
}