Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add AliasedLossPolicyV3 and move price updates to price feed store #299

Merged
merged 3 commits into from
Feb 19, 2025
Merged
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
311 changes: 311 additions & 0 deletions contracts/core/AliasedLossPolicyV3.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
// SPDX-License-Identifier: BUSL-1.1
// Gearbox Protocol. Generalized leverage for DeFi protocols
// (c) Gearbox Foundation, 2024.
pragma solidity ^0.8.17;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import {IAliasedLossPolicyV3} from "../interfaces/IAliasedLossPolicyV3.sol";
import {ICreditAccountV3} from "../interfaces/ICreditAccountV3.sol";
import {ICreditManagerV3} from "../interfaces/ICreditManagerV3.sol";
import {IPoolQuotaKeeperV3} from "../interfaces/IPoolQuotaKeeperV3.sol";
import {IPoolV3} from "../interfaces/IPoolV3.sol";
import {IPriceOracleV3, PriceFeedParams} from "../interfaces/IPriceOracleV3.sol";
import {IAddressProvider} from "../interfaces/base/IAddressProvider.sol";
import {IPriceFeedStore, PriceUpdate} from "../interfaces/base/IPriceFeedStore.sol";

import {BitMask} from "../libraries/BitMask.sol";
import {
AP_PRICE_FEED_STORE,
NO_VERSION_CONTROL,
PERCENTAGE_FACTOR,
RAY,
UNDERLYING_TOKEN_MASK
} from "../libraries/Constants.sol";
import {MarketHelper} from "../libraries/MarketHelper.sol";

import {ACLTrait} from "../traits/ACLTrait.sol";
import {PriceFeedValidationTrait} from "../traits/PriceFeedValidationTrait.sol";

import {TokenIsNotQuotedException} from "../interfaces/IExceptions.sol";

/// @title Aliased loss policy V3
/// @notice Loss policy that allows to double-check the decision on whether to liquidate a credit account with bad debt
/// using TWV recomputed with alias price feeds. This can be useful in scenarios where token's market price
/// drops for a short period of time while its fundamental value remains the same.
/// It also allows to restrict such liquidations to only be performed by accounts with `LOSS_LIQUIDATOR` role
/// which can then return premium to recover part of the losses.
contract AliasedLossPolicyV3 is ACLTrait, PriceFeedValidationTrait, IAliasedLossPolicyV3 {
using BitMask for uint256;
using EnumerableSet for EnumerableSet.AddressSet;
using MarketHelper for IPoolV3;

/// @dev Internal enum with possible price feed types
enum PriceFeedType {
Normal,
Aliased
}

/// @dev Internal struct that contains shared info needed for collateral calculation
struct SharedInfo {
address creditManager;
address priceOracle;
address quotaKeeper;
uint256 underlyingPriceRAY;
}

/// @dev Internal struct that contains token info needed for collateral calculation
struct TokenInfo {
address token;
uint16 lt;
uint256 balance;
uint256 quotaUSD;
PriceFeedParams aliasParams;
}

/// @notice Contract version
uint256 public constant override version = 3_10;

/// @notice Contract type
bytes32 public constant override contractType = "LOSS_POLICY::ALIASED";

/// @notice Pool for which the loss policy is applied
address public immutable override pool;

/// @notice Pool's underlying token
address public immutable override underlying;

/// @notice Price feed store
address public immutable override priceFeedStore;

/// @notice Access mode for loss liquidations
AccessMode public override accessMode = AccessMode.Permissionless;

/// @notice Whether policy checks are enabled
bool public override checksEnabled = true;

/// @dev Set of tokens that have alias price feeds
EnumerableSet.AddressSet internal _tokensWithAliasSet;

/// @dev Mapping from token to its alias price feed parameters
mapping(address => PriceFeedParams) internal _aliasPriceFeedParams;

/// @notice Constructor
/// @param pool_ Pool address
/// @param addressProvider_ Address provider contract address
/// @custom:tests U:[ALP-1]
constructor(address pool_, address addressProvider_) ACLTrait(IPoolV3(pool_).getACL()) {
pool = pool_;
underlying = IPoolV3(pool_).asset();
priceFeedStore = IAddressProvider(addressProvider_).getAddressOrRevert(AP_PRICE_FEED_STORE, NO_VERSION_CONTROL);
}

// ------- //
// GETTERS //
// ------- //

/// @notice Serializes the loss policy state
/// @custom:tests U:[ALP-1], U:[ALP-2], U:[ALP-3]
function serialize() external view override returns (bytes memory) {
address[] memory tokens = _tokensWithAliasSet.values();
uint256 numTokens = tokens.length;
PriceFeedParams[] memory priceFeedParams = new PriceFeedParams[](numTokens);
for (uint256 i; i < numTokens; ++i) {
priceFeedParams[i] = _aliasPriceFeedParams[tokens[i]];
}
return abi.encode(accessMode, checksEnabled, tokens, priceFeedParams);
}

/// @notice Returns whether `creditAccount` can be liquidated with loss by `caller`
/// @custom:tests U:[ALP-4], U:[ALP-5]
function isLiquidatableWithLoss(address creditAccount, address caller, Params calldata params)
external
override
returns (bool)
{
AccessMode accessMode_ = accessMode;
if (accessMode_ == AccessMode.Forbidden) return false;
if (accessMode_ == AccessMode.Permissioned && !_hasRole("LOSS_LIQUIDATOR", caller)) return false;
if (!checksEnabled) return true;

_updatePrices(params.extraData);

return _adjustForAliases(creditAccount, params.twvUSD) < params.totalDebtUSD;
}

/// @notice Returns the list of tokens that have alias price feeds
function getTokensWithAlias() external view override returns (address[] memory) {
return _tokensWithAliasSet.values();
}

/// @notice Returns `token`'s alias price feed parameters
function getAliasPriceFeedParams(address token) external view override returns (PriceFeedParams memory) {
return _aliasPriceFeedParams[token];
}

/// @notice Returns the list of alias price feeds that need to return a valid price to liquidate `creditAccount`
/// @custom:tests U:[ALP-6]
function getRequiredAliasPriceFeeds(address creditAccount)
external
view
override
returns (address[] memory priceFeeds)
{
address creditManager = ICreditAccountV3(creditAccount).creditManager();
uint256 remainingTokensMask =
ICreditManagerV3(creditManager).enabledTokensMaskOf(creditAccount).disable(UNDERLYING_TOKEN_MASK);
priceFeeds = new address[](remainingTokensMask.calcEnabledTokens());
uint256 numAliases;
while (remainingTokensMask != 0) {
uint256 tokenMask = remainingTokensMask.lsbMask();
remainingTokensMask ^= tokenMask;

address token = ICreditManagerV3(creditManager).getTokenByMask(tokenMask);
address aliasPriceFeed = _aliasPriceFeedParams[token].priceFeed;
if (aliasPriceFeed != address(0)) priceFeeds[numAliases++] = aliasPriceFeed;
}
assembly {
mstore(priceFeeds, numAliases)
}
}

// ------------- //
// CONFIGURATION //
// ------------- //

/// @notice Sets access mode for liquidations
/// @dev Reverts if caller is not configurator
/// @custom:tests U:[ALP-2]
function setAccessMode(AccessMode mode) external override configuratorOnly {
if (accessMode == mode) return;
accessMode = mode;
emit SetAccessMode(mode);
}

/// @notice Enables or disables policy checks
/// @dev Reverts if caller is not configurator
/// @custom:tests U:[ALP-2]
function setChecksEnabled(bool enabled) external override configuratorOnly {
if (checksEnabled == enabled) return;
checksEnabled = enabled;
emit SetChecksEnabled(enabled);
}

/// @notice Sets `token`'s alias price feed to `priceFeed`, unsets it if `priceFeed` is zero
/// @dev Reverts if caller is not configurator
/// @dev Reverts if `token` is not quoted (including underlying)
/// @dev Reverts if `priceFeed` is not known to the price feed store
/// @custom:tests U:[ALP-3]
function setAliasPriceFeed(address token, address priceFeed) external override configuratorOnly {
if (_aliasPriceFeedParams[token].priceFeed == priceFeed) return;

if (!IPoolQuotaKeeperV3(IPoolV3(pool).poolQuotaKeeper()).isQuotedToken(token)) {
revert TokenIsNotQuotedException();
}

if (priceFeed == address(0)) {
if (_tokensWithAliasSet.remove(token)) {
delete _aliasPriceFeedParams[token];
emit UnsetAliasPriceFeed(token);
}
return;
}

uint32 stalenessPeriod = IPriceFeedStore(priceFeedStore).getStalenessPeriod(priceFeed);
bool skipCheck = _validatePriceFeed(priceFeed, stalenessPeriod);
_aliasPriceFeedParams[token] = PriceFeedParams({
priceFeed: priceFeed,
stalenessPeriod: stalenessPeriod,
skipCheck: skipCheck,
tokenDecimals: ERC20(token).decimals()
});

_tokensWithAliasSet.add(token);
emit SetAliasPriceFeed(token, priceFeed, stalenessPeriod, skipCheck);
}

// --------- //
// INTERNALS //
// --------- //

/// @dev If provided non-empty `data`, updates prices in the price feed store
/// @custom:tests U:[ALP-5]
function _updatePrices(bytes calldata data) internal {
if (data.length == 0) return;
PriceUpdate[] memory priceUpdates = abi.decode(data, (PriceUpdate[]));
IPriceFeedStore(priceFeedStore).updatePrices(priceUpdates);
}

/// @dev Adjusts credit account's TWV for difference between normal and alias price feeds
/// @custom:tests U:[ALP-7]
function _adjustForAliases(address creditAccount, uint256 twvUSD) internal view returns (uint256 twvUSDAliased) {
SharedInfo memory sharedInfo = _getSharedInfo(creditAccount);
twvUSDAliased = twvUSD;

uint256 remainingTokensMask =
ICreditManagerV3(sharedInfo.creditManager).enabledTokensMaskOf(creditAccount).disable(UNDERLYING_TOKEN_MASK);
while (remainingTokensMask != 0) {
uint256 tokenMask = remainingTokensMask.lsbMask();
remainingTokensMask ^= tokenMask;

TokenInfo memory tokenInfo = _getTokenInfo(creditAccount, tokenMask, sharedInfo);
// no need to check other fields since `quotaUSD` is initialized only if all of them are non-zero
if (tokenInfo.quotaUSD == 0) continue;

twvUSDAliased += _getWeightedValueUSD(tokenInfo, sharedInfo, PriceFeedType.Aliased);
twvUSDAliased -= _getWeightedValueUSD(tokenInfo, sharedInfo, PriceFeedType.Normal);
}
}

/// @dev Returns the shared info needed for `creditAccount` collateral value calculation
/// @custom:tests U:[ALP-8]
function _getSharedInfo(address creditAccount) internal view returns (SharedInfo memory sharedInfo) {
sharedInfo.creditManager = ICreditAccountV3(creditAccount).creditManager();
sharedInfo.priceOracle = ICreditManagerV3(sharedInfo.creditManager).priceOracle();
sharedInfo.quotaKeeper = IPoolV3(pool).poolQuotaKeeper();
sharedInfo.underlyingPriceRAY = IPriceOracleV3(sharedInfo.priceOracle).convertToUSD(RAY, underlying);
}

/// @dev Returns the token info needed for `creditAccount` collateral value calculation
/// @custom:tests U:[ALP-9]
function _getTokenInfo(address creditAccount, uint256 tokenMask, SharedInfo memory sharedInfo)
internal
view
returns (TokenInfo memory info)
{
(info.token, info.lt) = ICreditManagerV3(sharedInfo.creditManager).collateralTokenByMask(tokenMask);
if (info.lt == 0) return info;

info.aliasParams = _aliasPriceFeedParams[info.token];
if (info.aliasParams.priceFeed == address(0)) return info;

info.balance = ERC20(info.token).balanceOf(creditAccount);
if (info.balance == 0) return info;

(uint256 quota,) = IPoolQuotaKeeperV3(sharedInfo.quotaKeeper).getQuota(creditAccount, info.token);
info.quotaUSD = quota * sharedInfo.underlyingPriceRAY / RAY;
}

/// @dev Returns the weighted value in USD (computed via either normal or alias price feed) for a single token
/// @custom:tests U:[ALP-10]
function _getWeightedValueUSD(TokenInfo memory tokenInfo, SharedInfo memory sharedInfo, PriceFeedType priceFeedType)
internal
view
returns (uint256)
{
uint256 valueUSD = priceFeedType == PriceFeedType.Aliased
? _convertToUSDAlias(tokenInfo.aliasParams, tokenInfo.balance)
: IPriceOracleV3(sharedInfo.priceOracle).convertToUSD(tokenInfo.balance, tokenInfo.token);

return Math.min(valueUSD * tokenInfo.lt / PERCENTAGE_FACTOR, tokenInfo.quotaUSD);
}

/// @dev Converts token amount to USD using its alias price feed
/// @custom:tests U:[ALP-11]
function _convertToUSDAlias(PriceFeedParams memory aliasParams, uint256 amount) internal view returns (uint256) {
int256 answer = _getValidatedPrice(aliasParams.priceFeed, aliasParams.stalenessPeriod, aliasParams.skipCheck);
return uint256(answer) * amount / (10 ** aliasParams.tokenDecimals);
}
}
37 changes: 1 addition & 36 deletions contracts/core/PriceOracleV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
PriceFeedDoesNotExistException,
PriceFeedIsNotUpdatableException
} from "../interfaces/IExceptions.sol";
import {IPriceOracleV3, PriceFeedParams, PriceUpdate} from "../interfaces/IPriceOracleV3.sol";
import {IPriceOracleV3, PriceFeedParams} from "../interfaces/IPriceOracleV3.sol";
import {IUpdatablePriceFeed} from "../interfaces/base/IPriceFeed.sol";

import {ACLTrait} from "../traits/ACLTrait.sol";
Expand All @@ -32,8 +32,6 @@ import {SanityCheckTrait} from "../traits/SanityCheckTrait.sol";
/// The primary purpose of reserve price feeds is to upper-bound main ones during the collateral check after
/// operations that allow users to offload mispriced tokens on Gearbox and withdraw underlying; they should
/// not be used for general collateral evaluation, including decisions on whether accounts are liquidatable.
/// - Finally, this contract serves as register for updatable price feeds and can be used to apply batched
/// on-demand price updates while ensuring that those are not calls to arbitrary contracts.
contract PriceOracleV3 is ACLTrait, PriceFeedValidationTrait, SanityCheckTrait, IPriceOracleV3 {
using EnumerableSet for EnumerableSet.AddressSet;

Expand All @@ -49,9 +47,6 @@ contract PriceOracleV3 is ACLTrait, PriceFeedValidationTrait, SanityCheckTrait,
/// @dev Set of all tokens that have price feeds
EnumerableSet.AddressSet internal _tokensSet;

/// @dev Set of all updatable price feeds
EnumerableSet.AddressSet internal _updatablePriceFeedsSet;

/// @notice Constructor
/// @param _acl ACL contract address
constructor(address _acl) ACLTrait(_acl) {}
Expand Down Expand Up @@ -125,27 +120,6 @@ contract PriceOracleV3 is ACLTrait, PriceFeedValidationTrait, SanityCheckTrait,
return amount * price / scale;
}

// ------------- //
// PRICE UPDATES //
// ------------- //

/// @notice Returns all updatable price feeds
function getUpdatablePriceFeeds() external view override returns (address[] memory) {
return _updatablePriceFeedsSet.values();
}

/// @notice Applies on-demand price feed updates, see `PriceUpdate` for details
/// @custom:tests U:[PO-5]
function updatePrices(PriceUpdate[] calldata updates) external override {
unchecked {
uint256 len = updates.length;
for (uint256 i; i < len; ++i) {
if (!_updatablePriceFeedsSet.contains(updates[i].priceFeed)) revert PriceFeedIsNotUpdatableException();
IUpdatablePriceFeed(updates[i].priceFeed).updatePrice(updates[i].data);
}
}
}

// ------------- //
// CONFIGURATION //
// ------------- //
Expand Down Expand Up @@ -204,15 +178,6 @@ contract PriceOracleV3 is ACLTrait, PriceFeedValidationTrait, SanityCheckTrait,
emit SetReservePriceFeed(token, priceFeed, stalenessPeriod, skipCheck);
}

/// @notice Adds `priceFeed` to the set of updatable price feeds
/// @dev Price feed must be updatable but is not required to satisfy all validity conditions,
/// e.g., decimals need not to be equal to 8
/// @custom:tests U:[PO-5]
function addUpdatablePriceFeed(address priceFeed) external override nonZeroAddress(priceFeed) configuratorOnly {
if (!_isUpdatable(priceFeed)) revert PriceFeedIsNotUpdatableException();
if (_updatablePriceFeedsSet.add(priceFeed)) emit AddUpdatablePriceFeed(priceFeed);
}

// --------- //
// INTERNALS //
// --------- //
Expand Down
Loading
Loading