// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "./Vault.sol"; import "./tokens/DepositToken.sol"; import "./tokens/DebtToken.sol"; import "./interfaces/ILedger.sol"; /** * @title VaultFactory * @notice Factory for creating vault instances with associated tokens * @dev Creates Vault, DepositToken, and DebtToken instances */ contract VaultFactory is AccessControl { bytes32 public constant VAULT_DEPLOYER_ROLE = keccak256("VAULT_DEPLOYER_ROLE"); address public immutable vaultImplementation; address public immutable depositTokenImplementation; address public immutable debtTokenImplementation; ILedger public ledger; address public entityRegistry; address public collateralAdapter; address public eMoneyJoin; mapping(address => address[]) public vaultsByEntity; // entity => vaults[] mapping(address => address) public vaultToEntity; // vault => entity event VaultCreated( address indexed vault, address indexed entity, address indexed owner, address depositToken, address debtToken ); constructor( address admin, address vaultImplementation_, address depositTokenImplementation_, address debtTokenImplementation_, address ledger_, address entityRegistry_, address collateralAdapter_, address eMoneyJoin_ ) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(VAULT_DEPLOYER_ROLE, admin); vaultImplementation = vaultImplementation_; depositTokenImplementation = depositTokenImplementation_; debtTokenImplementation = debtTokenImplementation_; ledger = ILedger(ledger_); entityRegistry = entityRegistry_; collateralAdapter = collateralAdapter_; eMoneyJoin = eMoneyJoin_; } /** * @notice Create a new vault for a regulated entity * @param owner Vault owner address * @param entity Regulated entity address * @param asset Collateral asset address (for deposit token) * @param currency eMoney currency address (for debt token) * @return vault Address of created vault * @return depositToken Address of deposit token * @return debtToken Address of debt token */ function createVault( address owner, address entity, address asset, address currency ) external onlyRole(VAULT_DEPLOYER_ROLE) returns ( address vault, address depositToken, address debtToken ) { require(owner != address(0), "VaultFactory: zero owner"); require(entity != address(0), "VaultFactory: zero entity"); // Deploy vault directly (not using proxy for simplicity) // In production, could use proxy pattern for upgradeability Vault vaultContract = new Vault( owner, entity, address(ledger), entityRegistry, collateralAdapter, eMoneyJoin ); vault = address(vaultContract); // Deploy deposit token (factory as admin so it can grant MINTER/BURNER to vault) bytes memory depositTokenInitData = abi.encodeWithSelector( DepositToken.initialize.selector, string(abi.encodePacked("Deposit ", _getAssetSymbol(asset))), string(abi.encodePacked("d", _getAssetSymbol(asset))), vault, asset, address(this) ); ERC1967Proxy depositTokenProxy = new ERC1967Proxy(depositTokenImplementation, depositTokenInitData); depositToken = address(depositTokenProxy); // Grant minter/burner roles to vault DepositToken(depositToken).grantRole(keccak256("MINTER_ROLE"), vault); DepositToken(depositToken).grantRole(keccak256("BURNER_ROLE"), vault); // Deploy debt token (factory as admin so it can grant MINTER/BURNER to vault) bytes memory debtTokenInitData = abi.encodeWithSelector( DebtToken.initialize.selector, string(abi.encodePacked("Debt ", _getCurrencySymbol(currency))), string(abi.encodePacked("debt", _getCurrencySymbol(currency))), vault, currency, address(this) ); ERC1967Proxy debtTokenProxy = new ERC1967Proxy(debtTokenImplementation, debtTokenInitData); debtToken = address(debtTokenProxy); // Grant minter/burner roles to vault DebtToken(debtToken).grantRole(keccak256("MINTER_ROLE"), vault); DebtToken(debtToken).grantRole(keccak256("BURNER_ROLE"), vault); // Configure vault with tokens (cast via payable since Vault has receive()) Vault(payable(vault)).setDepositToken(asset, depositToken); Vault(payable(vault)).setDebtToken(currency, debtToken); // Grant vault role in ledger ledger.grantVaultRole(vault); // Track vault vaultsByEntity[entity].push(vault); vaultToEntity[vault] = entity; emit VaultCreated(vault, entity, owner, depositToken, debtToken); } /** * @notice Create a vault with explicit decimals and debt transferability (DEX-ready) * @param depositDecimals Deposit token decimals (e.g. 6 for stablecoins; 0 = 18) * @param debtDecimals Debt token decimals (0 = 18) * @param debtTransferable If true, debt token is freely transferable (DEX-ready) */ function createVaultWithDecimals( address owner, address entity, address asset, address currency, uint8 depositDecimals, uint8 debtDecimals, bool debtTransferable ) external onlyRole(VAULT_DEPLOYER_ROLE) returns ( address vault, address depositToken, address debtToken ) { require(owner != address(0), "VaultFactory: zero owner"); require(entity != address(0), "VaultFactory: zero entity"); Vault vaultContract = new Vault( owner, entity, address(ledger), entityRegistry, collateralAdapter, eMoneyJoin ); vault = address(vaultContract); uint8 dDec = depositDecimals > 0 ? depositDecimals : 18; bytes memory depositTokenInitData = abi.encodeWithSelector( DepositToken.initializeWithDecimals.selector, string(abi.encodePacked("Deposit ", _getAssetSymbol(asset))), string(abi.encodePacked("d", _getAssetSymbol(asset))), vault, asset, address(this), dDec ); ERC1967Proxy depositTokenProxy = new ERC1967Proxy(depositTokenImplementation, depositTokenInitData); depositToken = address(depositTokenProxy); DepositToken(depositToken).grantRole(keccak256("MINTER_ROLE"), vault); DepositToken(depositToken).grantRole(keccak256("BURNER_ROLE"), vault); uint8 debtDec = debtDecimals > 0 ? debtDecimals : 18; bytes memory debtTokenInitData = abi.encodeWithSelector( DebtToken.initializeFull.selector, string(abi.encodePacked("Debt ", _getCurrencySymbol(currency))), string(abi.encodePacked("debt", _getCurrencySymbol(currency))), vault, currency, address(this), debtDec, debtTransferable ); ERC1967Proxy debtTokenProxy = new ERC1967Proxy(debtTokenImplementation, debtTokenInitData); debtToken = address(debtTokenProxy); DebtToken(debtToken).grantRole(keccak256("MINTER_ROLE"), vault); DebtToken(debtToken).grantRole(keccak256("BURNER_ROLE"), vault); Vault(payable(vault)).setDepositToken(asset, depositToken); Vault(payable(vault)).setDebtToken(currency, debtToken); ledger.grantVaultRole(vault); vaultsByEntity[entity].push(vault); vaultToEntity[vault] = entity; emit VaultCreated(vault, entity, owner, depositToken, debtToken); } /** * @notice Get asset symbol (helper) * @param asset Asset address * @return symbol Asset symbol */ function _getAssetSymbol(address asset) internal pure returns (string memory symbol) { if (asset == address(0)) { return "ETH"; } // In production, would fetch from ERC20 return "ASSET"; } /** * @notice Get currency symbol (helper) * @param currency Currency address * @return symbol Currency symbol */ function _getCurrencySymbol(address currency) internal pure returns (string memory symbol) { // In production, would fetch from eMoney token return "CURRENCY"; } /** * @notice Get vaults for an entity * @param entity Entity address * @return vaults Array of vault addresses */ function getVaultsByEntity(address entity) external view returns (address[] memory vaults) { return vaultsByEntity[entity]; } }