稳定币项目构建 (一)

项目构建

我们这里采用超额抵押的算法机制,来coding我们的算法稳定币dsc。此项目只限于学习,其本身的算法机制并不完善。

DecentralizedStableCoin合约

代币合约。这里我们的dsc代币对标ustd,1美元锚定为标准。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import {ERC20Burnable, ERC20} from "lib/openzepplin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import { Ownable } from "lib/openzepplin-contracts/contracts/access/Ownable.sol";

// 在 OpenZeppelin 合约包的未来版本中,必须使用合约所有者的地址声明 Ownable
// 作为参数。
// 例如:
// constructor() ERC20(“去中心化稳定币”, “DSC”) ownable(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) {}
contract DecentralizedStableCoin is ERC20Burnable, Ownable {

    error DecentralizedStableCoin__AmountMustBeGreaterThanZero();
    error DecentralizedStableCoin__BurnAmountExceedsBalance();
    error DecentralizedStableCoin__CannotMintToZeroAddress();

    event Burned(address indexed from, uint256 amount);
    event Minted(address indexed to, uint256 amount);

    constructor() ERC20("DecentralizedStableCoin", "DSC") {}

    function burn(uint256 _amount) public override onlyOwner {
        uint256 balance = balanceOf(msg.sender);

        if(_amount < 0 ){
            revert DecentralizedStableCoin__AmountMustBeGreaterThanZero();
        }

        if(balance < _amount){
            revert DecentralizedStableCoin__BurnAmountExceedsBalance();
        }

        super.burn(_amount);
        emit Burned(msg.sender, _amount);
    }

    function mint(address _to, uint256 _amount) public onlyOwner {
        if(_amount <= 0){
            revert DecentralizedStableCoin__AmountMustBeGreaterThanZero();
        }

        if(_to == address(0)){
            revert DecentralizedStableCoin__CannotMintToZeroAddress();
        }

        _mint(_to, _amount);
        emit Minted(_to, _amount);
    }

}

openzeppelin中的Ownable合约

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
        _;
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

这里设置ownable用到了继承的context合约的方法

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

_msgSender() 方法在我们继承 Ownable 合约的时候,自动进行了调用,在 OpenZeppelin 合约包的未来版本中,必须使用合约所有者的地址声明 Ownable 作为参数。 例如:

constructor() ERC20(“去中心化稳定币”, “DSC”) ownable(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) {}

关于openzeppelin中的ERC20Burnable合约

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/extensions/ERC20Burnable.sol)

pragma solidity ^0.8.0;

import "../ERC20.sol";
import "../../../utils/Context.sol";

/**
 * @dev Extension of {ERC20} that allows token holders to destroy both their own
 * tokens and those that they have an allowance for, in a way that can be
 * recognized off-chain (via event analysis).
 */
abstract contract ERC20Burnable is Context, ERC20 {
    /**
     * @dev Destroys `amount` tokens from the caller.
     *
     * See {ERC20-_burn}.
     */
    function burn(uint256 amount) public virtual {
        _burn(_msgSender(), amount);
    }

    /**
     * @dev Destroys `amount` tokens from `account`, deducting from the caller's
     * allowance.
     *
     * See {ERC20-_burn} and {ERC20-allowance}.
     *
     * Requirements:
     *
     * - the caller must have allowance for ``accounts``'s tokens of at least
     * `amount`.
     */
    function burnFrom(address account, uint256 amount) public virtual {
        _spendAllowance(account, _msgSender(), amount);
        _burn(account, amount);
    }
}

DSCEngine合约

这个合约是整个项目的核心。我们的项目是做一个提供质押铸造的稳定币,用户可以通过质押eth来获得 dsc 这个代币,其他用户可以清算达到清算阈值的资产。

先完善质押兑换这一个核心功能。

构造函数初始化参数

由于各个质押产品的价格不同,支持的token也就不一样,所以一开始我们应该要有一个白名单记录我们支持的质押代币,同时记录对应的价格源,因此也需要将两个参数绑定

  // 获取抵押品的实时价格
    mapping(address collateralToken => address priceFeed) public priceFeeds;
  // 构造函数,初始化抵押品和价格源
    constructor(address[] memory tokenAddresses, address[] memory priceFeedAddresses, address dscAddress) {
        if(tokenAddresses.length != priceFeedAddresses.length){
            revert DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame();
        }

        for(uint256 i = 0; i < tokenAddresses.length; i++){
            if(tokenAddresses[i] == address(0)){
                revert DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame();
            }
            priceFeeds[tokenAddresses[i]] = priceFeedAddresses[i];
            _collateralTokens.push(tokenAddresses[i]);
        }
        i_dsc = DecentralizedStableCoin(dscAddress);

从这里,获取了价格源,需要预言机去对应的价格源去获取价格因此需要一个datafeed合约来完成这件事情。

获取实时价格

获取质押代币的实时价格

AggregatorV3Interface接口

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// solhint-disable-next-line interface-starts-with-i
interface AggregatorV3Interface {
  function decimals() external view returns (uint8);

  function description() external view returns (string memory);

  function version() external view returns (uint256);

  function getRoundData(
    uint80 _roundId
  ) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);

  function latestRoundData()
    external
    view
    returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}
  1. roundId (uint80)
  • 含义:这是当前价格更新的“轮次ID”(Round ID)。
  • 解释:Chainlink 预言机是通过多轮次的方式来聚合数据的,每一轮都会有一个唯一的 roundId。这个 roundId 用于标识这是第几轮价格更新或报告。
  • 用途:通过 roundId,你可以知道当前的价格数据是哪一轮生成的。
  1. answer (int256)
  • 含义:这是预言机返回的实际答案,即你请求的数据结果。
  • 解释:对于价格预言机来说,answer 通常是某种资产的价格,例如 ETH/USD 或 BTC/USD 的价格。
  • 类型为 int256 是因为价格可能为负数(尽管在实际使用中很少见)。例如,它可以用于某些负值的经济数据。
  1. startedAt (uint256)
  • 含义:这是当前这一轮价格更新的启动时间。
  • 解释startedAt 代表这一轮价格数据采集的开始时间,通常是 UNIX 时间戳(即从1970年1月1日以来的秒数)。
  • 用途:通过这个时间戳,你可以知道这一轮价格数据什么时候开始聚合的。
  1. updatedAt (uint256)
  • 含义:这是当前价格更新的时间戳。
  • 解释updatedAt 代表预言机在这一轮价格更新的确切时间,也是 UNIX 时间戳格式。
  • 用途:可以用于追踪价格数据的最新更新时间,判断数据是否及时。
  1. answeredInRound (uint80)
  • 含义:这是价格数据成功报告的轮次ID。
  • 解释:这表示在哪一轮数据收集的最终答案是有效的。如果 answeredInRound 小于 roundId,则表明当前轮次的结果还没有最终确定或回答可能是来自于前几轮。
  • 用途:用来判断当前轮次的 answer 是在哪一轮被有效报告的,这可以帮助你验证数据的准确性。

根据接口,我们实例化一个接口对象来调用这些函数,获取我们需要的值

AggregatorV3Interface chainlinkPriceFeed

合约代码

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.18;

import {AggregatorV3Interface} from "lib/chainlink-brownie-contracts/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

library OracleLib {
    // 检查价格是否过时
    error OracleLib__StalePrice();

    uint256 private constant TIMEOUT = 3 hours;

    // 检查价格是否过时
    function staleCheckLatestRoundData(AggregatorV3Interface chainlinkPriceFeed)
     public view returns (
        uint80, 
        int256, 
        uint256, 
        uint256, 
        uint80) {

        (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = chainlinkPriceFeed.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 /*chainlinkPriceFeed*/) public pure returns (uint256) {
        return TIMEOUT;
    }
}

其中有两个要注意的地方

  • 预言机返回的值是根据 ustd 这一个稳定币返回的值,所以其精度是 1e8 而不是 1e18

    1e18 对应的单位是 1 wei

  • if(updatedAt == 0 ||  answeredInRound < roundId)

    updatedAt == 0 检查价格更新时间是否为0,如果是0,表示这个价格数据从未被更新过,这种情况通常意味着预言机可能出现了问题

    answeredInRound < roundId 实际回答价格的轮次ID,如果 answeredInRound 小于 roundId,表示使用了旧轮次的数据来回答当前轮次,这种情况可能意味着价格数据已经过时或者预言机网络出现了问题

获取价格之后,我们就可以根据用户质押的资产,以 USTD 为最小单位来预估用户资产,以此来铸造出对应价值的 dsc 代币

所以我们需要记录用户质押了多少资产以及其资产的预估价值。同时计算给定USD金额需要多少代币

USD-->token_amount?

 /**
     * @notice 计算给定USD金额需要多少代币
     * @dev 使用Chainlink预言机获取代币价格,然后进行计算
     * 例如:要借100 USD,ETH价格是2000 USD,则需要0.05 ETH
     * @param tokenCollateralAddress 代币地址
     * @param usdAmountIn USD金额(18位精度)
     * @return 需要的代币数量(以代币精度为单位)
     */
    function getTokenAmountFromUsd(address tokenCollateralAddress, uint256 usdAmountIn) public view returns (uint256) {
        AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeeds[tokenCollateralAddress]);
        (, int256 price,,,) = priceFeed.latestRoundData();
        return (usdAmountIn * _PRECISION) / (uint256(price) * _ADDITIONAL_FEED_PRECISION);
    }

质押资产

质押资产有两步操作

  • 记录用户质押资产的数量

  • 将用户的代币转入到当前合约中

    // 记录用户抵押品数量
    mapping(address user => mapping(address collateralToken => uint256 amount)) private _collateralDeposited;
    // 质押用户资产
    function despositCollateral(address tokenCollateralAddress
    , uint256 amountCollateral) public  {
          _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();
          }

质押完成,用户可以根据自身情况来铸造对应数量的dsc代币那么这里又涉及到了一个问题,我们知道代币的价格是有波动性的,作为不是稳定币的资产,今天的估值跟几个月后的又有所不同。用户可以铸造多少dsc代币?那么我们就需要一个功能来确定一件事情,如果ETH今天值 2000u,未来跌到1000u,那么他之前所铸造的dsc代币又该怎么处理?对于已经拥有了dsc代币这种情况,我们不可能说让他又还一部分回来,那么就只有一个选择,对他质押的资产进行清算。对于这两个问题,这里需要一个健康值,来确定他可以铸造多少dsc代币以及到多少价值的时候会被清算

给出一个标准,当 健康因子 >= 1 时,用户资产不会被清算,健康因子 < 1 时将由其他用户来清算质押的资产

* 健康因子 = 抵押品总价值 / 铸造的DSC数量  

那么我们首先就要知道抵押品的总价值是多少

获取抵押品总价值

我们需要确定两个个信息

  • 之前铸造了多少的dsc代币
  • 质押了多少数量的原生代币以及相对应的总价值

首先最先要确定的是原生代币(ETH) 价格,相对于 ustd 值多少 wei ,这里涉及到了精度转换

 // 添加抵押品精度
 uint256 private constant _ADDITIONAL_FEED_PRECISION = 1e10;

function _getUsdValue(address token, uint256 amount) private view returns(uint256) {
        AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeeds[token]);
        (, int256 price,,,) = priceFeed.latestRoundData();
        // price 返回的是1e8的精度
        return ((uint256(price) * _ADDITIONAL_FEED_PRECISION * amount) / _PRECISION);
    }

接着通过mapping,遍历用户拥有的token以及数量,并进行价值转换

/**
     * @notice 获取用户所有抵押品的总价值(以USD计)
     * @dev 遍历用户的所有抵押品,计算它们的总价值
     * @param user 要查询的用户地址
     * @return totalCollateralValue 用户所有抵押品的总价值(以USD计,18位精度)
     */
    function getAccountCollateralValue(address user) public view returns (uint256 totalCollateralValue) {
        for (uint256 index = 0; index &lt; _collateralTokens.length; index++) {
            address token = _collateralTokens[index];
            uint256 amount = _collateralDeposited[user][token];
            totalCollateralValue += _getUsdValue(token, amount);
        }
        return totalCollateralValue;
    }

计算健康因子

确定清算阈值,这里以 50% 为参数

    // 清算阈值
    uint256 private constant _LIQUIDATION_THRESHOLD = 50;
    // 清算精度
    uint256 private constant _LIQUIDATION_PRECISION = 100;

    /**
     * @notice 获取账户健康因子
     * @dev 返回用户账户的健康状况
     * 健康因子 = 抵押品总价值 / 铸造的DSC数量  
     * @return 健康因子,用uint256表示
     */
    function _calculateHealthFactor(
        uint256 totalDscMinted, 
        uint256 totalCollateralValue) internal pure returns (uint256) {
        if(totalDscMinted == 0){
            return type(uint256).max;// 如果DSC铸造为0,则健康因子为最大值。同时保证下面不会除以 0
        }
        uint256 collateralAdjustedForThreshold = (totalCollateralValue * _LIQUIDATION_THRESHOLD) / _LIQUIDATION_PRECISION;
        return (collateralAdjustedForThreshold * _PRECISION) / totalDscMinted;
    }

    function calculateHealthFactor(
        uint256 totalDscMinted, 
        uint256 totalCollateralValue) public pure returns (uint256) {
        return _calculateHealthFactor(totalDscMinted, totalCollateralValue);
    }

绑定用户地址

现在有个问题,我们只是可以计算健康因子,但并没有跟用户进行绑定,这里用mapping进行绑定是不现实的,因为这个值并没有独立性。我们就另建函数,传入 address user 参数。

    function _getAccountInformation(address user) private view returns (uint256 totalDscMinted, uint256 totalCollateralValue){
        totalDscMinted = _dscMinted[user];
        totalCollateralValue = getAccountCollateralValue(user);

    }

 /**
     * @notice 获取用户账户信息
     * @dev 返回用户铸造的DSC数量和所有抵押品总价值
     * @param user 用户地址
     * @return totalDscMinted 用户铸造的DSC数量
     * @return totalCollateralValue 用户所有抵押品总价值
     */
    function getAccountInformation(address user) public view returns (uint256 totalDscMinted, uint256 totalCollateralValue){
        (totalDscMinted, totalCollateralValue) = _getAccountInformation(user);
    }

判断健康状况

将健康因子跟用户进行绑定后,用户可以查看他们资产的健康状况。当健康因子正常,用户可以mint dsc代币,处于危险状况时,则不能进行mint 操作。我们需要一个函数来判断当前的健康状况

function _revertIfHealthFactorIsBroken(address user) internal view {
        uint256 healthFactor = _healthFactor(user);
        if(healthFactor &lt; _MIN_HEALTH_FACTOR){
            revert DSCEngine__HealthFactorIsBroken();
        }
    }

完善mint函数

这样,合约就可以放心的把mint交给用户了

 /**
     * @notice 铸造DSC代币
     * @dev 用户可以基于已存入的抵押品铸造DSC
     * 需要确保铸造后维持健康的抵押率
     */
    function mintDsc(uint256 amountDscToMint) public moreThanZero(amountDscToMint) {
        _dscMinted[msg.sender] += amountDscToMint;
        _revertIfHealthFactorIsBroken(msg.sender);
        bool minted = i_dsc.mint(msg.sender, amountDscToMint);
        if(!minted){
            revert DSCEngine__MintFailed();
        }
    }

赎回质押品

赎回功能是必须的。这里有两种情况

  • 直接赎回抵押品,用户必须保证有足够的dsc来保证健康因子处于正常的状态,否则就进行 revert 。
  • 赎回抵押品的同时,销毁dsc代币

直接赎回抵押品

很简单,先进行transfer操作,之后检查健康因子

function redeemCollateral(address tokenCollateralAddress, uint256 amountCollateral) external {
        _redeemCollateral(tokenCollateralAddress, amountCollateral, msg.sender, msg.sender);
        _revertIfHealthFactorIsBroken(msg.sender);
    }

    /**
     * @notice 赎回抵押品
     * @dev 允许用户取回他们的抵押品
     * 需要确保赎回后维持足够的抵押率
     */
    function _redeemCollateral(
        address tokenCollateralAddress,
        uint256 amountCollateral,
        address from,
        address to
    ) private  {
       _collateralDeposited[from][tokenCollateralAddress] -= amountCollateral;
       emit CollateralRedeemed(from, tokenCollateralAddress, amountCollateral);
       bool success = IERC20(tokenCollateralAddress).transfer(to, amountCollateral);
       if(!success){
        revert DSCEngine__TransferFailed();
       }
    }

ERC20中的transfer函数是以 msg.sender 参数进行操作的

 /**
     * @dev See {IERC20-transfer}.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - the caller must have a balance of at least `amount`.
     */
    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }

销毁dsc--赎回抵押品

burnDsc函数

这里有一点要注意,进行任何资金操作,由合约代理执行,都是需要授权的,除非使用本人操作,或者将资产转移到合约中,由合约进行操作。

所以这里并不能直接使用 i_dsc.burn(amountDscToBurn),burn函数也跟transfer一样,操作这是msg.sender。需要先将dsc代币转移给当前合约

 /**
     * @notice 销毁DSC代币
     * @dev 用户可以销毁自己持有的DSC
     * 通常用于减少债务或准备赎回抵押品
     */
    function _burnDsc(uint256 amountDscToBurn,address onBehalfOf, address dscFrom) private {
        _dscMinted[onBehalfOf] -= amountDscToBurn;
        bool success = i_dsc.transferFrom(dscFrom, address(this), amountDscToBurn);
        if(!success){
            revert DSCEngine__BurnFailed();
        }
        i_dsc.burn(amountDscToBurn);
    }

这里为什么需要传入两个地址呢?因为后面其他用户对该用户进行清算,想要以dsc代币获取该用户原生代币的时候,又要使用到burn函数,至于赎回的操作,我们传入两个msg.sender就行了。

  function redeemCollateralForDsc(address tokenCollateralAddress, uint256 amountDscToBurn,uint256 amountCollateralToRedeem) external {
        _burnDsc(amountDscToBurn, msg.sender, msg.sender);
        _redeemCollateral(tokenCollateralAddress, amountCollateralToRedeem, msg.sender, msg.sender);
        _revertIfHealthFactorIsBroken(msg.sender);
    }

清算机制

这里为了鼓励其他用户去清算,使用了奖励机制,这里的 bonus 设置为 10%

// 清算奖励
uint256 private constant _LIQUIDATION_BONUS = 10;

function liquidate(address collateral, address user, uint256 debtToCover) external {
        uint256 startingUserHealthFactor = _healthFactor(user);
        if(startingUserHealthFactor > _MIN_HEALTH_FACTOR){
            revert DSCEngine__HealthFactorIsNotBroken();
        }
        uint256 tokenAmountFromDebtCovered = getTokenAmountFromUsd(collateral, debtToCover);

        uint256 bonusCollateral = (tokenAmountFromDebtCovered * _LIQUIDATION_BONUS) / _LIQUIDATION_PRECISION;
        // 赎回抵押品
        _redeemCollateral(collateral, tokenAmountFromDebtCovered + bonusCollateral, user, msg.sender);
        _burnDsc(debtToCover, user, msg.sender);
        // 检查清算后的健康因子
        uint256 endingUserHealthFactor = _healthFactor(user);
        if(endingUserHealthFactor &lt;= _MIN_HEALTH_FACTOR){
            revert DSCEngine__HealthFactorIsBroken();
        }
        _revertIfHealthFactorIsBroken(msg.sender);

    }

其实关于清算机制,真正用于实践的话,是不行的。涉及到市场代币波动,以及清算活跃度的问题。就拿市场波动来说,如果一个代币的价格波动过大,比如比特币今天10w u,明天雪崩到 5w u了,在这种巨大的波动下,如果没有人即使的去清算资产,会产生资不抵债的问题。形象地假设抵押率是 100%

初始状态:
- 用户抵押了 1000 美元的 ETH
- 借出了 1000 DSC

当 ETH 价格瞬间下跌 20% 时:
- ETH 抵押品现在只值 800 美元
- 但仍有 1000 DSC 的债务

安全机制完善

要保证三个方面

  • 保证代币的地址是被初始化过的
  • 保证输入的数据是不等于0的,防止产生一些垃圾的日志信息,造成gas浪费
  • 保证不会被重入攻击
     // 检查数量是否大于0
    modifier moreThanZero(uint256 value){
        if(value &lt;= 0){
            revert DSCEngine__MoreThanZero();
        }
        _;

      // 检查抵押品是否被允许
    modifier isAllowedToken(address tokenAddress){
        if(priceFeeds[tokenAddress] == address(0)){
            revert DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame();
        }
        _;
    }

重入锁直接使用openzepplin的代码库

contract DSCEngine is ReentrancyGuard 

接着就是完善各个函数了,添加到各个函数的后面。

提供数据查询函数

便于获取数据,方便后续数据的对接

 function getCollateralBalanceOfUser(address user, address tokenCollateralAddress) public view returns (uint256) {
        return _collateralDeposited[user][tokenCollateralAddress];
    }

    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 _collateralTokens;
    }

    function getDsc() external view returns (address) {
        return address(i_dsc);
    }

    function getCollateralTokenPriceFeed(address token) external view returns (address) {
            return priceFeeds[token];
    }

    function getHealthFactor(address user) external view returns (uint256) {
        return _healthFactor(user);
    }
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
浪迹陨灭
浪迹陨灭
0x0c37...a92b
专注于solidity智能合约的开发