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

1

Configure the safety module

  • Define reserve pools for the Euler vault eToken. Pool 0 for fee shares and pool 1 for lender shares (both backed by the same eToken).

  • Include ControllerConfig({controller: ISafetyModuleController(cozyLiquidatorAddress), exists: true}) inside the ConfigUpdateCalldataParams supplied to a queued config update. This registers the liquidator as a controller via CozySafetyModuleManager.registerSafetyModuleController.

2

Deploy the liquidator for the target vault

Call CozyLiquidatorManager.createCozyLiquidator with:

  • The configured safety module.

  • The Euler vault address (eToken).

  • The EulerTrancheRaiseStrategy instance.

  • Fee-share and lender-share reserve pool IDs.

  • A deploy salt (for deterministic addresses if desired).

3

Wire the Euler vault

  • Vault governance must set the liquidator as the liquidate hook target (IEVault.setHookConfig) and flip CFG_DONT_SOCIALIZE_DEBT so Euler does not spread bad debt across depositors.

  • eVault.feeReceiver() should be set to the FeeReceiver contract so FeeReceiver receives a portion of the governor's fees.

4

(Optional) Divert vault fees to the safety module

  • Deploy a FeeReceiver pointing at the eToken, safety module, and fee reserve pool ID. Set it as the vault’s fee receiver and choose a safetyModuleShare (ZOC).

  • Anyone can call redirectFees() to push accumulated eTokens into the fee reserve pool.

Liquidation & Raise Lifecycle

1

Trigger liquidation through the liquidator

CozyLiquidator.liquidate(violator, collateral, repayAssets, minYield) is called instead of eVault.liquidate.

Note: You must use the CozyLiquidator as the entrypoint, otherwise CozyLiquidator will revert with LiquidationNotRoutedThroughCozyLiquidator()

2

Liquidator delegates to handler

CozyLiquidator locks re-entrancy (only the vault can re-enter) and delegate-calls CozyLiquidationHandler.handleCozyLiquidation.

3

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()

4

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, and triggerEventIdNonce.

    • The handler calls SafetyModule.trigger(triggerEventId, validityDuration) which moves the module into TRIGGERED state and increments numPendingRaises.

    • An AssetNeed for the Euler eToken shares is created by converting debt assets into eVault share amount.

    • SafetyModule.requestRaise is invoked with the EulerTrancheRaiseStrategy and pool IDs encoded in data.

5

Safety module processes the raise

  • Uses the raise strategy to split the need into concrete raise instructions.

  • Transfers the tapped eTokens to the liquidator, and decrements numPendingRaises

6

Handler repays the vault

Back in the handler, any eTokens received are sent to EVault.repayWithShares, cancelling the bad debt. BadDebtRepaid captures the repayment amount.

7

Liquidator stores snapshot and increments nonce

Control returns to CozyLiquidator, which stores and emits a TriggerEventStateSnapshot (violator, bad debt amount, amount repaid, timestamp) and increments triggerEventIdNonce.

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