{"content":{"title":"稳定币项目构建 (一）","body":"# 项目构建\r\n\r\n我们这里采用超额抵押的算法机制，来coding我们的算法稳定币dsc。此项目只限于学习，其本身的算法机制并不完善。\r\n\r\n## DecentralizedStableCoin合约\r\n\r\n代币合约。这里我们的dsc代币对标ustd，1美元锚定为标准。\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.18;\r\n\r\nimport {ERC20Burnable, ERC20} from \"lib/openzepplin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol\";\r\nimport { Ownable } from \"lib/openzepplin-contracts/contracts/access/Ownable.sol\";\r\n\r\n// 在 OpenZeppelin 合约包的未来版本中，必须使用合约所有者的地址声明 Ownable\r\n// 作为参数。\r\n// 例如：\r\n// constructor（） ERC20（“去中心化稳定币”， “DSC”） ownable（0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266） {}\r\ncontract DecentralizedStableCoin is ERC20Burnable, Ownable {\r\n\r\n    error DecentralizedStableCoin__AmountMustBeGreaterThanZero();\r\n    error DecentralizedStableCoin__BurnAmountExceedsBalance();\r\n    error DecentralizedStableCoin__CannotMintToZeroAddress();\r\n\r\n    event Burned(address indexed from, uint256 amount);\r\n    event Minted(address indexed to, uint256 amount);\r\n    \r\n    constructor() ERC20(\"DecentralizedStableCoin\", \"DSC\") {}\r\n\r\n    function burn(uint256 _amount) public override onlyOwner {\r\n        uint256 balance = balanceOf(msg.sender);\r\n        \r\n        if(_amount < 0 ){\r\n            revert DecentralizedStableCoin__AmountMustBeGreaterThanZero();\r\n        }\r\n\r\n        if(balance < _amount){\r\n            revert DecentralizedStableCoin__BurnAmountExceedsBalance();\r\n        }\r\n\r\n        super.burn(_amount);\r\n        emit Burned(msg.sender, _amount);\r\n    }\r\n\r\n    function mint(address _to, uint256 _amount) public onlyOwner {\r\n        if(_amount <= 0){\r\n            revert DecentralizedStableCoin__AmountMustBeGreaterThanZero();\r\n        }\r\n\r\n        if(_to == address(0)){\r\n            revert DecentralizedStableCoin__CannotMintToZeroAddress();\r\n        }\r\n\r\n        _mint(_to, _amount);\r\n        emit Minted(_to, _amount);\r\n    }\r\n        \r\n}\r\n```\r\n\r\n**openzeppelin中的Ownable合约**\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\n// OpenZeppelin Contracts v4.4.1 (access/Ownable.sol)\r\n\r\npragma solidity ^0.8.0;\r\n\r\nimport \"../utils/Context.sol\";\r\n\r\n/**\r\n * @dev Contract module which provides a basic access control mechanism, where\r\n * there is an account (an owner) that can be granted exclusive access to\r\n * specific functions.\r\n *\r\n * By default, the owner account will be the one that deploys the contract. This\r\n * can later be changed with {transferOwnership}.\r\n *\r\n * This module is used through inheritance. It will make available the modifier\r\n * `onlyOwner`, which can be applied to your functions to restrict their use to\r\n * the owner.\r\n */\r\nabstract contract Ownable is Context {\r\n    address private _owner;\r\n\r\n    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\r\n\r\n    /**\r\n     * @dev Initializes the contract setting the deployer as the initial owner.\r\n     */\r\n    constructor() {\r\n        _transferOwnership(_msgSender());\r\n    }\r\n\r\n    /**\r\n     * @dev Returns the address of the current owner.\r\n     */\r\n    function owner() public view virtual returns (address) {\r\n        return _owner;\r\n    }\r\n\r\n    /**\r\n     * @dev Throws if called by any account other than the owner.\r\n     */\r\n    modifier onlyOwner() {\r\n        require(owner() == _msgSender(), \"Ownable: caller is not the owner\");\r\n        _;\r\n    }\r\n\r\n    /**\r\n     * @dev Leaves the contract without owner. It will not be possible to call\r\n     * `onlyOwner` functions anymore. Can only be called by the current owner.\r\n     *\r\n     * NOTE: Renouncing ownership will leave the contract without an owner,\r\n     * thereby removing any functionality that is only available to the owner.\r\n     */\r\n    function renounceOwnership() public virtual onlyOwner {\r\n        _transferOwnership(address(0));\r\n    }\r\n\r\n    /**\r\n     * @dev Transfers ownership of the contract to a new account (`newOwner`).\r\n     * Can only be called by the current owner.\r\n     */\r\n    function transferOwnership(address newOwner) public virtual onlyOwner {\r\n        require(newOwner != address(0), \"Ownable: new owner is the zero address\");\r\n        _transferOwnership(newOwner);\r\n    }\r\n\r\n    /**\r\n     * @dev Transfers ownership of the contract to a new account (`newOwner`).\r\n     * Internal function without access restriction.\r\n     */\r\n    function _transferOwnership(address newOwner) internal virtual {\r\n        address oldOwner = _owner;\r\n        _owner = newOwner;\r\n        emit OwnershipTransferred(oldOwner, newOwner);\r\n    }\r\n}\r\n\r\n```\r\n\r\n这里设置ownable用到了继承的context合约的方法\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\n// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)\r\n\r\npragma solidity ^0.8.0;\r\n\r\n/**\r\n * @dev Provides information about the current execution context, including the\r\n * sender of the transaction and its data. While these are generally available\r\n * via msg.sender and msg.data, they should not be accessed in such a direct\r\n * manner, since when dealing with meta-transactions the account sending and\r\n * paying for execution may not be the actual sender (as far as an application\r\n * is concerned).\r\n *\r\n * This contract is only required for intermediate, library-like contracts.\r\n */\r\nabstract contract Context {\r\n    function _msgSender() internal view virtual returns (address) {\r\n        return msg.sender;\r\n    }\r\n\r\n    function _msgData() internal view virtual returns (bytes calldata) {\r\n        return msg.data;\r\n    }\r\n}\r\n\r\n```\r\n\r\n_msgSender() 方法在我们继承 Ownable 合约的时候，自动进行了调用，在 OpenZeppelin 合约包的未来版本中，必须使用合约所有者的地址声明 Ownable\r\n作为参数。\r\n例如：\r\n\r\n```\r\nconstructor（） ERC20（“去中心化稳定币”， “DSC”） ownable（0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266） {}\r\n```\r\n\r\n**关于openzeppelin中的ERC20Burnable合约**\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\n// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/extensions/ERC20Burnable.sol)\r\n\r\npragma solidity ^0.8.0;\r\n\r\nimport \"../ERC20.sol\";\r\nimport \"../../../utils/Context.sol\";\r\n\r\n/**\r\n * @dev Extension of {ERC20} that allows token holders to destroy both their own\r\n * tokens and those that they have an allowance for, in a way that can be\r\n * recognized off-chain (via event analysis).\r\n */\r\nabstract contract ERC20Burnable is Context, ERC20 {\r\n    /**\r\n     * @dev Destroys `amount` tokens from the caller.\r\n     *\r\n     * See {ERC20-_burn}.\r\n     */\r\n    function burn(uint256 amount) public virtual {\r\n        _burn(_msgSender(), amount);\r\n    }\r\n\r\n    /**\r\n     * @dev Destroys `amount` tokens from `account`, deducting from the caller's\r\n     * allowance.\r\n     *\r\n     * See {ERC20-_burn} and {ERC20-allowance}.\r\n     *\r\n     * Requirements:\r\n     *\r\n     * - the caller must have allowance for ``accounts``'s tokens of at least\r\n     * `amount`.\r\n     */\r\n    function burnFrom(address account, uint256 amount) public virtual {\r\n        _spendAllowance(account, _msgSender(), amount);\r\n        _burn(account, amount);\r\n    }\r\n}\r\n\r\n```\r\n\r\n## DSCEngine合约\r\n\r\n这个合约是整个项目的核心。我们的项目是做一个提供质押铸造的稳定币，用户可以通过质押eth来获得 dsc 这个代币，其他用户可以清算达到清算阈值的资产。\r\n\r\n先完善质押兑换这一个核心功能。\r\n\r\n### 构造函数初始化参数\r\n\r\n由于各个质押产品的价格不同，支持的token也就不一样，所以一开始我们应该要有一个白名单记录我们支持的质押代币，同时记录对应的价格源，因此也需要将两个参数绑定\r\n\r\n```solidity\r\n  // 获取抵押品的实时价格\r\n    mapping(address collateralToken => address priceFeed) public priceFeeds;\r\n  // 构造函数，初始化抵押品和价格源\r\n    constructor(address[] memory tokenAddresses, address[] memory priceFeedAddresses, address dscAddress) {\r\n        if(tokenAddresses.length != priceFeedAddresses.length){\r\n            revert DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame();\r\n        }\r\n\r\n        for(uint256 i = 0; i < tokenAddresses.length; i++){\r\n            if(tokenAddresses[i] == address(0)){\r\n                revert DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame();\r\n            }\r\n            priceFeeds[tokenAddresses[i]] = priceFeedAddresses[i];\r\n            _collateralTokens.push(tokenAddresses[i]);\r\n        }\r\n        i_dsc = DecentralizedStableCoin(dscAddress);\r\n```\r\n\r\n从这里，获取了价格源，需要预言机去对应的价格源去获取价格因此需要一个datafeed合约来完成这件事情。\r\n\r\n### 获取实时价格\r\n\r\n获取质押代币的实时价格\r\n\r\n**AggregatorV3Interface接口**\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.0;\r\n\r\n// solhint-disable-next-line interface-starts-with-i\r\ninterface AggregatorV3Interface {\r\n  function decimals() external view returns (uint8);\r\n\r\n  function description() external view returns (string memory);\r\n\r\n  function version() external view returns (uint256);\r\n\r\n  function getRoundData(\r\n    uint80 _roundId\r\n  ) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);\r\n\r\n  function latestRoundData()\r\n    external\r\n    view\r\n    returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);\r\n}\r\n\r\n```\r\n\r\n1. **`roundId` (`uint80`)**\r\n\r\n- **含义**：这是当前价格更新的“轮次ID”（Round ID）。\r\n- **解释**：Chainlink 预言机是通过多轮次的方式来聚合数据的，每一轮都会有一个唯一的 `roundId`。这个 `roundId` 用于标识这是第几轮价格更新或报告。\r\n- **用途**：通过 `roundId`，你可以知道当前的价格数据是哪一轮生成的。\r\n\r\n2. **`answer` (`int256`)**\r\n\r\n- **含义**：这是预言机返回的实际答案，即你请求的数据结果。\r\n- **解释**：对于价格预言机来说，`answer` 通常是某种资产的价格，例如 ETH/USD 或 BTC/USD 的价格。\r\n- **类型为 `int256`** 是因为价格可能为负数（尽管在实际使用中很少见）。例如，它可以用于某些负值的经济数据。\r\n\r\n3. **`startedAt` (`uint256`)**\r\n\r\n- **含义**：这是当前这一轮价格更新的启动时间。\r\n- **解释**：`startedAt` 代表这一轮价格数据采集的开始时间，通常是 UNIX 时间戳（即从1970年1月1日以来的秒数）。\r\n- **用途**：通过这个时间戳，你可以知道这一轮价格数据什么时候开始聚合的。\r\n\r\n4. **`updatedAt` (`uint256`)**\r\n\r\n- **含义**：这是当前价格更新的时间戳。\r\n- **解释**：`updatedAt` 代表预言机在这一轮价格更新的确切时间，也是 UNIX 时间戳格式。\r\n- **用途**：可以用于追踪价格数据的最新更新时间，判断数据是否及时。\r\n\r\n5. **`answeredInRound` (`uint80`)**\r\n\r\n- **含义**：这是价格数据成功报告的轮次ID。\r\n- **解释**：这表示在哪一轮数据收集的最终答案是有效的。如果 `answeredInRound` 小于 `roundId`，则表明当前轮次的结果还没有最终确定或回答可能是来自于前几轮。\r\n- **用途**：用来判断当前轮次的 `answer` 是在哪一轮被有效报告的，这可以帮助你验证数据的准确性。\r\n\r\n根据接口，我们实例化一个接口对象来调用这些函数，获取我们需要的值\r\n\r\n```solidity\r\nAggregatorV3Interface chainlinkPriceFeed\r\n```\r\n\r\n合约代码\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\n\r\npragma solidity ^0.8.18;\r\n\r\nimport {AggregatorV3Interface} from \"lib/chainlink-brownie-contracts/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol\";\r\n\r\nlibrary OracleLib {\r\n    // 检查价格是否过时\r\n    error OracleLib__StalePrice();\r\n\r\n    uint256 private constant TIMEOUT = 3 hours;\r\n\r\n    // 检查价格是否过时\r\n    function staleCheckLatestRoundData(AggregatorV3Interface chainlinkPriceFeed)\r\n     public view returns (\r\n        uint80, \r\n        int256, \r\n        uint256, \r\n        uint256, \r\n        uint80) {\r\n        \r\n        (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = chainlinkPriceFeed.latestRoundData();\r\n        if(updatedAt == 0 ||  answeredInRound < roundId){\r\n            revert OracleLib__StalePrice();\r\n        }\r\n        uint256 secondsSince = block.timestamp - updatedAt;\r\n        if(secondsSince > TIMEOUT){\r\n            revert OracleLib__StalePrice();\r\n        }\r\n        return (roundId, answer, startedAt, updatedAt, answeredInRound);\r\n    }\r\n\r\n    function getTimeout(AggregatorV3Interface /*chainlinkPriceFeed*/) public pure returns (uint256) {\r\n        return TIMEOUT;\r\n    }\r\n}\r\n```\r\n\r\n其中有两个要注意的地方\r\n\r\n- 预言机返回的值是根据 ustd 这一个稳定币返回的值，所以其精度是  1e8 而不是 1e18\r\n\r\n  1e18 对应的单位是 1 wei\r\n\r\n- ```\r\n  if(updatedAt == 0 ||  answeredInRound < roundId)\r\n  ```\r\n\r\n  updatedAt == 0  检查价格更新时间是否为0，如果是0，表示这个价格数据从未被更新过，这种情况通常意味着预言机可能出现了问题\r\n\r\n  answeredInRound < roundId   实际回答价格的轮次ID，如果 answeredInRound 小于 roundId，表示使用了旧轮次的数据来回答当前轮次，这种情况可能意味着价格数据已经过时或者预言机网络出现了问题\r\n\r\n获取价格之后，我们就可以根据用户质押的资产，以 USTD 为最小单位来预估用户资产，以此来铸造出对应价值的 dsc 代币\r\n\r\n所以我们需要记录用户质押了多少资产以及其资产的预估价值。同时计算给定USD金额需要多少代币\r\n\r\n#### USD-->token_amount?\r\n\r\n```solidity\r\n /**\r\n     * @notice 计算给定USD金额需要多少代币\r\n     * @dev 使用Chainlink预言机获取代币价格，然后进行计算\r\n     * 例如：要借100 USD，ETH价格是2000 USD，则需要0.05 ETH\r\n     * @param tokenCollateralAddress 代币地址\r\n     * @param usdAmountIn USD金额（18位精度）\r\n     * @return 需要的代币数量（以代币精度为单位）\r\n     */\r\n    function getTokenAmountFromUsd(address tokenCollateralAddress, uint256 usdAmountIn) public view returns (uint256) {\r\n        AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeeds[tokenCollateralAddress]);\r\n        (, int256 price,,,) = priceFeed.latestRoundData();\r\n        return (usdAmountIn * _PRECISION) / (uint256(price) * _ADDITIONAL_FEED_PRECISION);\r\n    }\r\n```\r\n\r\n#### 质押资产\r\n\r\n质押资产有两步操作\r\n\r\n- 记录用户质押资产的数量\r\n\r\n- 将用户的代币转入到当前合约中\r\n\r\n  ```solidity\r\n   // 记录用户抵押品数量\r\n  mapping(address user => mapping(address collateralToken => uint256 amount)) private _collateralDeposited;\r\n   // 质押用户资产\r\n  function despositCollateral(address tokenCollateralAddress\r\n  , uint256 amountCollateral) public  {\r\n          _collateralDeposited[msg.sender][tokenCollateralAddress] += amountCollateral;\r\n          emit CollateralDeposited(msg.sender, tokenCollateralAddress, amountCollateral);\r\n          bool success = IERC20(tokenCollateralAddress).transferFrom(msg.sender, address(this), amountCollateral);\r\n          if(!success){\r\n              revert DSCEngine__TransferFailed();\r\n          }\r\n  ```\r\n\r\n质押完成，用户可以根据自身情况来铸造对应数量的dsc代币那么这里又涉及到了一个问题，我们知道代币的价格是有波动性的，作为不是稳定币的资产，今天的估值跟几个月后的又有所不同。用户可以铸造多少dsc代币？那么我们就需要一个功能来确定一件事情，如果ETH今天值 2000u，未来跌到1000u，那么他之前所铸造的dsc代币又该怎么处理？对于已经拥有了dsc代币这种情况，我们不可能说让他又还一部分回来，那么就只有一个选择，对他质押的资产进行清算。对于这两个问题，这里需要一个健康值，来确定他可以铸造多少dsc代币以及到多少价值的时候会被清算\r\n\r\n给出一个标准，当  健康因子 >= 1 时，用户资产不会被清算，健康因子 < 1 时将由其他用户来清算质押的资产\r\n\r\n```\r\n* 健康因子 = 抵押品总价值 / 铸造的DSC数量  \r\n```\r\n\r\n那么我们首先就要知道抵押品的总价值是多少\r\n\r\n### 获取抵押品总价值\r\n\r\n我们需要确定两个个信息\r\n\r\n- 之前铸造了多少的dsc代币\r\n- 质押了多少数量的原生代币以及相对应的总价值\r\n\r\n首先最先要确定的是原生代币(ETH) 价格，相对于 ustd 值多少 wei ，这里涉及到了精度转换\r\n\r\n```solidity\r\n // 添加抵押品精度\r\n uint256 private constant _ADDITIONAL_FEED_PRECISION = 1e10;\r\n \r\nfunction _getUsdValue(address token, uint256 amount) private view returns(uint256) {\r\n        AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeeds[token]);\r\n        (, int256 price,,,) = priceFeed.latestRoundData();\r\n        // price 返回的是1e8的精度\r\n        return ((uint256(price) * _ADDITIONAL_FEED_PRECISION * amount) / _PRECISION);\r\n    }\r\n```\r\n\r\n接着通过mapping，遍历用户拥有的token以及数量，并进行价值转换\r\n\r\n```solidity\r\n/**\r\n     * @notice 获取用户所有抵押品的总价值（以USD计）\r\n     * @dev 遍历用户的所有抵押品，计算它们的总价值\r\n     * @param user 要查询的用户地址\r\n     * @return totalCollateralValue 用户所有抵押品的总价值（以USD计，18位精度）\r\n     */\r\n    function getAccountCollateralValue(address user) public view returns (uint256 totalCollateralValue) {\r\n        for (uint256 index = 0; index < _collateralTokens.length; index++) {\r\n            address token = _collateralTokens[index];\r\n            uint256 amount = _collateralDeposited[user][token];\r\n            totalCollateralValue += _getUsdValue(token, amount);\r\n        }\r\n        return totalCollateralValue;\r\n    }\r\n\r\n```\r\n\r\n### 计算健康因子\r\n\r\n确定清算阈值，这里以 50% 为参数\r\n\r\n```solidity\r\n\t// 清算阈值\r\n    uint256 private constant _LIQUIDATION_THRESHOLD = 50;\r\n    // 清算精度\r\n    uint256 private constant _LIQUIDATION_PRECISION = 100;\r\n    \r\n    /**\r\n     * @notice 获取账户健康因子\r\n     * @dev 返回用户账户的健康状况\r\n     * 健康因子 = 抵押品总价值 / 铸造的DSC数量  \r\n     * @return 健康因子，用uint256表示\r\n     */\r\n    function _calculateHealthFactor(\r\n        uint256 totalDscMinted, \r\n        uint256 totalCollateralValue) internal pure returns (uint256) {\r\n        if(totalDscMinted == 0){\r\n            return type(uint256).max;// 如果DSC铸造为0，则健康因子为最大值。同时保证下面不会除以 0\r\n        }\r\n        uint256 collateralAdjustedForThreshold = (totalCollateralValue * _LIQUIDATION_THRESHOLD) / _LIQUIDATION_PRECISION;\r\n        return (collateralAdjustedForThreshold * _PRECISION) / totalDscMinted;\r\n    }\r\n\r\n\r\n\tfunction calculateHealthFactor(\r\n        uint256 totalDscMinted, \r\n        uint256 totalCollateralValue) public pure returns (uint256) {\r\n        return _calculateHealthFactor(totalDscMinted, totalCollateralValue);\r\n    }\r\n```\r\n\r\n#### 绑定用户地址\r\n\r\n现在有个问题，我们只是可以计算健康因子，但并没有跟用户进行绑定，这里用mapping进行绑定是不现实的，因为这个值并没有独立性。我们就另建函数，传入 address user 参数。\r\n\r\n```solidity\r\n\tfunction _getAccountInformation(address user) private view returns (uint256 totalDscMinted, uint256 totalCollateralValue){\r\n        totalDscMinted = _dscMinted[user];\r\n        totalCollateralValue = getAccountCollateralValue(user);\r\n        \r\n    }\r\n    \r\n /**\r\n     * @notice 获取用户账户信息\r\n     * @dev 返回用户铸造的DSC数量和所有抵押品总价值\r\n     * @param user 用户地址\r\n     * @return totalDscMinted 用户铸造的DSC数量\r\n     * @return totalCollateralValue 用户所有抵押品总价值\r\n     */\r\n    function getAccountInformation(address user) public view returns (uint256 totalDscMinted, uint256 totalCollateralValue){\r\n        (totalDscMinted, totalCollateralValue) = _getAccountInformation(user);\r\n    }\r\n```\r\n\r\n#### 判断健康状况\r\n\r\n将健康因子跟用户进行绑定后，用户可以查看他们资产的健康状况。当健康因子正常，用户可以mint dsc代币，处于危险状况时，则不能进行mint 操作。我们需要一个函数来判断当前的健康状况\r\n\r\n```solidity\r\nfunction _revertIfHealthFactorIsBroken(address user) internal view {\r\n        uint256 healthFactor = _healthFactor(user);\r\n        if(healthFactor < _MIN_HEALTH_FACTOR){\r\n            revert DSCEngine__HealthFactorIsBroken();\r\n        }\r\n    }\r\n```\r\n\r\n#### 完善mint函数\r\n\r\n这样，合约就可以放心的把mint交给用户了\r\n\r\n```solidity\r\n /**\r\n     * @notice 铸造DSC代币\r\n     * @dev 用户可以基于已存入的抵押品铸造DSC\r\n     * 需要确保铸造后维持健康的抵押率\r\n     */\r\n    function mintDsc(uint256 amountDscToMint) public moreThanZero(amountDscToMint) {\r\n        _dscMinted[msg.sender] += amountDscToMint;\r\n        _revertIfHealthFactorIsBroken(msg.sender);\r\n        bool minted = i_dsc.mint(msg.sender, amountDscToMint);\r\n        if(!minted){\r\n            revert DSCEngine__MintFailed();\r\n        }\r\n    }\r\n```\r\n\r\n### 赎回质押品\r\n\r\n赎回功能是必须的。这里有两种情况\r\n\r\n- 直接赎回抵押品，用户必须保证有足够的dsc来保证健康因子处于正常的状态，否则就进行 revert 。\r\n- 赎回抵押品的同时，销毁dsc代币\r\n\r\n#### 直接赎回抵押品\r\n\r\n很简单，先进行transfer操作，之后检查健康因子\r\n\r\n```solidity\r\nfunction redeemCollateral(address tokenCollateralAddress, uint256 amountCollateral) external {\r\n        _redeemCollateral(tokenCollateralAddress, amountCollateral, msg.sender, msg.sender);\r\n        _revertIfHealthFactorIsBroken(msg.sender);\r\n    }\r\n\r\n    /**\r\n     * @notice 赎回抵押品\r\n     * @dev 允许用户取回他们的抵押品\r\n     * 需要确保赎回后维持足够的抵押率\r\n     */\r\n    function _redeemCollateral(\r\n        address tokenCollateralAddress,\r\n        uint256 amountCollateral,\r\n        address from,\r\n        address to\r\n    ) private  {\r\n       _collateralDeposited[from][tokenCollateralAddress] -= amountCollateral;\r\n       emit CollateralRedeemed(from, tokenCollateralAddress, amountCollateral);\r\n       bool success = IERC20(tokenCollateralAddress).transfer(to, amountCollateral);\r\n       if(!success){\r\n        revert DSCEngine__TransferFailed();\r\n       }\r\n    }\r\n```\r\n\r\nERC20中的transfer函数是以 msg.sender 参数进行操作的\r\n\r\n```solidity\r\n /**\r\n     * @dev See {IERC20-transfer}.\r\n     *\r\n     * Requirements:\r\n     *\r\n     * - `to` cannot be the zero address.\r\n     * - the caller must have a balance of at least `amount`.\r\n     */\r\n    function transfer(address to, uint256 amount) public virtual override returns (bool) {\r\n        address owner = _msgSender();\r\n        _transfer(owner, to, amount);\r\n        return true;\r\n    }\r\n```\r\n\r\n#### 销毁dsc--赎回抵押品\r\n\r\n**burnDsc函数**\r\n\r\n这里有一点要注意，进行任何资金操作，由合约代理执行，都是需要授权的，除非使用本人操作，或者将资产转移到合约中，由合约进行操作。\r\n\r\n所以这里并不能直接使用 i_dsc.burn(amountDscToBurn)，burn函数也跟transfer一样，操作这是msg.sender。需要先将dsc代币转移给当前合约\r\n\r\n```solidity\r\n /**\r\n     * @notice 销毁DSC代币\r\n     * @dev 用户可以销毁自己持有的DSC\r\n     * 通常用于减少债务或准备赎回抵押品\r\n     */\r\n    function _burnDsc(uint256 amountDscToBurn,address onBehalfOf, address dscFrom) private {\r\n        _dscMinted[onBehalfOf] -= amountDscToBurn;\r\n        bool success = i_dsc.transferFrom(dscFrom, address(this), amountDscToBurn);\r\n        if(!success){\r\n            revert DSCEngine__BurnFailed();\r\n        }\r\n        i_dsc.burn(amountDscToBurn);\r\n    }\r\n```\r\n\r\n这里为什么需要传入两个地址呢？因为后面其他用户对该用户进行清算，想要以dsc代币获取该用户原生代币的时候，又要使用到burn函数，至于赎回的操作，我们传入两个msg.sender就行了。\r\n\r\n```solidity\r\n  function redeemCollateralForDsc(address tokenCollateralAddress, uint256 amountDscToBurn,uint256 amountCollateralToRedeem) external {\r\n        _burnDsc(amountDscToBurn, msg.sender, msg.sender);\r\n        _redeemCollateral(tokenCollateralAddress, amountCollateralToRedeem, msg.sender, msg.sender);\r\n        _revertIfHealthFactorIsBroken(msg.sender);\r\n    }\r\n```\r\n\r\n#### 清算机制\r\n\r\n这里为了鼓励其他用户去清算，使用了奖励机制，这里的 bonus 设置为 10%\r\n\r\n```solidity\r\n// 清算奖励\r\nuint256 private constant _LIQUIDATION_BONUS = 10;\r\n\r\n\r\nfunction liquidate(address collateral, address user, uint256 debtToCover) external {\r\n        uint256 startingUserHealthFactor = _healthFactor(user);\r\n        if(startingUserHealthFactor > _MIN_HEALTH_FACTOR){\r\n            revert DSCEngine__HealthFactorIsNotBroken();\r\n        }\r\n        uint256 tokenAmountFromDebtCovered = getTokenAmountFromUsd(collateral, debtToCover);\r\n\r\n        uint256 bonusCollateral = (tokenAmountFromDebtCovered * _LIQUIDATION_BONUS) / _LIQUIDATION_PRECISION;\r\n        // 赎回抵押品\r\n        _redeemCollateral(collateral, tokenAmountFromDebtCovered + bonusCollateral, user, msg.sender);\r\n        _burnDsc(debtToCover, user, msg.sender);\r\n        // 检查清算后的健康因子\r\n        uint256 endingUserHealthFactor = _healthFactor(user);\r\n        if(endingUserHealthFactor <= _MIN_HEALTH_FACTOR){\r\n            revert DSCEngine__HealthFactorIsBroken();\r\n        }\r\n        _revertIfHealthFactorIsBroken(msg.sender);\r\n\r\n    }\r\n```\r\n\r\n其实关于清算机制，真正用于实践的话，是不行的。涉及到市场代币波动，以及清算活跃度的问题。就拿市场波动来说，如果一个代币的价格波动过大，比如比特币今天10w u，明天雪崩到 5w u了，在这种巨大的波动下，如果没有人即使的去清算资产，会产生资不抵债的问题。形象地假设抵押率是 100%\r\n\r\n```\r\n初始状态：\r\n- 用户抵押了 1000 美元的 ETH\r\n- 借出了 1000 DSC\r\n\r\n当 ETH 价格瞬间下跌 20% 时：\r\n- ETH 抵押品现在只值 800 美元\r\n- 但仍有 1000 DSC 的债务\r\n```\r\n\r\n### 安全机制完善\r\n\r\n要保证三个方面\r\n\r\n- 保证代币的地址是被初始化过的\r\n- 保证输入的数据是不等于0的，防止产生一些垃圾的日志信息，造成gas浪费\r\n- 保证不会被重入攻击\r\n\r\n```solidity\r\n\t // 检查数量是否大于0\r\n    modifier moreThanZero(uint256 value){\r\n        if(value <= 0){\r\n            revert DSCEngine__MoreThanZero();\r\n        }\r\n        _;\r\n        \r\n      // 检查抵押品是否被允许\r\n    modifier isAllowedToken(address tokenAddress){\r\n        if(priceFeeds[tokenAddress] == address(0)){\r\n            revert DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame();\r\n        }\r\n        _;\r\n    }\r\n```\r\n\r\n重入锁直接使用openzepplin的代码库\r\n\r\n```solidity\r\ncontract DSCEngine is ReentrancyGuard \r\n```\r\n\r\n接着就是完善各个函数了，添加到各个函数的后面。\r\n\r\n### 提供数据查询函数\r\n\r\n便于获取数据，方便后续数据的对接\r\n\r\n```solidity\r\n function getCollateralBalanceOfUser(address user, address tokenCollateralAddress) public view returns (uint256) {\r\n        return _collateralDeposited[user][tokenCollateralAddress];\r\n    }\r\n\r\n    function getPrecision() external pure returns (uint256) {\r\n        return _PRECISION;\r\n    }\r\n\r\n    function getAdditionalFeedPrecision() external pure returns (uint256) {\r\n        return _ADDITIONAL_FEED_PRECISION;\r\n    }\r\n\r\n    function getLiquidationThreshold() external pure returns (uint256) {\r\n        return _LIQUIDATION_THRESHOLD;\r\n    }\r\n\r\n    function getLiquidationBonus() external pure returns (uint256) {\r\n        return _LIQUIDATION_BONUS;\r\n    }\r\n\r\n    function getLiquidationPrecision() external pure returns (uint256) {\r\n        return _LIQUIDATION_PRECISION;\r\n    }\r\n\r\n    function getMinHealthFactor() external pure returns (uint256) {\r\n        return _MIN_HEALTH_FACTOR;\r\n    }\r\n\r\n    function getCollateralTokens() external view returns (address[] memory) {\r\n        return _collateralTokens;\r\n    }\r\n\r\n    function getDsc() external view returns (address) {\r\n        return address(i_dsc);\r\n    }\r\n\r\n    function getCollateralTokenPriceFeed(address token) external view returns (address) {\r\n            return priceFeeds[token];\r\n    }\r\n\r\n    function getHealthFactor(address user) external view returns (uint256) {\r\n        return _healthFactor(user);\r\n    }\r\n```"},"author":{"user":"https://learnblockchain.cn/people/19204","address":"0x0c3743ac31156269ea0ea04bdb1864645017a92b"},"history":null,"timestamp":1740882004,"version":1}