so can someone please help me come up with a solution to implement the change I want to make to my stable coin project please?
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 following properties:
- Exogenously Collateralized
- Dollar Pegged
- Algorithmically Stable
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. This contract is based on the MakerDAO DSS system
But I have created a separate contract(IndexedAssetPriceFeed.sol) that you can pass in multiple ChainLink price feed addresses (all USD pairs), the way it works is like this you pass in multiple price feeds of assets, and then it adds up all the USD values of all the assets then divides by the number of assets returning a single indexed average value in USD.
for example: you pass in 10 price feeds (asset1 / USD) = $3000; (asset2 / USD) = $2000; (asset3 / USD) = $500; (asset4 / USD) = $150; (asset5 / USD) = $200; (asset6 / USD) = $75; (asset7 / USD) = $20; (asset8 / USD) = $1; (asset9 / USD) = $17; (asset10 / USD) = $37;
asset1 + asset2 + ...asset10 = $6000 / 10 = $600.
and then I want to peg the stablecoin to the indexed average value instead of the US Dollar, so the stability comes from Diversification similar to how an Index fund on the stock market works. I don't want to change anything about the contract except change what the DSC token is pegged to.
But I need help with coming up with a way to implement this. so first I am going to provide the needed contracts and context.
- I am using the Foundry framework.
- Based on this GitHub repository 'https://github.com/Cyfrin/foundry-defi-stablecoin-f23' by Patrick Collins.
- tried solutions at the bottom.
so here is the IndexedAssetPriceFeed.sol contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {AggregatorV3Interface} from "../../lib/chainlink-brownie-contracts/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import {MockV3Aggregator} from "../../test/mocks/MockV3Aggregator.sol";
contract IndexedAssetPriceFeed {
error PriceTooHigh(uint256 givenPrice);
AggregatorV3Interface[] public priceFeeds;
MockV3Aggregator[] public mockPriceFeeds;
uint256 public lastKnownPrice;
mapping(address => uint256) public prices;
// can pass in the priceFeed Addresses in on deployment
constructor(AggregatorV3Interface[] memory _priceFeeds) {
priceFeeds = _priceFeeds;
}
function getLatestPrice() public returns (uint256) {
return updatePrice();
}
function updatePrice() public returns(uint256) {
uint256 total = 0;
uint256 count = 0;
for (uint256 i = 0; i < priceFeeds.length; i++) {
try priceFeeds[i].latestRoundData() returns (uint80, int256 price, uint256, uint256, uint80) {
prices[address(priceFeeds[i])] = uint256(price);
total += uint256(price);
count++;
} catch {
revert("Call to latestRoundData failed");
}
}
lastKnownPrice = total / count;
return lastKnownPrice;
}
function getLatestTimeStamp() public view returns (uint256) {
uint256 latestTimeStamp = 0;
for (uint256 i = 0; i < priceFeeds.length; i++) {
(, , , uint256 updatedAt, ) = priceFeeds[i].latestRoundData();
if (updatedAt > latestTimeStamp) {
latestTimeStamp = updatedAt;
}
}
return latestTimeStamp;
}
function setRoundDataForTesting(address _mockPriceFeedAddress, uint80 _roundId, int256 _answer, uint256 _timestamp, uint256 _startedAt) external {
// Assuming you want to set the round data for the first price feed
MockV3Aggregator mockPriceFeed = MockV3Aggregator(_mockPriceFeedAddress);
// Call the updateRoundData function on the price feed
mockPriceFeed.updateRoundData(_roundId, _answer, _timestamp, _startedAt);
}
function getPriceFeeds() public view returns (AggregatorV3Interface[] memory) {
return priceFeeds;
}
}
here is the OracleLib.sol contract if needed:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// import {AggregatorV3Interface} from "lib/chainlink-brownie-contracts/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import {console} from "lib/forge-std/src/console.sol";
import { IndexedAssetPriceFeed } from './IndexedAssetPriceFeed.sol';
import {AggregatorV3Interface} from "../../lib/chainlink-brownie-contracts/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
/*
* @title OracleLib
* @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(string message);
uint256 private constant TIMEOUT = 3 hours;
function staleCheckLatestRoundData(IndexedAssetPriceFeed indexedAssetPriceFeed)
public returns (uint256)
{
uint256 price = indexedAssetPriceFeed.getLatestPrice();
// console.log(price);
uint256 secondsSince = block.timestamp - indexedAssetPriceFeed.getLatestTimeStamp();
// console.log("seconds Since Latest timestamp: %s", secondsSince);
if (secondsSince > getTimeout()) revert OracleLib__StalePrice("Stale price detected");
// console.log(price);
return price;
}
function getTimeout() public pure returns (uint256) {
return TIMEOUT;
}
// AggregatorV3Interface price tests
function staleCheckLatestRoundDataOnCollateral(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("Stale price detected");
}
uint256 secondsSince = block.timestamp - updatedAt;
if (secondsSince > TIMEOUT) revert OracleLib__StalePrice("Stale price detected");
return (roundId, answer, startedAt, updatedAt, answeredInRound);
}
function getTimeoutAggregatorV3Interface(AggregatorV3Interface /* chainlinkFeed */ ) public pure returns (uint256) {
return TIMEOUT;
}
}
and here is the DSCEnigne contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {AggregatorV3Interface} from "lib/chainlink-brownie-contracts/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import {OracleLib} from "./libraries/OracleLib.sol";
import {IndexedAssetPriceFeed} from "./libraries/IndexedAssetPriceFeed.sol";
import {ReentrancyGuard} from "lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {DecentralizedStableCoin} from "./DecentralizedStableCoin.sol";
// import {console} from "../lib/forge-std/src/console.sol";
/*
* @title DSCEngine
* @author Dylan Katsch
* credit to Patrick Collins, this project is based on a repository for Patrick Collins's GitHub.
*
* 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
*
* @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);
// IndexedAssetPriceFeed indexedAssetPriceFeed = new IndexedAssetPriceFeed(priceFeedAddresses);
}
///////////////////
// 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 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)
{
_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
{
_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
* you 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
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 hav 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,,,) = OracleLib.staleCheckLatestRoundDataOnCollateral(priceFeed);
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,,,) = OracleLib.staleCheckLatestRoundDataOnCollateral(priceFeed);
// $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);
}
}
what I have tried:
obviously instantiate the IndexedAssetPriceFeed.sol contract in the constructor of the DSCEngine.sol contract
but then the pegging of the DSC token to USD happens in the _getUsdValue() function which returns the USD value of the deposited collateral the returned value goes through a few functions but ends up in the mintDsc() function where it gets pegged on minting
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();
}
}
function _getUsdValue(address token, uint256 amount) private view returns (uint256) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]);
(, int256 price,,,) = OracleLib.staleCheckLatestRoundDataOnCollateral(priceFeed);
return ((uint256(price) * ADDITIONAL_FEED_PRECISION) * amount) / PRECISION;
}
relevant function can be seen in the provided DSCEngine contract :
- getAccountCollateralValue()
- _healthFactor() & _calculateHealthFactor()
- depositCollateral()
so I don't know if I so call/get the average indexed value in the mintDsc function or in the _getUsdValue function, I think doing it in the mintDsc function will be simpler because we would have to change multiple functions to handle the extra information so something like this maybe?
function mintDsc(uint256 amountDscToMint) public moreThanZero(amountDscToMint) nonReentrant {
// Retrieve the indexed average price from the IndexedAssetPriceFeed contract
uint256 indexedPrice = indexedAssetPriceFeed.getLatestPrice();
// Calculate the total value of the collateral in indexed units
uint256 collateralValueInIndexedUnits = (amountDscToMint * indexedPrice) / PRECISION;
// Check if the collateral value meets the minimum requirements for minting
uint256 totalCollateralValueInIndexedUnits = getAccountCollateralValue(msg.sender);
require(totalCollateralValueInIndexedUnits >= collateralValueInIndexedUnits, "Insufficient collateral");
// Update the amount of DSC minted by the user
s_DSCMinted[msg.sender] += amountDscToMint;
// Mint the DSC tokens
bool minted = i_dsc.mint(msg.sender, amountDscToMint);
require(minted, "Minting failed");
// Revert if the health factor is broken after minting
revertIfHealthFactorIsBroken(msg.sender);
}
I know I am asking a lot but please help, I am also not sure that I 100% understand everything, maybe there is something I overlooked so if a more advanced dev comes up with a completely different solution it's cool.
Thank you to anyone willing to help. Please ask if there is any confusion about anything.