Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brave-otter-glides.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/protocol": minor
---

✨ auditor: support non-collateral markets
1,238 changes: 645 additions & 593 deletions .gas-snapshot

Large diffs are not rendered by default.

58 changes: 45 additions & 13 deletions contracts/Auditor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ contract Auditor is Initializable, AccessControlUpgradeable {
vars.price = assetPrice(m.priceFeed);

// sum all the collateral prices
sumCollateral += vars.balance.mulDivDown(vars.price, baseUnit).mulWadDown(adjustFactor);
if (!m.nonCollateral) sumCollateral += vars.balance.mulDivDown(vars.price, baseUnit).mulWadDown(adjustFactor);

// sum all the debt
sumDebtPlusEffects += vars.borrowBalance.mulDivUp(vars.price, baseUnit).divWadUp(adjustFactor);
Expand All @@ -138,7 +138,7 @@ contract Auditor is Initializable, AccessControlUpgradeable {
if (market == marketToSimulate) {
// calculate the effects of redeeming markets
// (having less collateral is the same as having more debt for this calculation)
if (withdrawAmount != 0) {
if (withdrawAmount != 0 && !m.nonCollateral) {
sumDebtPlusEffects += withdrawAmount.mulDivDown(vars.price, baseUnit).mulWadDown(adjustFactor);
}
}
Expand Down Expand Up @@ -179,8 +179,9 @@ contract Auditor is Initializable, AccessControlUpgradeable {
/// @param account address of the account to check for possible shortfall.
/// @param amount amount that the account wants to withdraw or transfer.
function checkShortfall(Market market, address account, uint256 amount) public view virtual {
// if the account is not 'in' the market, bypass the liquidity check
if ((accountMarkets[account] & (1 << markets[market].index)) == 0) return;
// bypass the liquidity check if the account is not 'in' the market or it is disabled as collateral
MarketData storage m = markets[market];
if ((accountMarkets[account] & (1 << m.index)) == 0 || m.nonCollateral) return;

// otherwise, perform a hypothetical liquidity check to guard against shortfall
(uint256 collateral, uint256 debt) = accountLiquidity(account, market, amount);
Expand Down Expand Up @@ -224,10 +225,12 @@ contract Auditor is Initializable, AccessControlUpgradeable {
base.totalDebt += value;
base.adjustedDebt += value.divWadUp(m.adjustFactor);

value = collateral.mulDivDown(m.price, m.baseUnit);
base.totalCollateral += value;
base.adjustedCollateral += value.mulWadDown(m.adjustFactor);
if (market == seizeMarket) base.seizeAvailable = value;
if (!marketData.nonCollateral) {
value = collateral.mulDivDown(m.price, m.baseUnit);
base.totalCollateral += value;
base.adjustedCollateral += value.mulWadDown(m.adjustFactor);
if (market == seizeMarket) base.seizeAvailable = value;
}
}
unchecked {
++i;
Expand Down Expand Up @@ -329,8 +332,10 @@ contract Auditor is Initializable, AccessControlUpgradeable {
if (marketMap & 1 != 0) {
Market market = marketList[i];
MarketData storage m = markets[market];
uint256 assets = market.maxWithdraw(account);
if (assets.mulDivDown(assetPrice(m.priceFeed), 10 ** m.decimals).mulWadDown(m.adjustFactor) > 0) return;
if (!m.nonCollateral) {
uint256 assets = market.maxWithdraw(account);
if (assets.mulDivDown(assetPrice(m.priceFeed), 10 ** m.decimals).mulWadDown(m.adjustFactor) > 0) return;
}
Comment thread
patitonar marked this conversation as resolved.
}
unchecked {
++i;
Expand Down Expand Up @@ -368,10 +373,12 @@ contract Auditor is Initializable, AccessControlUpgradeable {
/// @param market market to add to the protocol.
/// @param priceFeed address of Chainlink's Price Feed aggregator used to query the asset price in base.
/// @param adjustFactor market's adjust factor for the underlying asset.
/// @param nonCollateral true to list the market as ineligible as collateral.
function enableMarket(
Market market,
IPriceFeed priceFeed,
uint128 adjustFactor
uint128 adjustFactor,
bool nonCollateral
) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (market.auditor() != this) revert AuditorMismatch();
if (markets[market].isListed) revert MarketAlreadyListed();
Expand All @@ -383,14 +390,16 @@ contract Auditor is Initializable, AccessControlUpgradeable {
adjustFactor: adjustFactor,
decimals: decimals,
index: uint8(marketList.length),
priceFeed: priceFeed
priceFeed: priceFeed,
nonCollateral: nonCollateral
});

marketList.push(market);

emit MarketListed(market, decimals);
emit PriceFeedSet(market, priceFeed);
emit AdjustFactorSet(market, adjustFactor);
emit NonCollateralSet(market, nonCollateral);
}

/// @notice Sets the adjust factor for a certain market.
Expand All @@ -403,6 +412,21 @@ contract Auditor is Initializable, AccessControlUpgradeable {
emit AdjustFactorSet(market, adjustFactor);
}

/// @notice Sets whether a market's floating supply is ineligible as collateral.
/// @dev Disabling a market as collateral leaves positions that rely on it unhealthy and unliquidatable. The balance
/// stops counting as collateral, is excluded from the seizable set so liquidation reverts with `ZeroRepay`, and stays
/// freely withdrawable by its owner. The remaining debt is then liable to be cleared as bad debt against
/// `earningsAccumulator`. Do not enable this flag on a market with outstanding collateral-backed debt without first
/// winding that exposure down.
/// @param market address of the market to change collateral eligibility for.
/// @param nonCollateral true to make the market ineligible as collateral.
function setNonCollateral(Market market, bool nonCollateral) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (!markets[market].isListed) revert MarketNotListed();

markets[market].nonCollateral = nonCollateral;
emit NonCollateralSet(market, nonCollateral);
}

Comment thread
patitonar marked this conversation as resolved.
/// @notice Sets the Chainlink Price Feed Aggregator source for a market.
/// @param market market address of the asset.
/// @param priceFeed address of Chainlink's Price Feed aggregator used to query the asset price in base.
Expand Down Expand Up @@ -437,6 +461,11 @@ contract Auditor is Initializable, AccessControlUpgradeable {
/// @param account address of the account that just left a market.
event MarketExited(Market indexed market, address indexed account);

/// @notice Emitted when a market's collateral eligibility is changed by admin.
/// @param market address of the market whose collateral eligibility changed.
/// @param nonCollateral true if the market is now ineligible as collateral.
event NonCollateralSet(Market indexed market, bool nonCollateral);

/// @notice Emitted when a adjust factor is changed by admin.
/// @param market address of the market that has a new adjust factor.
/// @param adjustFactor adjust factor for the underlying asset.
Expand All @@ -452,17 +481,20 @@ contract Auditor is Initializable, AccessControlUpgradeable {
event PriceFeedSet(Market indexed market, IPriceFeed indexed priceFeed);

/// @notice Stores the market parameters used for liquidity calculations.
/// @param adjustFactor used to asses the lending power of the market's underlying asset.
/// @param adjustFactor used to assess the borrowing and lending power of the market's underlying asset.
/// @param decimals number of decimals of the market's underlying asset.
/// @param index index of the market in the `marketList`.
/// @param isListed true if the market is enabled.
/// @param priceFeed address of the price feed used to query the asset's price.
/// @param nonCollateral true if the market's floating supply is ineligible as collateral.
// solhint-disable-next-line gas-struct-packing
struct MarketData {
uint128 adjustFactor;
uint8 decimals;
uint8 index;
bool isListed;
IPriceFeed priceFeed;
bool nonCollateral;
Comment thread
patitonar marked this conversation as resolved.
}

/// @notice Stores the liquidator and lenders factors used in liquidations to calculate the amount to seize.
Expand Down
2 changes: 1 addition & 1 deletion contracts/periphery/DebtManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ contract DebtManager is Initializable {
}

function checkMarket(Market market) internal view {
(, , , bool listed, ) = auditor.markets(market);
(, , , bool listed, , ) = auditor.markets(market);
Comment thread
patitonar marked this conversation as resolved.
if (!listed) revert MarketNotListed();
}

Expand Down
72 changes: 43 additions & 29 deletions contracts/periphery/DebtPreviewer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ contract DebtPreviewer {
) internal view returns (uint256) {
MinDepositVars memory vars;
Auditor auditor = debtManager.auditor();
(vars.adjustFactorIn, vars.decimalsIn, , , vars.priceFeedIn) = auditor.markets(marketDeposit);
(vars.adjustFactorOut, vars.decimalsOut, , , vars.priceFeedOut) = auditor.markets(marketBorrow);
(vars.adjustFactorIn, vars.decimalsIn, , , vars.priceFeedIn, vars.nonCollateral) = auditor.markets(marketDeposit);
Comment thread
patitonar marked this conversation as resolved.
if (vars.nonCollateral) return 0;
(vars.adjustFactorOut, vars.decimalsOut, , , vars.priceFeedOut, ) = auditor.markets(marketBorrow);

return
minHealthFactor
Expand Down Expand Up @@ -182,8 +183,8 @@ contract DebtPreviewer {
uint256 amountIn
) internal view returns (uint256) {
Auditor auditor = debtManager.auditor();
(, uint256 decimalsOut, , , IPriceFeed priceFeedOut) = auditor.markets(marketBorrow);
(, uint256 decimalsIn, , , IPriceFeed priceFeedIn) = auditor.markets(marketDeposit);
(, uint256 decimalsOut, , , IPriceFeed priceFeedOut, ) = auditor.markets(marketBorrow);
(, uint256 decimalsIn, , , IPriceFeed priceFeedIn, ) = auditor.markets(marketDeposit);
return
amountIn.mulDivDown(auditor.assetPrice(priceFeedIn), 10 ** decimalsIn).mulDivDown(
10 ** decimalsOut,
Expand All @@ -206,8 +207,9 @@ contract DebtPreviewer {
) internal view returns (uint256) {
Auditor auditor = debtManager.auditor();
MaxRatioVars memory mr;
(mr.adjustFactorIn, , , , mr.priceFeedIn) = auditor.markets(marketDeposit);
(mr.adjustFactorOut, , , , ) = auditor.markets(marketBorrow);
(mr.adjustFactorIn, , , , mr.priceFeedIn, mr.nonCollateral) = auditor.markets(marketDeposit);
if (mr.nonCollateral) return 1e18;
(mr.adjustFactorOut, , , , , ) = auditor.markets(marketBorrow);
uint256 isolatedMaxRatio = minHealthFactor.divWadDown(
minHealthFactor - mr.adjustFactorIn.mulWadDown(mr.adjustFactorOut)
);
Expand All @@ -220,7 +222,7 @@ contract DebtPreviewer {
Auditor.MarketData memory md;
Auditor.AccountLiquidity memory vars;
mr.market = auditor.marketList(mr.i);
(md.adjustFactor, md.decimals, , , md.priceFeed) = auditor.markets(mr.market);
(md.adjustFactor, md.decimals, , , md.priceFeed, md.nonCollateral) = auditor.markets(mr.market);
(vars.balance, vars.borrowBalance) = mr.market.accountSnapshot(account);
vars.price = auditor.assetPrice(md.priceFeed);
mr.baseUnit = 10 ** md.decimals;
Expand All @@ -232,7 +234,7 @@ contract DebtPreviewer {
} else {
mr.adjustedDebt += vars.borrowBalance.mulDivUp(vars.price, mr.baseUnit).divWadUp(md.adjustFactor);
}
if (mr.market != marketDeposit) {
if (mr.market != marketDeposit && !md.nonCollateral) {
mr.adjustedCollateral += vars.balance.mulDivDown(vars.price, mr.baseUnit).mulWadDown(md.adjustFactor);
}
}
Expand All @@ -241,14 +243,19 @@ contract DebtPreviewer {
}
}

uint256 collateral = mr.adjustedCollateral.mulWadDown(mr.adjustFactorOut) +
minHealthFactor.mulWadDown(mr.principalUSD);
uint256 debt = minHealthFactor.mulWadDown(mr.adjustedDebt.mulWadDown(mr.adjustFactorOut));
if (debt >= collateral) return 1e18;
return
Math.min(
(mr.adjustedCollateral.mulWadDown(mr.adjustFactorOut) +
minHealthFactor.mulWadDown(mr.principalUSD) -
minHealthFactor.mulWadDown(mr.adjustedDebt.mulWadDown(mr.adjustFactorOut))).divWadDown(
Math.max(
Math.min(
(collateral - debt).divWadDown(
mr.principalUSD.mulWadDown(minHealthFactor - mr.adjustFactorIn.mulWadDown(mr.adjustFactorOut))
),
isolatedMaxRatio
isolatedMaxRatio
),
1e18
);
}

Expand All @@ -275,19 +282,23 @@ contract DebtPreviewer {
if (mw.principal <= 0) return 0;

mw.auditor = debtManager.auditor();
(, , , , , mw.nonCollateral) = mw.auditor.markets(marketDeposit);
if (mw.nonCollateral) return uint256(mw.principal);
Auditor.MarketData memory md;
Auditor.AccountLiquidity memory vars;
mw.marketMap = mw.auditor.accountMarkets(account);
mw.borrowAssets = floatingBorrowAssets(marketBorrow, account);
for (mw.i = 0; mw.marketMap != 0; mw.marketMap >>= 1) {
if (mw.marketMap & 1 != 0) {
mw.market = mw.auditor.marketList(mw.i);
(md.adjustFactor, md.decimals, , , md.priceFeed) = mw.auditor.markets(mw.market);
(md.adjustFactor, md.decimals, , , md.priceFeed, md.nonCollateral) = mw.auditor.markets(mw.market);
uint256 baseUnit = 10 ** md.decimals;
(vars.balance, vars.borrowBalance) = mw.market.accountSnapshot(account);
vars.price = mw.auditor.assetPrice(md.priceFeed);
{
mw.memAdjColl = vars.balance.mulDivDown(vars.price, baseUnit).mulWadDown(md.adjustFactor);
mw.memAdjColl = md.nonCollateral
? 0
: vars.balance.mulDivDown(vars.price, baseUnit).mulWadDown(md.adjustFactor);
mw.memAdjDebt = vars.borrowBalance.mulDivDown(vars.price, baseUnit).divWadDown(md.adjustFactor);
mw.adjustedCollateral += mw.memAdjColl;

Expand All @@ -312,13 +323,14 @@ contract DebtPreviewer {
}
}
{
(mw.adjustFactorIn, , , , mw.priceFeedIn) = mw.auditor.markets(marketDeposit);
(mw.adjustFactorOut, , , , ) = mw.auditor.markets(marketBorrow);
(mw.adjustFactorIn, , , , mw.priceFeedIn, ) = mw.auditor.markets(marketDeposit);
(mw.adjustFactorOut, , , , , ) = mw.auditor.markets(marketBorrow);
mw.memOtherDebt = mw.otherDebt.mulWadDown(mw.adjustFactorOut).mulWadDown(minHealthFactor);
mw.memOtherCollateral = (mw.otherCollateral).mulWadDown(mw.adjustFactorOut);
}

if (mw.memOtherDebt <= mw.memOtherCollateral) {
if (mw.adjustedDebt + mw.adjPrincipalForRepay >= mw.adjustedCollateral + mw.adjustedRepay) return 0;
return
Math.min(
Math
Expand All @@ -332,15 +344,14 @@ contract DebtPreviewer {
);
}

return
uint256(mw.principal) -
(mw.memOtherDebt - mw.memOtherCollateral)
.divWadDown(
mw.adjustFactorIn.mulWadDown(ratio).mulWadDown(mw.adjustFactorOut) +
minHealthFactor -
ratio.mulWadDown(minHealthFactor)
)
.mulDivDown(10 ** marketDeposit.decimals(), mw.auditor.assetPrice(mw.priceFeedIn));
uint256 support = mw.adjustFactorIn.mulWadDown(ratio).mulWadDown(mw.adjustFactorOut) + minHealthFactor;
uint256 ratioDebt = ratio.mulWadDown(minHealthFactor);
if (ratioDebt >= support) return 0;
uint256 requiredPrincipal = (mw.memOtherDebt - mw.memOtherCollateral).divWadDown(support - ratioDebt).mulDivDown(
10 ** marketDeposit.decimals(),
mw.auditor.assetPrice(mw.priceFeedIn)
);
return uint256(mw.principal) > requiredPrincipal ? uint256(mw.principal) - requiredPrincipal : 0;
}

/// @notice Calculates the crossed principal amount for a given `account` in the input and output markets.
Expand All @@ -353,8 +364,8 @@ contract DebtPreviewer {
IPriceFeed priceFeedIn;
IPriceFeed priceFeedOut;
Auditor auditor = debtManager.auditor();
(, decimalsIn, , , priceFeedIn) = auditor.markets(marketDeposit);
(, decimalsOut, , , priceFeedOut) = auditor.markets(marketBorrow);
(, decimalsIn, , , priceFeedIn, ) = auditor.markets(marketDeposit);
(, decimalsOut, , , priceFeedOut, ) = auditor.markets(marketBorrow);

return
int256(marketDeposit.maxWithdraw(account)) -
Expand Down Expand Up @@ -469,7 +480,7 @@ contract DebtPreviewer {
r.controller = market.rewardsController();
Auditor auditor = debtManager.auditor();
if (address(r.controller) != address(0)) {
(, r.underlyingDecimals, , , r.underlyingPriceFeed) = auditor.markets(market);
(, r.underlyingDecimals, , , r.underlyingPriceFeed, ) = auditor.markets(market);
unchecked {
r.underlyingBaseUnit = 10 ** r.underlyingDecimals;
}
Expand Down Expand Up @@ -578,6 +589,7 @@ struct Limit {

struct MaxRatioVars {
uint256 i;
bool nonCollateral;
uint256 baseUnit;
uint256 marketMap;
uint256 principalUSD;
Expand All @@ -592,6 +604,7 @@ struct MaxRatioVars {
struct MaxWithdrawVars {
uint256 i;
int256 principal;
bool nonCollateral;
uint256 marketMap;
uint256 otherDebt;
uint256 memAdjDebt;
Expand Down Expand Up @@ -619,6 +632,7 @@ struct MinDepositVars {
IPriceFeed priceFeedIn;
uint256 adjustFactorOut;
IPriceFeed priceFeedOut;
bool nonCollateral;
}

struct Rates {
Expand Down
2 changes: 1 addition & 1 deletion contracts/periphery/DebtRoller.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ contract DebtRoller is IFlashLoanRecipient, Initializable, AccessControlUpgradea
}

function _checkMarket(Market market) internal view {
(, , , bool listed, ) = auditor.markets(market);
(, , , bool listed, , ) = auditor.markets(market);
Comment thread
patitonar marked this conversation as resolved.
if (!listed) revert NotMarket();
}
}
Expand Down
2 changes: 1 addition & 1 deletion contracts/periphery/InstallmentsRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ contract InstallmentsRouter {
/// @notice Reverts if the Market is not listed by the Auditor.
/// @param market The Market to check.
function checkMarket(Market market) internal view {
(, , , bool listed, ) = auditor.markets(market);
(, , , bool listed, , ) = auditor.markets(market);
Comment thread
patitonar marked this conversation as resolved.
if (!listed) revert MarketNotListed();
}

Expand Down
Loading