Skip to content
Closed
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
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ dynamic_test_linking = true
additional_compiler_profiles = [
{ name = "hub", optimizer = true, via_ir = true, optimizer_runs = 22_300 },
{ name = "spoke", optimizer = true, via_ir = true, optimizer_runs = 750 },
{ name = "spoke-small", optimizer = true, via_ir = true, optimizer_runs = 200 },
]

compilation_restrictions = [
{ paths = "src/hub/instances/HubInstance.sol", via_ir = true, optimizer_runs = 22_300 },
{ paths = "src/spoke/instances/SpokeInstance.sol", via_ir = true, optimizer_runs = 750 },
{ paths = "src/spoke/instances/BabylonSpokeInstance.sol", via_ir = true, optimizer_runs = 200 },
]

[bind_json]
Expand Down
104 changes: 104 additions & 0 deletions src/spoke/BabylonSpoke.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: LicenseRef-BUSL
pragma solidity 0.8.28;

import {LiquidationLogic} from 'src/spoke/libraries/LiquidationLogic.sol';
import {BabylonLiquidationLogic} from 'src/spoke/libraries/BabylonLiquidationLogic.sol';
import {IBabylonSpoke} from 'src/spoke/interfaces/IBabylonSpoke.sol';
import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol';
import {Spoke} from 'src/spoke/Spoke.sol';

/// @title BabylonSpoke
/// @author Aave Labs
/// @notice Spoke variant for the Babylon integration: liquidations are bounded by a collateral cap,
/// with per-reserve flags to bypass dust protection and target health factor sizing.
abstract contract BabylonSpoke is IBabylonSpoke, Spoke {
/// @dev Map of reserve identifiers to their liquidation bypass flags.
mapping(uint256 reserveId => LiquidationBypass) internal _liquidationBypass;

/// @inheritdoc IBabylonSpoke
function updateLiquidationBypass(
uint256 reserveId,
LiquidationBypass calldata bypass
) external restricted {
require(reserveId < _reserveCount, ReserveNotListed());
_liquidationBypass[reserveId] = bypass;
emit UpdateLiquidationBypass(reserveId, bypass);
}

/// @dev The canonical liquidation entry point is disabled on this Spoke: liquidations go
/// through the cap-bounded `liquidationCall` overload.
function liquidationCall(
uint256,
uint256,
address,
uint256,
bool
) external pure override(ISpoke, Spoke) {
revert UnsupportedLiquidationCall();
}

/// @inheritdoc IBabylonSpoke
function liquidationCall(
uint256 collateralReserveId,
uint256 debtReserveId,
address user,
uint256 debtToCover,
uint256 maxCollateralToRemove,
bool receiveShares
) external nonReentrant {
// dust protection and target health factor sizing are bypassed if either reserve has the corresponding flag set
LiquidationBypass storage collateralBypass = _liquidationBypass[collateralReserveId];
LiquidationBypass storage debtBypass = _liquidationBypass[debtReserveId];

UserAccountData memory userAccountData = _calculateUserAccountData(user);
BabylonLiquidationLogic.LiquidateUserParams memory params = BabylonLiquidationLogic
.LiquidateUserParams({
collateralReserveId: collateralReserveId,
debtReserveId: debtReserveId,
liquidationConfig: _liquidationConfig,
oracle: ORACLE,
user: user,
debtToCover: debtToCover,
overrides: BabylonLiquidationLogic.LiquidationOverrides({
maxCollateralToRemove: maxCollateralToRemove,
dustThreshold: collateralBypass.bypassLiquidationDust || debtBypass.bypassLiquidationDust
? 0
: DUST_LIQUIDATION_THRESHOLD,
bypassTargetHealthFactor: collateralBypass.bypassTargetHealthFactor ||
debtBypass.bypassTargetHealthFactor
}),
userAccountData: userAccountData,
liquidator: msg.sender,
receiveShares: receiveShares
});

bool isUserInDeficit = BabylonLiquidationLogic.liquidateUser({
reserves: _reserves,
userPositions: _userPositions,
positionStatus: _positionStatus,
dynamicConfig: _dynamicConfig,
params: params
});

if (isUserInDeficit) {
// report deficit for all debt reserves, including the reserve being repaid
LiquidationLogic.notifyReportDeficit(
_reserves,
_userPositions,
_positionStatus,
_reserveCount,
user
);
} else {
uint256 newRiskPremium = _calculateUserAccountData(user).riskPremium;
_notifyRiskPremiumUpdate(user, newRiskPremium);
}
}

/// @inheritdoc IBabylonSpoke
function getLiquidationBypass(
uint256 reserveId
) external view returns (LiquidationBypass memory) {
return _liquidationBypass[reserveId];
}
}
2 changes: 1 addition & 1 deletion src/spoke/Spoke.sol
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ abstract contract Spoke is
address user,
uint256 debtToCover,
bool receiveShares
) external nonReentrant {
) external virtual nonReentrant {
UserAccountData memory userAccountData = _calculateUserAccountData(user);
LiquidationLogic.LiquidateUserParams memory params = LiquidationLogic.LiquidateUserParams({
collateralReserveId: collateralReserveId,
Expand Down
34 changes: 34 additions & 0 deletions src/spoke/instances/BabylonSpokeInstance.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: LicenseRef-BUSL
pragma solidity 0.8.28;

import {BabylonSpoke} from 'src/spoke/BabylonSpoke.sol';
import {Spoke} from 'src/spoke/Spoke.sol';

/// @title BabylonSpokeInstance
/// @author Aave Labs
/// @notice Implementation contract for the BabylonSpoke.
contract BabylonSpokeInstance is BabylonSpoke {
uint64 public constant SPOKE_REVISION = 1;

/// @dev Constructor.
/// @dev During upgrade, must ensure that the new oracle is supporting existing assets on the Spoke and the replaced oracle.
/// @param oracle_ The address of the oracle.
/// @param maxUserReservesLimit_ The maximum number of collateral and borrow reserves a user can have.
constructor(address oracle_, uint16 maxUserReservesLimit_) Spoke(oracle_, maxUserReservesLimit_) {
_disableInitializers();
}

/// @notice Initializer.
/// @dev The authority contract must implement the `AccessManaged` interface for access control.
/// @param authority The address of the authority contract which manages permissions.
function initialize(address authority) external override reinitializer(SPOKE_REVISION) {
emit SetSpokeImmutables(ORACLE, MAX_USER_RESERVES_LIMIT);

require(authority != address(0), InvalidAddress());
__AccessManaged_init(authority);
if (_liquidationConfig.targetHealthFactor == 0) {
_liquidationConfig.targetHealthFactor = HEALTH_FACTOR_LIQUIDATION_THRESHOLD;
emit UpdateLiquidationConfig(_liquidationConfig);
}
}
}
57 changes: 57 additions & 0 deletions src/spoke/interfaces/IBabylonSpoke.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: LicenseRef-BUSL
pragma solidity ^0.8.0;

import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol';

/// @title IBabylonSpoke
/// @author Aave Labs
/// @notice Full interface for the BabylonSpoke.
interface IBabylonSpoke is ISpoke {
/// @notice Per-reserve liquidation bypass flags.
/// @dev bypassLiquidationDust True if the liquidation dust protection is bypassed for liquidations involving the reserve.
/// @dev bypassTargetHealthFactor True if liquidations involving the reserve are not sized by the target health factor.
struct LiquidationBypass {
bool bypassLiquidationDust;
bool bypassTargetHealthFactor;
}

/// @notice Emitted when the liquidation bypass flags of a reserve are updated.
/// @param reserveId The identifier of the reserve.
/// @param bypass The new liquidation bypass flags.
event UpdateLiquidationBypass(uint256 indexed reserveId, LiquidationBypass bypass);

/// @notice Thrown when the disabled canonical liquidation entry point is called.
error UnsupportedLiquidationCall();

/// @notice Updates the liquidation bypass flags of a reserve.
/// @param reserveId The identifier of the reserve.
/// @param bypass The new liquidation bypass flags.
function updateLiquidationBypass(uint256 reserveId, LiquidationBypass calldata bypass) external;

/// @notice Liquidates a user position with a cap on the total collateral removed.
/// @dev It reverts if the reserves associated with any of the given reserve identifiers are not listed.
/// @dev The Spoke pulls underlying repaid debt assets from caller (Liquidator), hence it needs prior approval.
/// @dev The total collateral removed is capped at `maxCollateralToRemove`; when the cap binds, the repaid
/// debt is resized to exactly consume it, and the remaining collateral and debt must respect the dust threshold.
/// @dev Dust protection and target health factor sizing are bypassed if the corresponding bypass flag is set
/// on either the collateral or the debt reserve.
/// @param collateralReserveId The reserveId of the underlying asset used as collateral by the liquidated user.
/// @param debtReserveId The reserveId of the underlying asset borrowed by the liquidated user, to be repaid by Liquidator.
/// @param user The address of the user to liquidate.
/// @param debtToCover The desired amount of debt to cover.
/// @param maxCollateralToRemove The maximum total amount of collateral to remove from the user, expressed in asset units. Use `type(uint256).max` for no cap.
/// @param receiveShares True to receive collateral in supplied shares, false to receive in underlying assets.
function liquidationCall(
uint256 collateralReserveId,
uint256 debtReserveId,
address user,
uint256 debtToCover,
uint256 maxCollateralToRemove,
bool receiveShares
) external;

/// @notice Returns the liquidation bypass flags of a reserve.
/// @param reserveId The identifier of the reserve.
/// @return The liquidation bypass flags.
function getLiquidationBypass(uint256 reserveId) external view returns (LiquidationBypass memory);
}
Loading
Loading