// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "./interfaces/ILiquidation.sol"; import "./interfaces/ILedger.sol"; import "./interfaces/ICollateralAdapter.sol"; import "./interfaces/IeMoneyJoin.sol"; /** * @title Liquidation * @notice Handles liquidation of undercollateralized vaults * @dev Permissioned liquidators only, no public auctions */ contract Liquidation is ILiquidation, AccessControl, ReentrancyGuard { bytes32 public constant LIQUIDATOR_ROLE = keccak256("LIQUIDATOR_ROLE"); ILedger public ledger; ICollateralAdapter public collateralAdapter; IeMoneyJoin public eMoneyJoin; uint256 public constant BASIS_POINTS = 10000; uint256 public override liquidationBonus = 500; // 5% bonus in basis points constructor( address admin, address ledger_, address collateralAdapter_, address eMoneyJoin_ ) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(LIQUIDATOR_ROLE, admin); ledger = ILedger(ledger_); collateralAdapter = ICollateralAdapter(collateralAdapter_); eMoneyJoin = IeMoneyJoin(eMoneyJoin_); } /** * @notice Liquidate an undercollateralized vault * @param vault Vault address to liquidate * @param currency Debt currency address * @param maxDebt Maximum debt to liquidate * @return seizedCollateral Amount of collateral seized * @return repaidDebt Amount of debt repaid */ function liquidate( address vault, address currency, uint256 maxDebt ) external override nonReentrant onlyRole(LIQUIDATOR_ROLE) returns (uint256 seizedCollateral, uint256 repaidDebt) { (bool liquidatable, ) = this.canLiquidate(vault); require(liquidatable, "Liquidation: vault not liquidatable"); // Get current debt uint256 currentDebt = ledger.debt(vault, currency); require(currentDebt > 0, "Liquidation: no debt"); // Calculate debt to repay (min of maxDebt and currentDebt) repaidDebt = maxDebt < currentDebt ? maxDebt : currentDebt; // Get liquidation ratio for the currency uint256 liqRatio = ledger.liquidationRatio(currency); // Calculate collateral value needed: repaidDebt * (1 + bonus) * liquidationRatio / 10000 // In XAU terms uint256 collateralValueNeeded = (repaidDebt * (BASIS_POINTS + liquidationBonus) * liqRatio) / (BASIS_POINTS * BASIS_POINTS); // For simplicity, assume ETH collateral (address(0)) // In production, would determine which collateral to seize address collateralAsset = address(0); uint256 collateralBalance = ledger.collateral(vault, collateralAsset); // Calculate how much collateral to seize based on ETH/XAU price // This is simplified - in production would use oracle seizedCollateral = collateralValueNeeded; // Simplified: assume 1:1 for now // Ensure we don't seize more than available if (seizedCollateral > collateralBalance) { seizedCollateral = collateralBalance; // Recalculate repaidDebt based on actual seized collateral repaidDebt = (seizedCollateral * BASIS_POINTS * BASIS_POINTS) / ((BASIS_POINTS + liquidationBonus) * liqRatio); } // Burn eMoney from liquidator (they provide eMoney to repay debt) eMoneyJoin.burn(currency, msg.sender, repaidDebt); // Update debt ledger.modifyDebt(vault, currency, -int256(repaidDebt)); // Seize collateral collateralAdapter.seize(vault, collateralAsset, seizedCollateral, msg.sender); emit VaultLiquidated(vault, currency, seizedCollateral, repaidDebt, msg.sender); } /** * @notice Check if a vault can be liquidated * @param vault Vault address * @return liquidatable True if vault can be liquidated * @return healthRatio Current health ratio in basis points */ function canLiquidate(address vault) external view override returns (bool liquidatable, uint256 healthRatio) { (healthRatio, , ) = ledger.getVaultHealth(vault); // Vault is liquidatable if health ratio is below liquidation ratio // Need to check for each currency - simplified here // In production, would check all currencies // For now, assume vault is liquidatable if health < 110% (liquidation ratio + buffer) liquidatable = healthRatio < 11000; // 110% in basis points } /** * @notice Set liquidation bonus * @param bonus New liquidation bonus in basis points */ function setLiquidationBonus(uint256 bonus) external onlyRole(DEFAULT_ADMIN_ROLE) { require(bonus <= 1000, "Liquidation: bonus too high"); // Max 10% liquidationBonus = bonus; } /** * @notice Set ledger address * @param ledger_ New ledger address */ function setLedger(address ledger_) external onlyRole(DEFAULT_ADMIN_ROLE) { require(ledger_ != address(0), "Liquidation: zero address"); ledger = ILedger(ledger_); } /** * @notice Set collateral adapter address * @param collateralAdapter_ New adapter address */ function setCollateralAdapter(address collateralAdapter_) external onlyRole(DEFAULT_ADMIN_ROLE) { require(collateralAdapter_ != address(0), "Liquidation: zero address"); collateralAdapter = ICollateralAdapter(collateralAdapter_); } /** * @notice Set eMoney join address * @param eMoneyJoin_ New join address */ function setEMoneyJoin(address eMoneyJoin_) external onlyRole(DEFAULT_ADMIN_ROLE) { require(eMoneyJoin_ != address(0), "Liquidation: zero address"); eMoneyJoin = IeMoneyJoin(eMoneyJoin_); } }