// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import { ERC20, IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import { ERC4626 } from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC4626.sol";
import { ReentrancyGuard } from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";

/// @title VaultfireBIPVaultV3
/// @notice Non-custodial ERC-4626 vault with virtual shares/assets to mitigate
/// first-depositor donation attacks.
/// @dev OpenZeppelin ERC4626 supplies the audited ERC-20 share token, all
/// standard preview/max methods, directional rounding, SafeERC20 transfers,
/// and delegated withdrawals through ERC-20 allowances. The custom guards
/// below reject zero amounts, fee-on-transfer assets, and deposits that would
/// transfer assets while minting zero shares.
contract VaultfireBIPVaultV3 is ERC4626, ReentrancyGuard {
    // ---------------------------------------------------------------------
    // Errors + Events
    // ---------------------------------------------------------------------

    error NotOwner();
    error ZeroAddress();
    error ZeroAmount();
    error ZeroShares();
    error UnsupportedAssetTransfer();
    error AssetCapZero();

    event DepositV3(address indexed sender, address indexed receiver, uint256 assets, uint256 shares);
    event WithdrawV3(
        address indexed sender, address indexed receiver, address indexed owner_, uint256 assets, uint256 shares
    );
    event OwnerSetV3(address indexed previous, address indexed current);

    // ---------------------------------------------------------------------
    // Storage
    // ---------------------------------------------------------------------

    address public owner;
    /// @notice Immutable gross asset ceiling for the pilot. Donations count
    ///         toward this ceiling because ERC-4626 totalAssets includes them.
    uint256 public immutable totalAssetCap;

    // Keep the previous accounting aliases for downstream V3 consumers. The
    // canonical ERC-20 names are totalSupply() and balanceOf().
    uint256 public constant VIRTUAL_ASSETS = 1;
    uint256 public constant VIRTUAL_SHARES_DECIMAL_OFFSET = 6;

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    constructor(address asset_, address owner_, uint256 totalAssetCap_)
        ERC20("Vaultfire BIP Vault V3 Share", "vBIPV3")
        ERC4626(IERC20(asset_))
    {
        if (asset_ == address(0) || owner_ == address(0)) {
            revert ZeroAddress();
        }
        if (totalAssetCap_ == 0) revert AssetCapZero();
        owner = owner_;
        totalAssetCap = totalAssetCap_;
    }

    /// @notice Returns the ERC-20 share balance under the legacy V3 name.
    function shareOf(address account) external view returns (uint256) {
        return balanceOf(account);
    }

    /// @notice Returns the ERC-20 share supply under the legacy V3 name.
    function totalShares() external view returns (uint256) {
        return totalSupply();
    }

    function setOwner(address newOwner) external onlyOwner nonReentrant {
        if (newOwner == address(0)) revert ZeroAddress();
        emit OwnerSetV3(owner, newOwner);
        owner = newOwner;
    }

    // ---------------------------------------------------------------------
    // ERC-4626 configuration and guarded entry points
    // ---------------------------------------------------------------------

    /// @dev Creates 1e6 virtual shares per virtual asset. OpenZeppelin's
    /// ERC4626 conversion functions add one virtual asset automatically.
    function _decimalsOffset() internal pure override returns (uint8) {
        return 6;
    }

    /// @notice Remaining gross asset capacity. This preserves ERC-4626's
    /// max-function semantics while preventing new liabilities above the
    /// immutable pilot cap.
    function maxDeposit(address) public view override returns (uint256) {
        uint256 held = totalAssets();
        return held >= totalAssetCap ? 0 : totalAssetCap - held;
    }

    function maxMint(address receiver) public view override returns (uint256) {
        if (totalAssetCap == type(uint256).max) return type(uint256).max;
        return previewDeposit(maxDeposit(receiver));
    }

    /// @notice Deposits assets and mints the exact previewed nonzero shares.
    function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256 shares) {
        if (assets == 0) revert ZeroAmount();
        if (receiver == address(0)) revert ZeroAddress();

        shares = previewDeposit(assets);
        if (shares == 0) revert ZeroShares();

        return super.deposit(assets, receiver);
    }

    /// @notice Mints shares, pulling the rounded-up previewed asset amount.
    function mint(uint256 shares, address receiver) public override nonReentrant returns (uint256 assets) {
        if (shares == 0) revert ZeroAmount();
        if (receiver == address(0)) revert ZeroAddress();

        return super.mint(shares, receiver);
    }

    /// @notice Withdraws exact assets, spending the owner's share allowance
    /// when the caller differs from the owner.
    function withdraw(uint256 assets, address receiver, address owner_)
        public
        override
        nonReentrant
        returns (uint256 shares)
    {
        if (assets == 0) revert ZeroAmount();
        if (receiver == address(0) || owner_ == address(0)) revert ZeroAddress();

        return super.withdraw(assets, receiver, owner_);
    }

    /// @notice Redeems exact shares, spending the owner's share allowance
    /// when the caller differs from the owner.
    function redeem(uint256 shares, address receiver, address owner_)
        public
        override
        nonReentrant
        returns (uint256 assets)
    {
        if (shares == 0) revert ZeroAmount();
        if (receiver == address(0) || owner_ == address(0)) revert ZeroAddress();

        return super.redeem(shares, receiver, owner_);
    }

    /// @dev Rejects fee-on-transfer/rebasing behavior during a deposit so the
    /// minted shares always remain fully backed by assets actually received.
    function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override {
        // slither-disable-next-line reentrancy-balance
        uint256 assetsBefore = IERC20(asset()).balanceOf(address(this));
        super._deposit(caller, receiver, assets, shares);
        uint256 assetsAfter = IERC20(asset()).balanceOf(address(this));
        if (assetsAfter < assetsBefore || assetsAfter - assetsBefore != assets) {
            revert UnsupportedAssetTransfer();
        }
        emit DepositV3(caller, receiver, assets, shares);
    }

    function _withdraw(address caller, address receiver, address owner_, uint256 assets, uint256 shares)
        internal
        override
    {
        // slither-disable-next-line reentrancy-balance
        uint256 assetsBefore = IERC20(asset()).balanceOf(address(this));
        // slither-disable-next-line reentrancy-balance
        uint256 receiverBefore = IERC20(asset()).balanceOf(receiver);
        super._withdraw(caller, receiver, owner_, assets, shares);
        uint256 assetsAfter = IERC20(asset()).balanceOf(address(this));
        uint256 receiverAfter = IERC20(asset()).balanceOf(receiver);
        if (
            assetsAfter > assetsBefore || assetsBefore - assetsAfter != assets || receiverAfter < receiverBefore
                || receiverAfter - receiverBefore != assets
        ) {
            revert UnsupportedAssetTransfer();
        }
        emit WithdrawV3(caller, receiver, owner_, assets, shares);
    }
}
