// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./interfaces/IVault.sol"; import "./interfaces/ILedger.sol"; import "./interfaces/IRegulatedEntityRegistry.sol"; import "./interfaces/ICollateralAdapter.sol"; import "./interfaces/IeMoneyJoin.sol"; import "./tokens/DepositToken.sol"; import "./tokens/DebtToken.sol"; /** * @title Vault * @notice Aave-style vault for deposit, borrow, repay, withdraw operations * @dev Each vault is owned by a regulated entity */ contract Vault is IVault, AccessControl, ReentrancyGuard { using SafeERC20 for IERC20; bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); address public override owner; address public entity; // Regulated entity address ILedger public ledger; IRegulatedEntityRegistry public entityRegistry; ICollateralAdapter public collateralAdapter; IeMoneyJoin public eMoneyJoin; // Token mappings mapping(address => address) public depositTokens; // asset => DepositToken mapping(address => address) public debtTokens; // currency => DebtToken constructor( address owner_, address entity_, address ledger_, address entityRegistry_, address collateralAdapter_, address eMoneyJoin_ ) { owner = owner_; entity = entity_; ledger = ILedger(ledger_); entityRegistry = IRegulatedEntityRegistry(entityRegistry_); collateralAdapter = ICollateralAdapter(collateralAdapter_); eMoneyJoin = IeMoneyJoin(eMoneyJoin_); _grantRole(DEFAULT_ADMIN_ROLE, owner_); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); // Factory can set tokens during creation } /** * @notice Set deposit token for an asset * @param asset Asset address * @param depositToken Deposit token address */ function setDepositToken(address asset, address depositToken) external onlyRole(DEFAULT_ADMIN_ROLE) { depositTokens[asset] = depositToken; } /** * @notice Set debt token for a currency * @param currency Currency address * @param debtToken Debt token address */ function setDebtToken(address currency, address debtToken) external onlyRole(DEFAULT_ADMIN_ROLE) { debtTokens[currency] = debtToken; } /** * @notice Deposit M0 collateral into vault * @param asset Collateral asset address (address(0) for native ETH) * @param amount Amount to deposit */ function deposit(address asset, uint256 amount) external payable override nonReentrant { require(amount > 0, "Vault: zero amount"); require( entityRegistry.isEligible(entity) && (entityRegistry.isAuthorized(entity, msg.sender) || entityRegistry.isOperator(entity, msg.sender) || msg.sender == owner), "Vault: not authorized" ); if (asset == address(0)) { require(msg.value == amount, "Vault: value mismatch"); } else { require(msg.value == 0, "Vault: unexpected ETH"); IERC20(asset).safeTransferFrom(msg.sender, address(collateralAdapter), amount); } // Deposit via adapter collateralAdapter.deposit{value: asset == address(0) ? amount : 0}(address(this), asset, amount); // Mint deposit token address depositToken = depositTokens[asset]; if (depositToken != address(0)) { DepositToken(depositToken).mint(msg.sender, amount); } emit Deposited(asset, amount, msg.sender); } /** * @notice Borrow eMoney against collateral * @param currency eMoney currency address * @param amount Amount to borrow */ function borrow(address currency, uint256 amount) external override nonReentrant { require(amount > 0, "Vault: zero amount"); require( entityRegistry.isEligible(entity) && (entityRegistry.isAuthorized(entity, msg.sender) || entityRegistry.isOperator(entity, msg.sender) || msg.sender == owner), "Vault: not authorized" ); // Check if borrow is allowed (bool canBorrow, bytes32 reasonCode) = ledger.canBorrow(address(this), currency, amount); require(canBorrow, string(abi.encodePacked("Vault: borrow not allowed: ", reasonCode))); // Update debt in ledger ledger.modifyDebt(address(this), currency, int256(amount)); // Mint debt token address debtToken = debtTokens[currency]; if (debtToken != address(0)) { DebtToken(debtToken).mint(msg.sender, amount); } // Mint eMoney to borrower eMoneyJoin.mint(currency, msg.sender, amount); emit Borrowed(currency, amount, msg.sender); } /** * @notice Repay borrowed eMoney * @param currency eMoney currency address * @param amount Amount to repay */ function repay(address currency, uint256 amount) external override nonReentrant { require(amount > 0, "Vault: zero amount"); uint256 currentDebt = ledger.debt(address(this), currency); require(currentDebt > 0, "Vault: no debt"); // Burn eMoney from repayer eMoneyJoin.burn(currency, msg.sender, amount); // Update debt in ledger uint256 repayAmount = amount > currentDebt ? currentDebt : amount; ledger.modifyDebt(address(this), currency, -int256(repayAmount)); // Burn debt token address debtToken = debtTokens[currency]; if (debtToken != address(0)) { uint256 debtTokenBalance = DebtToken(debtToken).balanceOf(msg.sender); uint256 burnAmount = repayAmount > debtTokenBalance ? debtTokenBalance : repayAmount; DebtToken(debtToken).burn(msg.sender, burnAmount); } emit Repaid(currency, repayAmount, msg.sender); } /** * @notice Withdraw collateral from vault * @param asset Collateral asset address * @param amount Amount to withdraw */ function withdraw(address asset, uint256 amount) external override nonReentrant { require(amount > 0, "Vault: zero amount"); require( entityRegistry.isEligible(entity) && (entityRegistry.isAuthorized(entity, msg.sender) || entityRegistry.isOperator(entity, msg.sender) || msg.sender == owner), "Vault: not authorized" ); // Check vault health after withdrawal uint256 collateralBalance = ledger.collateral(address(this), asset); require(collateralBalance >= amount, "Vault: insufficient collateral"); // Check if withdrawal would make vault unsafe // Simplified check - in production would calculate health after withdrawal (uint256 healthRatio, , ) = ledger.getVaultHealth(address(this)); require(healthRatio >= 11000, "Vault: withdrawal would make vault unsafe"); // 110% minimum // Burn deposit token address depositToken = depositTokens[asset]; if (depositToken != address(0)) { uint256 depositTokenBalance = DepositToken(depositToken).balanceOf(msg.sender); require(depositTokenBalance >= amount, "Vault: insufficient deposit tokens"); DepositToken(depositToken).burn(msg.sender, amount); } // Withdraw via adapter collateralAdapter.withdraw(address(this), asset, amount); emit Withdrawn(asset, amount, msg.sender); } /** * @notice Get vault health * @return healthRatio Collateralization ratio in basis points * @return collateralValue Total collateral value in XAU * @return debtValue Total debt value in XAU */ function getHealth() external view override returns ( uint256 healthRatio, uint256 collateralValue, uint256 debtValue ) { return ledger.getVaultHealth(address(this)); } /// @notice Accept ETH from CollateralAdapter withdraw receive() external payable {} }