浪迹陨灭nd
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9

项目构建
我们这里采用超额抵押的算法机制,来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); }
roundId
(uint80
)
- 含义:这是当前价格更新的“轮次ID”(Round ID)。
- 解释:Chainlink 预言机是通过多轮次的方式来聚合数据的,每一轮都会有一个唯一的
roundId
。这个roundId
用于标识这是第几轮价格更新或报告。 - 用途:通过
roundId
,你可以知道当前的价格数据是哪一轮生成的。
answer
(int256
)
- 含义:这是预言机返回的实际答案,即你请求的数据结果。
- 解释:对于价格预言机来说,
answer
通常是某种资产的价格,例如 ETH/USD 或 BTC/USD 的价格。 - 类型为
int256
是因为价格可能为负数(尽管在实际使用中很少见)。例如,它可以用于某些负值的经济数据。
startedAt
(uint256
)
- 含义:这是当前这一轮价格更新的启动时间。
- 解释:
startedAt
代表这一轮价格数据采集的开始时间,通常是 UNIX 时间戳(即从1970年1月1日以来的秒数)。 - 用途:通过这个时间戳,你可以知道这一轮价格数据什么时候开始聚合的。
updatedAt
(uint256
)
- 含义:这是当前价格更新的时间戳。
- 解释:
updatedAt
代表预言机在这一轮价格更新的确切时间,也是 UNIX 时间戳格式。 - 用途:可以用于追踪价格数据的最新更新时间,判断数据是否及时。
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 < _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 < _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 <= _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 <= 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); }
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
