// 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); } }