// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "../interfaces/ICollateralAdapter.sol"; import "../interfaces/ILedger.sol"; /** * @title CollateralAdapter * @notice Handles M0 collateral deposits and withdrawals * @dev Only callable by Vaults, only accepts approved assets */ contract CollateralAdapter is ICollateralAdapter, AccessControl, ReentrancyGuard { using SafeERC20 for IERC20; bytes32 public constant VAULT_ROLE = keccak256("VAULT_ROLE"); bytes32 public constant LIQUIDATOR_ROLE = keccak256("LIQUIDATOR_ROLE"); ILedger public ledger; mapping(address => bool) public approvedAssets; constructor(address admin, address ledger_) { _grantRole(DEFAULT_ADMIN_ROLE, admin); ledger = ILedger(ledger_); } /** * @notice Deposit M0 collateral * @param vault Vault address * @param asset Collateral asset address (address(0) for native ETH) * @param amount Amount to deposit */ function deposit(address vault, address asset, uint256 amount) external payable override nonReentrant onlyRole(VAULT_ROLE) { require(approvedAssets[asset] || asset == address(0), "CollateralAdapter: asset not approved"); require(amount > 0, "CollateralAdapter: zero amount"); if (asset == address(0)) { // Native ETH deposit require(msg.value == amount, "CollateralAdapter: value mismatch"); // ETH is held by this contract } else { // ERC20 token deposit require(msg.value == 0, "CollateralAdapter: unexpected ETH"); IERC20(asset).safeTransferFrom(msg.sender, address(this), amount); } // Update ledger ledger.modifyCollateral(vault, asset, int256(amount)); emit CollateralDeposited(vault, asset, amount); } /** * @notice Withdraw M0 collateral * @param vault Vault address * @param asset Collateral asset address * @param amount Amount to withdraw */ function withdraw(address vault, address asset, uint256 amount) external override nonReentrant onlyRole(VAULT_ROLE) { require(amount > 0, "CollateralAdapter: zero amount"); // Update ledger (will revert if insufficient collateral) ledger.modifyCollateral(vault, asset, -int256(amount)); if (asset == address(0)) { // Native ETH withdrawal (bool success, ) = payable(vault).call{value: amount}(""); require(success, "CollateralAdapter: ETH transfer failed"); } else { // ERC20 token withdrawal IERC20(asset).safeTransfer(vault, amount); } emit CollateralWithdrawn(vault, asset, amount); } /** * @notice Seize collateral during liquidation * @param vault Vault address * @param asset Collateral asset address * @param amount Amount to seize * @param liquidator Liquidator address */ function seize(address vault, address asset, uint256 amount, address liquidator) external override nonReentrant onlyRole(LIQUIDATOR_ROLE) { require(amount > 0, "CollateralAdapter: zero amount"); require(liquidator != address(0), "CollateralAdapter: zero liquidator"); // Update ledger ledger.modifyCollateral(vault, asset, -int256(amount)); if (asset == address(0)) { // Native ETH seizure (bool success, ) = payable(liquidator).call{value: amount}(""); require(success, "CollateralAdapter: ETH transfer failed"); } else { // ERC20 token seizure IERC20(asset).safeTransfer(liquidator, amount); } emit CollateralSeized(vault, asset, amount, liquidator); } /** * @notice Approve an asset for collateral * @param asset Asset address */ function approveAsset(address asset) external onlyRole(DEFAULT_ADMIN_ROLE) { approvedAssets[asset] = true; } /** * @notice Revoke asset approval * @param asset Asset address */ function revokeAsset(address asset) external onlyRole(DEFAULT_ADMIN_ROLE) { approvedAssets[asset] = false; } /** * @notice Set ledger address * @param ledger_ New ledger address */ function setLedger(address ledger_) external onlyRole(DEFAULT_ADMIN_ROLE) { require(ledger_ != address(0), "CollateralAdapter: zero address"); ledger = ILedger(ledger_); } // Allow contract to receive ETH receive() external payable {} }