// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; /// @title SigmaVault /// @author sigmacode.io (Sanicura d.o.o.) /// @notice Fee-free ERC-4626 tokenized vault over a single ERC-20 asset, with a configurable deposit cap. /// @dev Yield accrues by increasing the vault's asset balance (e.g. a strategy or treasury "donates" profits), /// which raises the share price for all holders pro rata. /// /// Inflation ("donation") attack mitigation: OpenZeppelin's virtual shares/assets with a /// {DECIMALS_OFFSET} of 6. Shares have `asset.decimals() + 6` decimals, so an attacker who front-runs /// the first deposit and donates `X` assets would have to lose roughly `X * 1e6`-fold more value than /// they could steal. Rounding always favours the vault (ERC-4626 spec). /// /// Deposit cap: {maxDeposit} / {maxMint} return the remaining headroom, so standard ERC-4626 integrators /// and routers see the limit before calling. Exceeding it reverts with OZ's `ERC4626ExceededMaxDeposit` /// / `ERC4626ExceededMaxMint`. Withdrawals are never restricted by the cap. /// /// Not supported: fee-on-transfer and rebasing assets. contract SigmaVault is ERC4626, Ownable2Step { /// @notice Extra decimals of shares relative to the asset (virtual-shares offset). uint8 public constant DECIMALS_OFFSET = 6; /// @notice Maximum total assets the vault accepts through deposits/mints. uint256 public depositCap; /// @notice Emitted when the deposit cap changes. event DepositCapUpdated(uint256 oldCap, uint256 newCap); /// @param asset_ Underlying ERC-20. /// @param name_ Share token name. /// @param symbol_ Share token symbol. /// @param initialOwner Owner allowed to change the cap. /// @param depositCap_ Initial deposit cap in asset units. constructor(IERC20 asset_, string memory name_, string memory symbol_, address initialOwner, uint256 depositCap_) ERC20(name_, symbol_) ERC4626(asset_) Ownable(initialOwner) { depositCap = depositCap_; emit DepositCapUpdated(0, depositCap_); } /// @notice Sets a new deposit cap. Lowering it below current assets only blocks new deposits. /// @param newCap New cap in asset units. function setDepositCap(uint256 newCap) external onlyOwner { emit DepositCapUpdated(depositCap, newCap); depositCap = newCap; } /// @notice Remaining assets that can be deposited before hitting the cap. function maxDeposit(address) public view override returns (uint256) { uint256 assets = totalAssets(); return assets >= depositCap ? 0 : depositCap - assets; } /// @notice Remaining shares that can be minted before hitting the cap. function maxMint(address receiver) public view override returns (uint256) { return _convertToShares(maxDeposit(receiver), Math.Rounding.Floor); } /// @dev Virtual shares offset for inflation-attack resistance. function _decimalsOffset() internal pure override returns (uint8) { return DECIMALS_OFFSET; } }