Euler Vault Integration
This guide explains how to integrate Cozy Safety Module into a Euler Vault as a bad debt backstop. Use it as a reference when deploying, wiring, or operating the integration. Euler vaults can atomically repay bad debt post-liquidation using eTokens tapped from a Safety Module. This provides an alternative mechanism to managing bad debt with bad debt socialization (the default option).
Why Atomic Bad Debt Repayment Matters
State mismatch between liquidation and debt repayment events introduces edge cases that are difficult to track/prevent. Here are 2 examples of how things can get tricky:
Inside eVault.liquidate, Euler explicitly verifies that the “EVC account status checks deferred” flag is turned off for the violator. That guard stops users from going underwater in a checks-deferred context and calling liquidate. If you decouple bad-debt repayment from the liquidation, you must repeat that check during repayment. Otherwise, anyone can borrow, mimic bad debt while checks are deferred, trigger the safety module, and have their own debt repaid. They effectively walk away with borrowed assets at the expense of safety module stakers.
Another DOS vector arises in the window between an account becoming bad debt (non-zero debt, zero collateral) and the eventual trigger or repayment. A griefer can transfer a single wei of collateral to that account so it no longer qualifies as bad debt, blocking the trigger. A safety module staker might do this to avoid being tapped, letting them earn risk-free yield for only the gas cost of the griefing transaction.
contract EulerTrancheRaiseStrategy is IRaiseStrategy {
/// @notice Converts asset needs to safety module specific raises using a tranching strategy which prioritizes
/// raising the fee share reserve pool prior to the lender share reserve pool.
/// @param originSafetyModule_ The safety module that triggered the raise
/// @param assetNeeds_ The asset needs to be converted to raises
/// @param data_ Encoded reserve pool ids: abi.encode(uint8 feeShareReservePoolId, uint8 lenderShareReservePoolId)
function calculateRaise(ISafetyModule originSafetyModule_, AssetNeed[] memory assetNeeds_, bytes calldata data_)
external
returns (SafetyModuleRaise[] memory);
}
contract CozyLiquidatorManager is Ownable, ICozyLiquidatorManager
/// @notice Deploys a new CozyLiquidator with the provided parameters.
/// @param safetyModule_ The associated SafetyModule.
/// @param eVault_ The associated Euler vault.
/// @param raiseStrategy_ The associated raise strategy.
/// @param feeShareReservePoolId_ The reserve pool ID to use for the fee share.
/// @param lenderShareReservePoolId_ The reserve pool ID to use for the lender share.
/// @param salt_ Used to compute the resulting address of the CozyLiquidator along with `msg.sender`.
/// @return cozyLiquidator_ The newly created CozyLiquidator.
function createCozyLiquidator(
ISafetyModule safetyModule_,
IEVault eVault_,
EulerTrancheRaiseStrategy raiseStrategy_,
uint8 feeShareReservePoolId_,
uint8 lenderShareReservePoolId_,
bytes32 salt_
) external returns (ICozyLiquidator cozyLiquidator_)
}
contract FeeReceiver is Ownable {
/**
* @notice Constructor that sets all parameters for the fee receiver
* @param asset_ Euler vault token that this fee receiver manages
* @param safetyModule_ The SafetyModule to which fees are redirected
* @param safetyModuleReservePoolId_ The reserve pool ID in the safety module for fee shares
* @param safetyModuleShare_ Percentage of fees that should be redirected to the safety module represented as a ZOC
* (e.g. 5000 = 50%)
* @param owner_ Address of the owner who can claim any undirected fees
*/
constructor(
IERC20 asset_,
ISafetyModule safetyModule_,
uint8 safetyModuleReservePoolId_,
uint256 safetyModuleShare_,
address owner_
)
Components
CozyLiquidatorManager.createCozyLiquidator
deploys minimal proxy liquidators that are parameterized for a specific safety module + vault.CozyLiquidator
is the controller that Euler vaults call into.CozyLiquidationHandler.handleCozyLiquidation
is delegate called by the liquidator to execute the liquidation, and trigger + raise the Safety Module.EulerTrancheRaiseStrategy
is a raise strategy which specifies that eTokens deposited by the vault governor (via the FeeReceiver) get tapped prior to eTokens deposited by lenders.FeeReceiver
redirects a configurable portion of vault fees into the safety module’s fee share reserve pool.
Deployment & Configuration
Configure the safety module
Define reserve pools for the Euler vault eToken. Pool
0
for fee shares and pool1
for lender shares (both backed by the same eToken).Include
ControllerConfig({controller: ISafetyModuleController(cozyLiquidatorAddress), exists: true})
inside theConfigUpdateCalldataParams
supplied to a queued config update. This registers the liquidator as a controller viaCozySafetyModuleManager.registerSafetyModuleController
.
Wire the Euler vault
Vault governance must set the liquidator as the
liquidate
hook target (IEVault.setHookConfig
) and flipCFG_DONT_SOCIALIZE_DEBT
so Euler does not spread bad debt across depositors.eVault.feeReceiver()
should be set to theFeeReceiver
contract soFeeReceiver
receives a portion of the governor's fees.
Liquidation & Raise Lifecycle
Handler executes Euler liquidation
The handler executes the real Euler liquidation via the EVC
, emitting LiquidationExecuted
. Since CozyLiquidator is set as the pre-hook on the vault, the eVault immediately calls back into CozyLiquidator.liquidate()
, which returns a no-op and continues on to eVault.liquidate()
If residual debt remains, trigger the safety module
If collateral is insufficient and residual debt remains:
A deterministic
triggerEventId
is computed from the violator, remaining debt, timestamp, andtriggerEventIdNonce
.The handler calls
SafetyModule.trigger(triggerEventId, validityDuration)
which moves the module intoTRIGGERED
state and incrementsnumPendingRaises
.An
AssetNeed
for the Euler eToken shares is created by converting debt assets into eVault share amount.SafetyModule.requestRaise
is invoked with theEulerTrancheRaiseStrategy
and pool IDs encoded indata
.
Observability
/// SafetyModule
/// @dev Emitted when the SafetyModule is triggered.
event Triggered(ISafetyModuleController indexed controller_, bytes32 indexed triggerEventId_, uint256 expiresAt_);
event ReservePoolTapped(
ISafetyModuleController indexed safetyModuleController_,
bytes32 triggerEventId_,
address indexed receiver_,
uint8 indexed reservePoolId_,
uint256 assetAmount_
);
/// @dev Emitted when a safety module is tapped.
event SafetyModuleTapped(
ISafetyModuleController indexed safetyModuleController_, bytes32 indexed triggerEventId_, address indexed receiver_
);
/// @dev Emitted when the SafetyModule is requested to raise.
event RaiseRequested(
ISafetyModuleController indexed controller_,
bytes32 indexed triggerEventId_,
address indexed receiver_,
AssetNeed[] assetNeeds_,
IRaiseStrategy raiseStrategy_,
bytes data_
);
/// CozyLiquidator
/// @notice Emitted on trigger event state snapshot.
event TriggerEventStateSnapshot(bytes32 indexed triggerEventId_, bytes triggerEventStateSnapshot_);
/// CozyLiquidationHandler
/// @notice Emitted when a liquidation is executed.
event LiquidationExecuted(
address liquidator_, address violator_, address collateral_, uint256 repayAssets_, uint256 minYieldBalance_
);
/// @notice Emitted when bad debt is repaid.
event BadDebtRepaid(bytes32 indexed triggerEventId_, uint256 badDebtAmount_, uint256 badDebtRepaid_);
Last updated