// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.21; import { Owned } from "solmate/src/auth/Owned.sol"; import { ERC721 } from "./ERC721/ERC721.sol"; import { ErrorTypes } from "../errorTypes.sol"; import { StorageRead } from "../../../libraries/storageRead.sol"; abstract contract VaultFactoryVariables is Owned, ERC721, StorageRead { /// @dev ERC721 tokens name string internal constant ERC721_NAME = "Fluid Vault"; /// @dev ERC721 tokens symbol string internal constant ERC721_SYMBOL = "fVLT"; /*////////////////////////////////////////////////////////////// STORAGE VARIABLES //////////////////////////////////////////////////////////////*/ // ------------ storage variables from inherited contracts (Owned and ERC721) come before vars here -------- // ----------------------- slot 0 --------------------------- // address public owner; // from Owned // 12 bytes empty // ----------------------- slot 1 --------------------------- // string public name; // ----------------------- slot 2 --------------------------- // string public symbol; // ----------------------- slot 3 --------------------------- // mapping(uint256 => uint256) internal _tokenConfig; // ----------------------- slot 4 --------------------------- // mapping(address => mapping(uint256 => uint256)) internal _ownerConfig; // ----------------------- slot 5 --------------------------- // uint256 public totalSupply; // ----------------------- slot 6 --------------------------- // mapping(uint256 => address) public getApproved; // ----------------------- slot 7 --------------------------- // mapping(address => mapping(address => bool)) public isApprovedForAll; // ----------------------- slot 8 --------------------------- /// @dev deployer can deploy new Vault contract /// owner can add/remove deployer. /// Owner is deployer by default. mapping(address => bool) internal _deployers; // ----------------------- slot 9 --------------------------- /// @dev global auths can update any vault config. /// owner can add/remove global auths. /// Owner is global auth by default. mapping(address => bool) internal _globalAuths; // ----------------------- slot 10 --------------------------- /// @dev vault auths can update specific vault config. /// owner can add/remove vault auths. /// Owner is vault auth by default. /// vault => auth => add/remove mapping(address => mapping(address => bool)) internal _vaultAuths; // ----------------------- slot 11 --------------------------- /// @dev total no of vaults deployed by the factory /// only addresses that have deployer role or owner can deploy new vault. uint256 internal _totalVaults; // ----------------------- slot 12 --------------------------- /// @dev vault deployment logics for deploying vault /// These logic contracts hold the deployment logics of specific vaults and are called via .delegatecall inside deployVault(). /// only addresses that have owner can add/remove new vault deployment logic. mapping(address => bool) internal _vaultDeploymentLogics; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ constructor(address owner_) Owned(owner_) ERC721(ERC721_NAME, ERC721_SYMBOL) {} } abstract contract VaultFactoryEvents { /// @dev Emitted when a new vault is deployed. /// @param vault The address of the newly deployed vault. /// @param vaultId The id of the newly deployed vault. event VaultDeployed(address indexed vault, uint256 indexed vaultId); /// @dev Emitted when a new token/position is minted by a vault. /// @param vault The address of the vault that minted the token. /// @param user The address of the user who received the minted token. /// @param tokenId The ID of the newly minted token. event NewPositionMinted(address indexed vault, address indexed user, uint256 indexed tokenId); /// @dev Emitted when the deployer is modified by owner. /// @param deployer Address whose deployer status is updated. /// @param allowed Indicates whether the address is authorized as a deployer or not. event LogSetDeployer(address indexed deployer, bool indexed allowed); /// @dev Emitted when the globalAuth is modified by owner. /// @param globalAuth Address whose globalAuth status is updated. /// @param allowed Indicates whether the address is authorized as a deployer or not. event LogSetGlobalAuth(address indexed globalAuth, bool indexed allowed); /// @dev Emitted when the vaultAuth is modified by owner. /// @param vaultAuth Address whose vaultAuth status is updated. /// @param allowed Indicates whether the address is authorized as a deployer or not. /// @param vault Address of the specific vault related to the authorization change. event LogSetVaultAuth(address indexed vaultAuth, bool indexed allowed, address indexed vault); /// @dev Emitted when the vault deployment logic is modified by owner. /// @param vaultDeploymentLogic The address of the vault deployment logic contract. /// @param allowed Indicates whether the address is authorized as a deployer or not. event LogSetVaultDeploymentLogic(address indexed vaultDeploymentLogic, bool indexed allowed); } abstract contract VaultFactoryCore is VaultFactoryVariables, VaultFactoryEvents { constructor(address owner_) validAddress(owner_) VaultFactoryVariables(owner_) {} /// @dev validates that an address is not the zero address modifier validAddress(address value_) { if (value_ == address(0)) { revert FluidVaultError(ErrorTypes.VaultFactory__InvalidParams); } _; } } /// @dev Implements Vault Factory auth-only callable methods. Owner / auths can set various config values and /// can define the allow-listed deployers. abstract contract VaultFactoryAuth is VaultFactoryCore { /// @notice Sets an address (`deployer_`) as allowed deployer or not. /// This function can only be called by the owner. /// @param deployer_ The address to be set as deployer. /// @param allowed_ A boolean indicating whether the specified address is allowed to deploy vaults. function setDeployer(address deployer_, bool allowed_) external onlyOwner validAddress(deployer_) { _deployers[deployer_] = allowed_; emit LogSetDeployer(deployer_, allowed_); } /// @notice Sets an address (`globalAuth_`) as a global authorization or not. /// This function can only be called by the owner. /// @param globalAuth_ The address to be set as global authorization. /// @param allowed_ A boolean indicating whether the specified address is allowed to update any vault config. function setGlobalAuth(address globalAuth_, bool allowed_) external onlyOwner validAddress(globalAuth_) { _globalAuths[globalAuth_] = allowed_; emit LogSetGlobalAuth(globalAuth_, allowed_); } /// @notice Sets an address (`vaultAuth_`) as allowed vault authorization or not for a specific vault (`vault_`). /// This function can only be called by the owner. /// @param vault_ The address of the vault for which the authorization is being set. /// @param vaultAuth_ The address to be set as vault authorization. /// @param allowed_ A boolean indicating whether the specified address is allowed to update the specific vault config. function setVaultAuth( address vault_, address vaultAuth_, bool allowed_ ) external onlyOwner validAddress(vaultAuth_) { _vaultAuths[vault_][vaultAuth_] = allowed_; emit LogSetVaultAuth(vaultAuth_, allowed_, vault_); } /// @notice Sets an address as allowed vault deployment logic (`deploymentLogic_`) contract or not. /// This function can only be called by the owner. /// @param deploymentLogic_ The address of the vault deployment logic contract to be set. /// @param allowed_ A boolean indicating whether the specified address is allowed to deploy new type of vault. function setVaultDeploymentLogic( address deploymentLogic_, bool allowed_ ) public onlyOwner validAddress(deploymentLogic_) { _vaultDeploymentLogics[deploymentLogic_] = allowed_; emit LogSetVaultDeploymentLogic(deploymentLogic_, allowed_); } /// @notice Spell allows owner aka governance to do any arbitrary call on factory /// @param target_ Address to which the call needs to be delegated /// @param data_ Data to execute at the delegated address function spell(address target_, bytes memory data_) external onlyOwner returns (bytes memory response_) { assembly { let succeeded := delegatecall(gas(), target_, add(data_, 0x20), mload(data_), 0, 0) let size := returndatasize() response_ := mload(0x40) mstore(0x40, add(response_, and(add(add(size, 0x20), 0x1f), not(0x1f)))) mstore(response_, size) returndatacopy(add(response_, 0x20), 0, size) switch iszero(succeeded) case 1 { // throw if delegatecall failed returndatacopy(0x00, 0x00, size) revert(0x00, size) } } } /// @notice Checks if the provided address (`deployer_`) is authorized as a deployer. /// @param deployer_ The address to be checked for deployer authorization. /// @return Returns `true` if the address is a deployer, otherwise `false`. function isDeployer(address deployer_) public view returns (bool) { return _deployers[deployer_] || owner == deployer_; } /// @notice Checks if the provided address (`globalAuth_`) has global vault authorization privileges. /// @param globalAuth_ The address to be checked for global authorization privileges. /// @return Returns `true` if the given address has global authorization privileges, otherwise `false`. function isGlobalAuth(address globalAuth_) public view returns (bool) { return _globalAuths[globalAuth_] || owner == globalAuth_; } /// @notice Checks if the provided address (`vaultAuth_`) has vault authorization privileges for the specified vault (`vault_`). /// @param vault_ The address of the vault to check. /// @param vaultAuth_ The address to be checked for vault authorization privileges. /// @return Returns `true` if the given address has vault authorization privileges for the specified vault, otherwise `false`. function isVaultAuth(address vault_, address vaultAuth_) public view returns (bool) { return _vaultAuths[vault_][vaultAuth_] || owner == vaultAuth_; } /// @notice Checks if the provided (`vaultDeploymentLogic_`) address has authorization for vault deployment. /// @param vaultDeploymentLogic_ The address of the vault deploy logic to check for authorization privileges. /// @return Returns `true` if the given address has authorization privileges for vault deployment, otherwise `false`. function isVaultDeploymentLogic(address vaultDeploymentLogic_) public view returns (bool) { return _vaultDeploymentLogics[vaultDeploymentLogic_]; } } /// @dev implements VaultFactory deploy vault related methods. abstract contract VaultFactoryDeployment is VaultFactoryCore, VaultFactoryAuth { /// @dev Deploys a contract using the CREATE opcode with the provided bytecode (`bytecode_`). /// This is an internal function, meant to be used within the contract to facilitate the deployment of other contracts. /// @param bytecode_ The bytecode of the contract to be deployed. /// @return address_ Returns the address of the deployed contract. function _deploy(bytes memory bytecode_) internal returns (address address_) { if (bytecode_.length == 0) { revert FluidVaultError(ErrorTypes.VaultFactory__InvalidOperation); } /// @solidity memory-safe-assembly assembly { address_ := create(0, add(bytecode_, 0x20), mload(bytecode_)) } if (address_ == address(0)) { revert FluidVaultError(ErrorTypes.VaultFactory__InvalidOperation); } } /// @notice Deploys a new vault using the specified deployment logic `vaultDeploymentLogic_` and data `vaultDeploymentData_`. /// Only accounts with deployer access or the owner can deploy a new vault. /// @param vaultDeploymentLogic_ The address of the vault deployment logic contract. /// @param vaultDeploymentData_ The data to be used for vault deployment. /// @return vault_ Returns the address of the newly deployed vault. function deployVault( address vaultDeploymentLogic_, bytes calldata vaultDeploymentData_ ) external returns (address vault_) { // Revert if msg.sender doesn't have deployer access or is an owner. if (!isDeployer(msg.sender)) revert FluidVaultError(ErrorTypes.VaultFactory__Unauthorized); // Revert if vaultDeploymentLogic_ is not whitelisted. if (!isVaultDeploymentLogic(vaultDeploymentLogic_)) revert FluidVaultError(ErrorTypes.VaultFactory__Unauthorized); // Vault ID for the new vault and also acts as `nonce` for CREATE uint256 vaultId_ = ++_totalVaults; // compute vault address for vault id. vault_ = getVaultAddress(vaultId_); // deploy the vault using vault deployment logic by making .delegatecall (bool success_, bytes memory data_) = vaultDeploymentLogic_.delegatecall(vaultDeploymentData_); if (!(success_ && vault_ == _deploy(abi.decode(data_, (bytes))) && isVault(vault_))) { revert FluidVaultError(ErrorTypes.VaultFactory__InvalidVaultAddress); } emit VaultDeployed(vault_, vaultId_); } /// @notice Computes the address of a vault based on its given ID (`vaultId_`). /// @param vaultId_ The ID of the vault. /// @return vault_ Returns the computed address of the vault. function getVaultAddress(uint256 vaultId_) public view returns (address vault_) { // @dev based on https://ethereum.stackexchange.com/a/61413 // nonce of smart contract always starts with 1. so, with nonce 0 there won't be any deployment // hence, nonce of vault deployment starts with 1. bytes memory data; if (vaultId_ == 0x00) { return address(0); } else if (vaultId_ <= 0x7f) { data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), address(this), uint8(vaultId_)); } else if (vaultId_ <= 0xff) { data = abi.encodePacked(bytes1(0xd7), bytes1(0x94), address(this), bytes1(0x81), uint8(vaultId_)); } else if (vaultId_ <= 0xffff) { data = abi.encodePacked(bytes1(0xd8), bytes1(0x94), address(this), bytes1(0x82), uint16(vaultId_)); } else if (vaultId_ <= 0xffffff) { data = abi.encodePacked(bytes1(0xd9), bytes1(0x94), address(this), bytes1(0x83), uint24(vaultId_)); } else { data = abi.encodePacked(bytes1(0xda), bytes1(0x94), address(this), bytes1(0x84), uint32(vaultId_)); } return address(uint160(uint256(keccak256(data)))); } /// @notice Checks if a given address (`vault_`) corresponds to a valid vault. /// @param vault_ The vault address to check. /// @return Returns `true` if the given address corresponds to a valid vault, otherwise `false`. function isVault(address vault_) public view returns (bool) { if (vault_.code.length == 0) { return false; } else { // VAULT_ID() function signature is 0x540acabc (bool success_, bytes memory data_) = vault_.staticcall(hex"540acabc"); return success_ && vault_ == getVaultAddress(abi.decode(data_, (uint256))); } } /// @notice Returns the total number of vaults deployed by the factory. /// @return Returns the total number of vaults. function totalVaults() external view returns (uint256) { return _totalVaults; } } abstract contract VaultFactoryERC721 is VaultFactoryCore, VaultFactoryDeployment { /// @notice Mints a new ERC721 token for a specific vault (`vaultId_`) to a specified user (`user_`). /// Only the corresponding vault is authorized to mint a token. /// @param vaultId_ The ID of the vault that's minting the token. /// @param user_ The address receiving the minted token. /// @return tokenId_ The ID of the newly minted token. function mint(uint256 vaultId_, address user_) external returns (uint256 tokenId_) { if (msg.sender != getVaultAddress(vaultId_)) revert FluidVaultError(ErrorTypes.VaultFactory__InvalidVault); // Using _mint() instead of _safeMint() to allow any msg.sender to receive ERC721 without onERC721Received holder. tokenId_ = _mint(user_, vaultId_); emit NewPositionMinted(msg.sender, user_, tokenId_); } /// @notice Returns the URI of the specified token ID (`id_`). /// In this implementation, an empty string is returned as no specific URI is defined. /// @param id_ The ID of the token to query. /// @return An empty string since no specific URI is defined in this implementation. function tokenURI(uint256 id_) public view virtual override returns (string memory) { return ""; } } /// @title Fluid VaultFactory /// @notice creates Fluid vault protocol vaults, which are interacting with Fluid Liquidity to deposit / borrow funds. /// Vaults are created at a deterministic address, given an incrementing `vaultId` (see `getVaultAddress()`). /// Vaults can only be deployed by allow-listed deployer addresses. /// This factory also implements ERC721-Enumerable, the NFTs are used to represent created user positions. Only vaults /// can mint new NFTs. /// @dev Note the deployed vaults start out with no config at Liquidity contract. /// This must be done by Liquidity auths in a separate step, otherwise no deposits will be possible. /// This contract is not upgradeable. It supports adding new vault deployment logic contracts for new, future vaults. contract FluidVaultFactory is VaultFactoryCore, VaultFactoryAuth, VaultFactoryDeployment, VaultFactoryERC721 { constructor(address owner_) VaultFactoryCore(owner_) {} }