Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/implementation/ConcretePredepositVaultImpl.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
import {ConcreteStandardVaultImpl} from "./ConcreteStandardVaultImpl.sol";
import {IConcreteStandardVaultImpl} from "../interface/IConcreteStandardVaultImpl.sol";
import {IConcretePredepositVaultImpl} from "../interface/IConcretePredepositVaultImpl.sol";
import {
ConcretePredepositVaultImplStorageLib as PDVLib
} from "../lib/storage/ConcretePredepositVaultImplStorageLib.sol";
import {ConcreteV2RolesLib as RolesLib} from "../lib/Roles.sol";
import {IPredepostVaultOApp} from "../periphery/interface/IPredepostVaultOApp.sol";
import {ConcreteV2ConversionLib as ConversionLib} from "../lib/Conversion.sol";
import {Math} from "@openzeppelin-contracts/utils/math/Math.sol";
import {
ConcreteCachedVaultStateStorageLib as CachedVaultStateLib
} from "../lib/storage/ConcreteCachedVaultStateStorageLib.sol";
/**
* @title ConcretePredepositVaultImpl
* @notice A vault implementation that extends ConcreteStandardVaultImpl with cross-chain share claiming via a standalone OApp.
* @dev This is useful for pre-deposit phases where users can claim their shares on a different chain.
* Use setWithdrawLimits(0, 0) to disable withdrawals during the predeposit phase.
* Claims can only occur when cachedTotalAssets() == 0 (all assets allocated away).
* The vault uses a separate OApp contract for cross-chain messaging.
*
* @custom:warning IMPORTANT: Assets deposited into this vault are intended to be bridged to a remote chain.
* Users MUST have custody/control of their address on the destination chain to receive shares.
* Shares are sent to the same address on the remote chain - ensure you control this address
* before depositing or claiming. Loss of custody on the destination chain means loss of funds.
*/
contract ConcretePredepositVaultImpl is ConcreteStandardVaultImpl, IConcretePredepositVaultImpl {
using ConversionLib for uint256;
// Message type identifier for cross-chain claims
uint16 public constant MSG_TYPE_CLAIM = 1;
uint16 public constant MSG_TYPE_BATCH_CLAIM = 2;
/// @notice Event emitted when OApp address is set
event OAppSet(address indexed oapp);
/**
* @dev Constructor
* @param factory The address of the factory
*/
constructor(address factory) ConcreteStandardVaultImpl(factory) {}
/**
* @dev Initialization function that will be called when a proxy vault is deployed through `ConcreteFactory`.
* @param initialVersion The initial version of the vault
* @param owner The owner of the vault
* @param data Encoded initialization data (allocateModule, asset, initialVaultManager, name, symbol)
*/
function _initialize(uint64 initialVersion, address owner, bytes memory data) internal virtual override {
(
address allocateModuleAddr,
address asset,
address initialVaultManager,
string memory name,
string memory symbol
) = abi.decode(data, (address, address, address, string, string));
// Call parent initialization
super._initialize(
initialVersion, owner, abi.encode(allocateModuleAddr, asset, initialVaultManager, name, symbol)
);
// Initialize self claims setting to false (can be enabled via setSelfClaimsEnabled)
PDVLib.ConcretePredepositVaultImplStorage storage $ = PDVLib.fetch();
$.selfClaimsEnabled = false;
}
/// @inheritdoc IConcretePredepositVaultImpl
function claimOnTargetChain(bytes calldata options) external payable nonReentrant withYieldAccrual {
PDVLib.ConcretePredepositVaultImplStorage storage $ = PDVLib.fetch();
// Ensure self claims are enabled
require($.selfClaimsEnabled, SelfClaimsDisabled());
_validateClaimConditions($);
// Get user's current share balance
uint256 userShares = balanceOf(msg.sender);
require(userShares != 0, NoSharesToClaim());
// decrease cached totalAssets proportionally to the user's shares to maintain the share price
uint256 assets = userShares.calcConvertToAssets(totalSupply(), cachedTotalAssets(), Math.Rounding.Floor, false);
CachedVaultStateLib.fetch().cachedTotalAssets = cachedTotalAssets() - assets;
_burn(msg.sender, userShares);
// Store locked shares
$.lockedShares[msg.sender] += userShares;
bytes memory payload = abi.encode(MSG_TYPE_CLAIM, msg.sender, userShares);
// Send the message via the OApp (quote and fee validation done internally)
IPredepostVaultOApp($.oapp).send{value: msg.value}(payload, options, msg.sender);
emit SharesClaimedOnTargetChain(msg.sender, userShares);
}
/// @inheritdoc IConcretePredepositVaultImpl
function batchClaimOnTargetChain(bytes calldata addressesData, bytes calldata options)
external
payable
nonReentrant
withYieldAccrual
onlyRole(RolesLib.VAULT_MANAGER)
{
PDVLib.ConcretePredepositVaultImplStorage storage $ = PDVLib.fetch();
_validateClaimConditions($);
// Decode addresses array
address[] memory addresses = abi.decode(addressesData, (address[]));
require(addresses.length > 0 && addresses.length <= 150, BadAddressArrayLength(addresses.length));
uint256[] memory sharesArray = new uint256[](addresses.length);
uint256 totalShares = 0;
for (uint256 i = 0; i < addresses.length; i++) {
address user = addresses[i];
require(user != address(0), InvalidUserAddress());
uint256 userShares = balanceOf(user);
if (userShares == 0) continue; // Skip users with no shares, already claimed, duplicates in list
// decrease cached totalAssets proportionally to the user's shares to maintain the share price
uint256 assets =
userShares.calcConvertToAssets(totalSupply(), cachedTotalAssets(), Math.Rounding.Floor, false);
CachedVaultStateLib.fetch().cachedTotalAssets = cachedTotalAssets() - assets;
_burn(user, userShares);
// Store locked shares
$.lockedShares[user] += userShares;
// Store in batch arrays
sharesArray[i] = userShares;
totalShares += userShares;
emit SharesClaimedOnTargetChain(user, userShares);
}
require(totalShares > 0, NoSharesInBatch());
bytes memory payload = abi.encode(MSG_TYPE_BATCH_CLAIM, addresses, sharesArray);
// Send the message via the OApp (quote and fee validation done internally)
IPredepostVaultOApp($.oapp).send{value: msg.value}(payload, options, msg.sender);
}
/// @inheritdoc IConcretePredepositVaultImpl
function getLockedShares(address user) external view returns (uint256) {
return PDVLib.fetch().lockedShares[user];
}
/// @inheritdoc IConcretePredepositVaultImpl
function setSelfClaimsEnabled(bool enabled) external onlyRole(RolesLib.VAULT_MANAGER) {
PDVLib.ConcretePredepositVaultImplStorage storage $ = PDVLib.fetch();
$.selfClaimsEnabled = enabled;
emit SelfClaimsEnabledUpdated(enabled);
}
/// @inheritdoc IConcretePredepositVaultImpl
function getSelfClaimsEnabled() external view returns (bool) {
return PDVLib.fetch().selfClaimsEnabled;
}
/// @inheritdoc IConcretePredepositVaultImpl
function setOApp(address oappAddress) external onlyRole(RolesLib.VAULT_MANAGER) {
PDVLib.ConcretePredepositVaultImplStorage storage $ = PDVLib.fetch();
$.oapp = oappAddress;
emit OAppSet(oappAddress);
}
/// @inheritdoc IConcretePredepositVaultImpl
function getOApp() external view returns (address) {
return PDVLib.fetch().oapp;
}
/**
* @dev Upgrade function that handles migration from ConcreteStandardVaultImpl to ConcretePredepositVaultImpl
* @dev Sets selfClaimsEnabled to false by default (can be enabled via setSelfClaimsEnabled)
*/
function _upgrade(
uint64,
/* oldVersion */
uint64,
/* newVersion */
bytes calldata /* data */
)
internal
virtual
override
{
// Initialize self claims setting to false (can be enabled via setSelfClaimsEnabled)
PDVLib.ConcretePredepositVaultImplStorage storage $ = PDVLib.fetch();
$.selfClaimsEnabled = false;
}
/**
* @dev Internal function to validate claim conditions
* @param $ Storage reference to ConcretePredepositVaultImplStorage
*/
function _validateClaimConditions(PDVLib.ConcretePredepositVaultImplStorage storage $) internal view {
// Ensure OApp is set
require($.oapp != address(0), OAppNotSet());
// Ensure deposits are locked
(uint256 maxDepositAmount,) = getDepositLimits();
require(maxDepositAmount == 0, DepositsNotLocked());
// Ensure withdrawals are locked
(uint256 maxWithdrawAmount,) = getWithdrawLimits();
require(maxWithdrawAmount == 0, WithdrawalsNotLocked());
}
}
"
},
"src/implementation/ConcreteStandardVaultImpl.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.24;
/**
* @title ConcreteStandardVaultImpl
* @notice ERC-4626 upgradeable standard vault implementation for the Concrete Earn V2 protocol.
* Holds an underlying ERC20 asset and exposes deposit/mint/withdraw/redeem flows.
* Integrates with strategy modules to route assets to yield sources.
*
* @author Blueprint Finance
* @custom:protocol Concrete Earn V2
* @custom:oz-upgrades Use OZ Upgradeable patterns and eip7201 storage layout
* @custom:source on request
* @custom:audits on request
* @custom:license AGPL-3.0
*/
// ─────────────────────────────────────────────────────────────────────────────
// External dependencies
// ─────────────────────────────────────────────────────────────────────────────
import {
AccessControlEnumerableUpgradeable
} from "@openzeppelin-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";
import {
ERC4626Upgradeable,
IERC20,
IERC4626,
Math,
SafeERC20
} from "@openzeppelin-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import {Address} from "@openzeppelin-contracts/utils/Address.sol";
import {EnumerableSet} from "@openzeppelin-contracts/utils/structs/EnumerableSet.sol";
import {Ownable} from "@openzeppelin-contracts/access/Ownable.sol";
import {SafeCast} from "@openzeppelin-contracts/utils/math/SafeCast.sol";
// ─────────────────────────────────────────────────────────────────────────────
// Protocol-facing interfaces
// ─────────────────────────────────────────────────────────────────────────────
import {IConcreteStandardVaultImpl} from "../interface/IConcreteStandardVaultImpl.sol";
import {IStrategyTemplate} from "../interface/IStrategyTemplate.sol";
// ─────────────────────────────────────────────────────────────────────────────
// Internal modules/contracts
// ─────────────────────────────────────────────────────────────────────────────
import {IAllocateModule} from "../module/AllocateModule.sol";
import {UpgradeableVault} from "../common/UpgradeableVault.sol";
// ─────────────────────────────────────────────────────────────────────────────
// Internal libraries
// ─────────────────────────────────────────────────────────────────────────────
import {ConcreteV2ConstantsLib as ConstantsLib} from "../lib/Constants.sol";
import {ConcreteV2ConversionLib as ConversionLib} from "../lib/Conversion.sol";
import {Hooks, HooksLibV1 as HooksLib} from "../lib/Hooks.sol";
import {ConcreteV2RolesLib as RolesLib} from "../lib/Roles.sol";
import {StateInitLib} from "../lib/StateInitLib.sol";
import {StateSetterLib} from "../lib/StateSetterLib.sol";
import {Time} from "../lib/Time.sol";
// ─────────────────────────────────────────────────────────────────────────────
// Storage layout libraries
// ─────────────────────────────────────────────────────────────────────────────
import {
ConcreteCachedVaultStateStorageLib as CachedVaultStateLib
} from "../lib/storage/ConcreteCachedVaultStateStorageLib.sol";
import {ConcreteStandardVaultImplStorageLib as SVLib} from "../lib/storage/ConcreteStandardVaultImplStorageLib.sol";
contract ConcreteStandardVaultImpl is
ERC4626Upgradeable,
UpgradeableVault,
AccessControlEnumerableUpgradeable,
IConcreteStandardVaultImpl
{
using Address for address;
using EnumerableSet for EnumerableSet.AddressSet;
using SafeCast for uint256;
using ConversionLib for uint256;
using HooksLib for Hooks;
event HooksSet(Hooks hooks);
/// @dev Modifier to accrue yield before function execution
modifier withYieldAccrual() {
_accrueYield();
_;
}
/**
* @dev Constructor
* @param factory The address of the factory
*/
constructor(address factory) UpgradeableVault(factory) {}
/// @inheritdoc IConcreteStandardVaultImpl
function allocate(bytes calldata data) public virtual nonReentrant onlyRole(RolesLib.ALLOCATOR) withYieldAccrual {
// delegatecall allocate module
bytes memory delegateData = abi.encodeWithSelector(IAllocateModule.allocateFunds.selector, data);
allocateModule().functionDelegateCall(delegateData);
require(IERC20(asset()).balanceOf(address(this)) >= _lockedAssets(), InsufficientBalance());
}
/// @inheritdoc IConcreteStandardVaultImpl
function accrueYield() external virtual nonReentrant {
_accrueYield();
}
/// @inheritdoc IConcreteStandardVaultImpl
function updateManagementFee(uint16 managementFee_)
external
nonReentrant
onlyRole(RolesLib.VAULT_MANAGER)
withYieldAccrual
{
StateSetterLib.updateManagementFee(managementFee_);
}
/// @inheritdoc IConcreteStandardVaultImpl
function updateManagementFeeRecipient(address recipient) external nonReentrant withYieldAccrual {
require(_msgSender() == Ownable(factory).owner(), InvalidFactoryOwner());
StateSetterLib.updateManagementFeeRecipient(recipient);
}
/// @inheritdoc IConcreteStandardVaultImpl
function updatePerformanceFee(uint16 performanceFee_)
external
nonReentrant
onlyRole(RolesLib.VAULT_MANAGER)
withYieldAccrual
{
StateSetterLib.updatePerformanceFee(performanceFee_);
}
/// @inheritdoc IConcreteStandardVaultImpl
function updatePerformanceFeeRecipient(address recipient) external nonReentrant withYieldAccrual {
require(msg.sender == Ownable(factory).owner(), InvalidFactoryOwner());
StateSetterLib.updatePerformanceFeeRecipient(recipient);
}
/// @inheritdoc IConcreteStandardVaultImpl
function setDepositLimits(uint256 minDepositAmount, uint256 maxDepositAmount)
external
nonReentrant
onlyRole(RolesLib.VAULT_MANAGER)
{
StateSetterLib.setDepositLimits(minDepositAmount, maxDepositAmount);
}
/// @inheritdoc IConcreteStandardVaultImpl
function setWithdrawLimits(uint256 minWithdrawAmount, uint256 maxWithdrawAmount)
external
nonReentrant
onlyRole(RolesLib.VAULT_MANAGER)
{
StateSetterLib.setWithdrawLimits(minWithdrawAmount, maxWithdrawAmount);
}
/// @inheritdoc IConcreteStandardVaultImpl
function getFeeConfig()
external
view
override
returns (
uint16 currentManagementFee,
address currentManagementFeeRecipient,
uint32 currentLastManagementFeeAccrual,
uint16 currentPerformanceFee,
address currentPerformanceFeeRecipient
)
{
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
return (
$.managementFee,
$.managementFeeRecipient,
$.lastManagementFeeAccrual,
$.performanceFee,
$.performanceFeeRecipient
);
}
/**
* @inheritdoc IERC4626
*/
function deposit(uint256 assets, address receiver)
public
virtual
override(IERC4626, ERC4626Upgradeable)
nonReentrant
withYieldAccrual
returns (uint256)
{
require(receiver != address(0), InvalidReceiver());
Hooks memory h = SVLib.fetch().hooks;
uint256 totalAssetsBeforeDeposit = cachedTotalAssets();
// invoke pre-deposit hook if enabled
if (h.checkIsValid(HooksLib.PRE_DEPOSIT)) {
h.preDeposit(_msgSender(), assets, receiver, totalAssetsBeforeDeposit);
}
// deposit assets
uint256 maxAssets = maxDeposit(receiver);
if (assets > maxAssets) {
revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets);
}
uint256 shares = assets.calcConvertToShares(totalSupply(), totalAssetsBeforeDeposit, Math.Rounding.Floor, true);
(uint256 maxDepositAmount, uint256 minDepositAmount) = getDepositLimits();
require(
assets + totalAssetsBeforeDeposit <= maxDepositAmount && assets >= minDepositAmount,
AssetAmountOutOfBounds(msg.sender, assets, minDepositAmount, maxDepositAmount)
);
_deposit(_msgSender(), receiver, assets, shares);
// invoke post-deposit hook if enabled
if (h.checkIsValid(HooksLib.POST_DEPOSIT)) {
h.postDeposit(_msgSender(), assets, shares, receiver, cachedTotalAssets());
}
return shares;
}
/**
* @inheritdoc IERC4626
*/
function mint(uint256 shares, address receiver)
public
virtual
override(IERC4626, ERC4626Upgradeable)
nonReentrant
withYieldAccrual
returns (uint256)
{
require(receiver != address(0), InvalidReceiver());
Hooks memory h = SVLib.fetch().hooks;
uint256 totalAssetsBeforeDeposit = cachedTotalAssets();
// invoke pre-mint hook if enabled
if (h.checkIsValid(HooksLib.PRE_MINT)) h.preMint(_msgSender(), shares, receiver, totalAssetsBeforeDeposit);
uint256 maxShares = maxMint(receiver);
if (shares > maxShares) {
revert ERC4626ExceededMaxMint(receiver, shares, maxShares);
}
uint256 assets = shares.calcConvertToAssets(totalSupply(), totalAssetsBeforeDeposit, Math.Rounding.Ceil, true);
(uint256 maxDepositAmount, uint256 minDepositAmount) = getDepositLimits();
require(
assets + totalAssetsBeforeDeposit <= maxDepositAmount && assets >= minDepositAmount,
AssetAmountOutOfBounds(_msgSender(), assets, minDepositAmount, maxDepositAmount)
);
_deposit(_msgSender(), receiver, assets, shares);
// invoke post-mint hook if enabled
if (h.checkIsValid(HooksLib.POST_MINT)) {
h.postMint(_msgSender(), assets, shares, receiver, cachedTotalAssets());
}
return assets;
}
/**
* @inheritdoc IERC4626
*/
function withdraw(uint256 assets, address receiver, address owner)
public
virtual
override(IERC4626, ERC4626Upgradeable)
nonReentrant
withYieldAccrual
returns (uint256)
{
require(receiver != address(0), InvalidReceiver());
Hooks memory h = SVLib.fetch().hooks;
uint256 totalAssetsBeforeWithdrawal = cachedTotalAssets();
if (h.checkIsValid(HooksLib.PRE_WITHDRAW)) {
h.preWithdraw(_msgSender(), assets, receiver, owner, totalAssetsBeforeWithdrawal);
}
// Optimistic maxAssets that does not account for the withdrawals from strategies, this is to avoid the need to call _simulateWithdraw().
// If maxAssets is greater than the actual withdrawable amount, _executeWithdraw() will revert.
uint256 totalSupply_ = totalSupply();
uint256 maxAssets =
balanceOf(owner).calcConvertToAssets(totalSupply_, totalAssetsBeforeWithdrawal, Math.Rounding.Floor, false);
uint256 shares = assets.calcConvertToShares(totalSupply_, totalAssetsBeforeWithdrawal, Math.Rounding.Ceil, true);
if (assets > maxAssets || _executeWithdraw(_msgSender(), receiver, owner, assets, shares) < assets) {
revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);
}
(uint256 maxWithdrawAmount, uint256 minWithdrawAmount) = getWithdrawLimits();
require(
assets <= maxWithdrawAmount && assets >= minWithdrawAmount,
AssetAmountOutOfBounds(_msgSender(), assets, minWithdrawAmount, maxWithdrawAmount)
);
// invoke post-withdraw hook if enabled
if (h.checkIsValid(HooksLib.POST_WITHDRAW)) {
h.postWithdraw(_msgSender(), assets, shares, receiver, cachedTotalAssets());
}
return shares;
}
/**
* @inheritdoc IERC4626
*/
function redeem(uint256 shares, address receiver, address owner)
public
virtual
override(IERC4626, ERC4626Upgradeable)
nonReentrant
withYieldAccrual
returns (uint256)
{
require(receiver != address(0), InvalidReceiver());
Hooks memory h = SVLib.fetch().hooks;
uint256 totalAssetsBeforeWithdrawal = cachedTotalAssets();
// invoke pre-redeem hook if enabled
if (h.checkIsValid(HooksLib.PRE_REDEEM)) {
h.preRedeem(_msgSender(), shares, receiver, owner, totalAssetsBeforeWithdrawal);
}
// Optimistic maxShares that does not account for the withdrawals from strategies, this is to avoid the need to call _simulateWithdraw().
// If maxShares is greater than the actual redeemable amount, _executeWithdraw() will revert.
uint256 maxShares = balanceOf(owner);
uint256 assets =
shares.calcConvertToAssets(totalSupply(), totalAssetsBeforeWithdrawal, Math.Rounding.Floor, true);
if (shares > maxShares || _executeWithdraw(_msgSender(), receiver, owner, assets, shares) < assets) {
revert ERC4626ExceededMaxRedeem(owner, shares, maxShares);
}
(uint256 maxWithdrawAmount, uint256 minWithdrawAmount) = getWithdrawLimits();
require(
assets <= maxWithdrawAmount && assets >= minWithdrawAmount,
AssetAmountOutOfBounds(_msgSender(), assets, minWithdrawAmount, maxWithdrawAmount)
);
// invoke post-redeem hook if enabled
if (h.checkIsValid(HooksLib.POST_REDEEM)) {
h.postRedeem(_msgSender(), assets, shares, receiver, cachedTotalAssets());
}
return assets;
}
/**
* @inheritdoc IConcreteStandardVaultImpl
*/
function setHooks(Hooks memory hooks) external virtual nonReentrant onlyRole(RolesLib.HOOK_MANAGER) {
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
$.hooks = hooks;
emit HooksSet(hooks);
}
/**
* @notice overwrites the deallocation order from strategies;
*/
function setDeallocationOrder(address[] calldata order) external virtual nonReentrant onlyRole(RolesLib.ALLOCATOR) {
StateSetterLib.setDeallocationOrder(order);
}
/**
* @inheritdoc IConcreteStandardVaultImpl
*/
function addStrategy(address strategy) public virtual nonReentrant onlyRole(RolesLib.STRATEGY_MANAGER) {
require(IStrategyTemplate(strategy).asset() == asset(), InvalidStrategyAsset());
StateSetterLib.addStrategy(strategy);
}
/**
* @inheritdoc IConcreteStandardVaultImpl
*/
function removeStrategy(address strategy) public virtual nonReentrant onlyRole(RolesLib.STRATEGY_MANAGER) {
StateSetterLib.removeStrategy(strategy);
}
/**
* @inheritdoc IConcreteStandardVaultImpl
*/
function toggleStrategyStatus(address strategy) public virtual nonReentrant onlyRole(RolesLib.STRATEGY_MANAGER) {
StateSetterLib.toggleStrategyStatus(strategy);
}
/**
* @notice Returns the deallocation order from strategies.
*/
function getDeallocationOrder() external view returns (address[] memory order) {
return SVLib.fetch().deallocationOrder;
}
/**
* @dev See {IERC4626-previewDeposit}.
*/
function previewDeposit(uint256 assets)
public
view
virtual
override(IERC4626, ERC4626Upgradeable)
returns (uint256)
{
(uint256 totalAssetsPreview, uint256 totalSupply) = _previewAccrueYieldAndFees();
return assets.calcConvertToShares(totalSupply, totalAssetsPreview, Math.Rounding.Floor, false);
}
/**
* @inheritdoc IERC4626
*/
function previewMint(uint256 shares) public view virtual override(IERC4626, ERC4626Upgradeable) returns (uint256) {
(uint256 totalAssetsPreview, uint256 totalSupply) = _previewAccrueYieldAndFees();
return shares.calcConvertToAssets(totalSupply, totalAssetsPreview, Math.Rounding.Ceil, false);
}
/**
* @inheritdoc IERC4626
*/
function previewWithdraw(uint256 assets)
public
view
virtual
override(IERC4626, ERC4626Upgradeable)
returns (uint256)
{
(uint256 totalAssetsPreview, uint256 totalSupply) = _previewAccrueYieldAndFees();
return assets.calcConvertToShares(totalSupply, totalAssetsPreview, Math.Rounding.Ceil, false);
}
/**
* @inheritdoc IERC4626
*/
function previewRedeem(uint256 shares)
public
view
virtual
override(IERC4626, ERC4626Upgradeable)
returns (uint256)
{
(uint256 totalAssetsPreview, uint256 totalSupply) = _previewAccrueYieldAndFees();
return shares.calcConvertToAssets(totalSupply, totalAssetsPreview, Math.Rounding.Floor, false);
}
/**
* @inheritdoc IConcreteStandardVaultImpl
*/
function previewAccrueYield() public view virtual returns (uint256, uint256) {
return _previewAccrueYieldAndFees();
}
/**
* @inheritdoc IERC4626
*/
function totalAssets()
public
view
virtual
override(IERC4626, ERC4626Upgradeable)
returns (uint256 totalAssetsWithYield)
{
(totalAssetsWithYield,) = _previewAccrueYieldAndFees();
}
/**
* @inheritdoc IERC4626
*/
function maxRedeem(address owner) public view virtual override(IERC4626, ERC4626Upgradeable) returns (uint256) {
(uint256 maxAssets, uint256 expectedTotalAssets, uint256 expectedTotalSupply) = _maxWithdraw(owner);
return maxAssets.calcConvertToShares(expectedTotalSupply, expectedTotalAssets, Math.Rounding.Floor, false);
}
/**
* @inheritdoc IERC4626
*/
function maxWithdraw(address owner)
public
view
virtual
override(IERC4626, ERC4626Upgradeable)
returns (uint256 maxAssets)
{
(maxAssets,,) = _maxWithdraw(owner);
}
/**
* @inheritdoc IConcreteStandardVaultImpl
*/
function getStrategyData(address strategy) public view returns (StrategyData memory) {
return SVLib.fetch().strategyData[strategy];
}
/**
* @inheritdoc IConcreteStandardVaultImpl
*/
function getStrategies() public view returns (address[] memory) {
return SVLib.fetch().strategies.values();
}
/**
* @inheritdoc IConcreteStandardVaultImpl
*/
function allocateModule() public view returns (address) {
return SVLib.fetch().allocateModule;
}
/// @inheritdoc IConcreteStandardVaultImpl
function getDepositLimits() public view returns (uint256, uint256) {
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
return ($.maxDepositAmount, $.minDepositAmount);
}
/// @inheritdoc IConcreteStandardVaultImpl
function getWithdrawLimits() public view returns (uint256, uint256) {
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
return ($.maxWithdrawAmount, $.minWithdrawAmount);
}
/// @inheritdoc IConcreteStandardVaultImpl
function managementFee() public view returns (address, uint16, uint32) {
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
return ($.managementFeeRecipient, $.managementFee, $.lastManagementFeeAccrual);
}
/// @inheritdoc IConcreteStandardVaultImpl
function performanceFee() public view returns (address, uint16) {
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
return ($.performanceFeeRecipient, $.performanceFee);
}
/// @inheritdoc IConcreteStandardVaultImpl
function getTotalAllocated() public view returns (uint256 totalAllocated) {
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
address[] memory strategies = $.strategies.values();
for (uint256 i = 0; i < strategies.length; i++) {
totalAllocated += $.strategyData[strategies[i]].allocated;
}
}
function cachedTotalAssets() public view returns (uint256) {
return CachedVaultStateLib.fetch().cachedTotalAssets;
}
/**
* @dev Initialization function that will be called when a proxy vault is deployed through `ConcreteFactory`.
*/
function _initialize(
uint64,
/*initialVersion*/
address,
/*owner*/
bytes memory data
)
internal
virtual
override
{
(
address allocateModuleAddr,
address asset,
address initialVaultManager,
string memory name,
string memory symbol
) = abi.decode(data, (address, address, address, string, string));
require(allocateModuleAddr != address(0), InvalidAllocateModule());
require(asset != address(0), InvalidAsset());
require(initialVaultManager != address(0), InvalidInitialVaultManager());
require(bytes(name).length > 0, InvalidName());
require(bytes(symbol).length > 0, InvalidSymbol());
__ERC20_init_unchained(name, symbol);
__ERC4626_init_unchained(IERC20(asset));
__AccessControlEnumerable_init_unchained();
StateInitLib.stateInitStandardVaultImpl(allocateModuleAddr, initialVaultManager, _msgSender());
}
/**
* @dev Upgrade function that will be called when a proxy vault upgrades to this implementation
*/
function _upgrade(
uint64,
/* oldVersion */
uint64,
/* newVersion */
bytes calldata /* data */
)
internal
virtual
override
{}
/**
* @dev Internal function that executes the yield accrual operation across all active strategies.
* @dev This function iterates through all strategies managed by the vault, calculates
* yield generated and losses incurred since the last yield accrual, and updates the vault's
* internal accounting accordingly.
* @dev This function does not trigger actual fund movements, it only updates accounting
* to reflect the current state of strategy allocations.
*/
function _accrueYield() internal virtual returns (uint256) {
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
uint256 totalPositiveYield;
uint256 totalNegativeYield;
{
address[] memory strategies = $.strategies.values();
uint256 strategiesCounter = strategies.length;
for (uint256 i; i < strategiesCounter; ++i) {
(uint256 positiveYield, uint256 loss, uint256 strategyTotalAllocatedValue) =
_previewStrategyYield(strategies[i]);
// update the strategy allocated amount only if there is yield or loss, otherwise it's the same amount as when we called `allocate()`.
// we do update the lastTotalAssets after netting the yield and loss
if (positiveYield != 0 || loss != 0) {
$.strategyData[strategies[i]].allocated = strategyTotalAllocatedValue.toUint120();
totalPositiveYield += positiveYield;
totalNegativeYield += loss;
emit StrategyYieldAccrued(strategies[i], strategyTotalAllocatedValue, positiveYield, loss);
}
}
}
CachedVaultStateLib.ConcreteCachedVaultStateStorage storage $cached = CachedVaultStateLib.fetch();
uint256 totalAssetsCached = $cached.cachedTotalAssets + totalPositiveYield - totalNegativeYield;
// update the lastTotalAssets
$cached.cachedTotalAssets = totalAssetsCached;
// Accrue management fees after accruing yield to calculate fee asset amount on total vault AUM
accrueManagementFee(totalAssetsCached);
// Accrue performance fees on net yield amount
accruePerformanceFee(totalAssetsCached, totalPositiveYield, totalNegativeYield);
emit YieldAccrued(totalPositiveYield, totalNegativeYield);
return totalAssetsCached;
}
/**
* @dev Internal function that handles withdrawal operations, including strategy deallocation when needed.
* @dev This function implements the core withdrawal logic for the vault, automatically managing
* fund retrieval from both idle vault balance and allocated strategies to fulfill withdrawal requests.
* @dev Withdrawal Process:
* 1. First attempts to use idle funds (assets sitting in the vault contract)
* 2. If idle funds are insufficient, iterates through active strategies to deallocate funds
* 3. For each strategy, respects the strategy's maxWithdraw() limit
* 4. Updates strategy allocation accounting after successful deallocations
* 5. Delegates to parent contract for final ERC4626 withdrawal execution
* @dev Requirements:
* - Combined idle funds and strategy liquidity must be sufficient for withdrawal amount
* - All deallocated strategies must be in Active status
* - Strategy onWithdraw() calls must succeed and return expected amounts
* @param caller The address that initiated the withdrawal (for access control)
* @param receiver The address that will receive the withdrawn assets
* @param owner The address whose shares are being burned for the withdrawal
* @param assets The amount of assets to withdraw from the vault
* @param shares The amount of shares to burn in exchange for the assets
*/
function _executeWithdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
internal
virtual
returns (uint256)
{
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
uint256 floatingFunds = IERC20(asset()).balanceOf(address(this));
uint256 lockedAssets = _lockedAssets();
uint256 totalWithdrawableAmount = floatingFunds >= lockedAssets ? floatingFunds - lockedAssets : 0;
if (totalWithdrawableAmount < assets) {
address[] memory deallocationOrder = $.deallocationOrder;
uint256 strategiesCounter = deallocationOrder.length;
uint256 desiredAssets;
for (uint256 i; i < strategiesCounter; ++i) {
if (
($.strategyData[deallocationOrder[i]].status != IConcreteStandardVaultImpl.StrategyStatus.Active)
|| !$.strategies.contains(deallocationOrder[i])
) continue;
unchecked {
desiredAssets = assets - totalWithdrawableAmount;
}
uint256 withdrawableAmountFromStrategy = IStrategyTemplate(deallocationOrder[i]).maxWithdraw();
uint256 withdrawAmount =
(withdrawableAmountFromStrategy >= desiredAssets) ? desiredAssets : withdrawableAmountFromStrategy;
if (withdrawAmount > 0) {
// Actually withdraw from the strategy
uint256 actualWithdrawn = IStrategyTemplate(deallocationOrder[i]).onWithdraw(withdrawAmount);
// Update strategy allocated amount
$.strategyData[deallocationOrder[i]].allocated -= actualWithdrawn.toUint120();
totalWithdrawableAmount += actualWithdrawn;
}
if (totalWithdrawableAmount >= assets) break;
}
}
_withdraw(caller, receiver, owner, assets, shares);
return totalWithdrawableAmount;
}
function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
internal
virtual
override
{
if (caller != owner) {
_spendAllowance(owner, caller, shares);
}
// If asset() is ERC-777, `transfer` can trigger a reentrancy AFTER the transfer happens through the
// `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
// calls the vault, which is assumed not malicious.
//
// Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
// shares are burned and after the assets are transferred, which is a valid state.
CachedVaultStateLib.fetch().cachedTotalAssets -= assets;
_burn(owner, shares);
SafeERC20.safeTransfer(IERC20(asset()), receiver, assets);
emit Withdraw(caller, receiver, owner, assets, shares);
}
/**
* @dev Deposit/mint common workflow.
*/
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override {
// If asset() is ERC-777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the
// `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
// calls the vault, which is assumed not malicious.
//
// Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
// assets are transferred and before the shares are minted, which is a valid state.
// slither-disable-next-line reentrancy-no-eth
SafeERC20.safeTransferFrom(IERC20(asset()), caller, address(this), assets);
CachedVaultStateLib.fetch().cachedTotalAssets += assets;
_mint(receiver, shares);
emit Deposit(caller, receiver, assets, shares);
}
/**
* @dev Accrue management fees by minting shares to the fee recipient.
* @param totalAssetsAmount The total assets in the vault to calculate fee on
*/
function accrueManagementFee(uint256 totalAssetsAmount) internal {
(uint256 feeShares, uint256 feeAmount) = previewManagementFee(totalAssetsAmount);
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
// Update last accrual timestamp
$.lastManagementFeeAccrual = Time.timestamp();
// Mint shares to management fee recipient
address managementFeeRecipient = $.managementFeeRecipient;
if (feeShares != 0 && managementFeeRecipient != address(0)) {
// Mint shares to management fee recipient
_mint(managementFeeRecipient, feeShares);
emit ManagementFeeAccrued(managementFeeRecipient, feeShares, feeAmount);
}
}
/**
* @dev Accrue performance fees by minting shares to the fee recipient.
* @param totalAssetsAmount The total assets in the vault to calculate fee on
* @param positiveYield The total positive yield generated by all strategies
* @param loss The total losses incurred by all strategies
*/
function accruePerformanceFee(uint256 totalAssetsAmount, uint256 positiveYield, uint256 loss) internal {
(uint256 performanceFeeShares, uint256 feeAmount) =
previewPerformanceFee(totalAssetsAmount, positiveYield, loss, totalSupply());
if (performanceFeeShares == 0) return;
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
// Mint shares to performance fee recipient
address performanceFeeRecipient = $.performanceFeeRecipient;
if (performanceFeeRecipient != address(0)) {
_mint(performanceFeeRecipient, performanceFeeShares);
emit PerformanceFeeAccrued(performanceFeeRecipient, performanceFeeShares, feeAmount);
}
}
/**
* @dev Simulates the yield accrual operation across all strategies including fees.
* @return totalAssets The projected total assets after yield accrual (current + yield - losses)
* @return totalSupply The projected total supply after yield accrual (current + management fee shares + performance fee shares)
*/
function _previewAccrueYieldAndFees() internal view virtual returns (uint256, uint256) {
(uint256 totalPositiveYield, uint256 totalNegativeYield) = _previewYieldNoFees();
uint256 totalSupplyCached = totalSupply();
uint256 totalAssetsCached = cachedTotalAssets() + totalPositiveYield - totalNegativeYield;
(uint256 managementFeeShares,) = previewManagementFee(totalAssetsCached);
totalSupplyCached += managementFeeShares;
(uint256 performanceFeeShares,) =
previewPerformanceFee(totalAssetsCached, totalPositiveYield, totalNegativeYield, totalSupplyCached);
totalSupplyCached += performanceFeeShares;
return (totalAssetsCached, totalSupplyCached);
}
/**
* @dev Calculates total positive and negative yield across all strategies.
* @return totalPositiveYield The sum of all positive yields from strategies
* @return totalNegativeYield The sum of all losses from strategies
*/
function _previewYieldNoFees()
internal
view
virtual
returns (uint256 totalPositiveYield, uint256 totalNegativeYield)
{
address[] memory strategies = SVLib.fetch().strategies.values();
uint256 strategiesCounter = strategies.length;
for (uint256 i; i < strategiesCounter; ++i) {
(uint256 positiveYield, uint256 loss,) = _previewStrategyYield(strategies[i]);
if (positiveYield != 0 || loss != 0) {
totalPositiveYield += positiveYield;
totalNegativeYield += loss;
}
}
}
/**
* @dev Accrues yield and accounts for losses for a single strategy.
* @dev This function queries the current total allocated value from a strategy,
* compares it against the previously recorded allocated amount, and calculates
* the yield generated or loss incurred since the last yield accrual.
* @param strategy The address of the strategy contract to accrue yield from.
* @return yield The amount of positive yield generated by the strategy since last accrual.
* @return loss The amount of loss incurred by the strategy since last accrual.
*/
function _previewStrategyYield(address strategy) internal view returns (uint256, uint256, uint256) {
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
uint120 strategyAllocatedAmount = $.strategyData[strategy].allocated;
if ($.strategyData[strategy].status != IConcreteStandardVaultImpl.StrategyStatus.Active) {
return (0, 0, 0);
}
uint256 currentTotalAllocatedValue = IStrategyTemplate(strategy).totalAllocatedValue();
currentTotalAllocatedValue =
(currentTotalAllocatedValue >= type(uint120).max) ? type(uint120).max : currentTotalAllocatedValue;
uint256 yield;
uint256 loss;
if (currentTotalAllocatedValue == strategyAllocatedAmount) {
return (yield, loss, currentTotalAllocatedValue);
} else if (currentTotalAllocatedValue > strategyAllocatedAmount) {
yield = currentTotalAllocatedValue - strategyAllocatedAmount;
} else {
loss = strategyAllocatedAmount - currentTotalAllocatedValue;
}
return (yield, loss, currentTotalAllocatedValue);
}
/**
* @dev Preview management fee accrual.
* @param _lastTotalAssets The total assets deposited in the vault to calculate fee on
* @return feeShares The number of shares to mint as management fee
* @return feeAmount The asset value of the management fee
*/
function previewManagementFee(uint256 _lastTotalAssets)
internal
view
returns (uint256 feeShares, uint256 feeAmount)
{
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
if ($.managementFee == 0) return (0, 0);
uint32 currentTime = Time.timestamp();
uint32 lastAccrual = $.lastManagementFeeAccrual;
if (currentTime == lastAccrual) return (0, 0);
uint256 timeElapsed = currentTime - lastAccrual;
// management fee is calculated on total vault AUM(after yield accrual)
uint256 annualFeeAmount = (_lastTotalAssets * $.managementFee) / ConstantsLib.BASIS_POINTS_DENOMINATOR;
feeAmount = (annualFeeAmount * timeElapsed) / (365 days);
if (feeAmount == 0) return (0, 0);
// sanity check - clamp the fee amount to the last total assets
if (feeAmount > _lastTotalAssets) {
feeAmount = _lastTotalAssets;
}
// convert fee amount to shares using total assets net of the fee to prevent dilution. of the fee amount
feeShares =
feeAmount.calcConvertToShares(totalSupply(), _lastTotalAssets - feeAmount, Math.Rounding.Floor, false);
return (feeShares, feeAmount);
}
/**
* @dev Preview performance fee accrual.
* @param totalAssetsAmount The total assets in the vault to calculate fee on
* @param positiveYield The total positive yield generated by all strategies
* @param loss The total losses incurred by all strategies
* @param totalSupply The current total supply of vault shares
* @return shares The number of shares to mint as performance fee
* @return feeAmount The asset value of the performance fee
*/
function previewPerformanceFee(uint256 totalAssetsAmount, uint256 positiveYield, uint256 loss, uint256 totalSupply)
internal
view
returns (uint256, uint256)
{
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
if ($.performanceFee == 0 || (loss >= positiveYield)) return (0, 0);
uint256 netPositiveYield = positiveYield - loss;
uint256 feeAmount = Math.mulDiv(netPositiveYield, $.performanceFee, ConstantsLib.BASIS_POINTS_DENOMINATOR);
if (feeAmount == 0) return (0, 0);
uint256 feeShares =
feeAmount.calcConvertToShares(totalSupply, totalAssetsAmount - feeAmount, Math.Rounding.Floor, false);
return (feeShares, feeAmount);
}
/**
* @dev Internal function to calculate the maximum amount of assets that can be withdrawn by an owner.
* @dev The calculation considers:
* - Owner's current share balance converted to equivalent assets
* - Current vault liquidity (idle assets + withdrawable amounts from strategies)
* - Strategy withdrawal limitations and availability
* @param owner The address of the account for which to calculate maximum withdrawal.
* @return The maximum amount of assets that can actually be withdrawn by the owner,
* considering both ownership rights and liquidity constraints.
* @return totalAssets The total amount of assets in the vault after previewing the yield accrual
*/
function _maxWithdraw(address owner) internal view virtual returns (uint256, uint256, uint256) {
uint256 ownerShares = balanceOf(owner);
(uint256 totalAssetsPreview, uint256 totalSupplyPreview) = _previewAccrueYieldAndFees();
uint256 maxAssets =
ownerShares.calcConvertToAssets(totalSupplyPreview, totalAssetsPreview, Math.Rounding.Floor, false);
return (_simulateWithdraw(maxAssets), totalAssetsPreview, totalSupplyPreview);
}
/// @dev Simulate withdrawing an amount of assets from the vault.
/// @param requestedAssets Amount of assets to withdraw.
/// @return Amount of assets filled.
function _simulateWithdraw(uint256 requestedAssets) internal view virtual returns (uint256) {
SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
uint256 totalWithdrawableAmount = IERC20(asset()).balanceOf(address(this));
if (totalWithdrawableAmount < requestedAssets) {
address[] memory deallocationOrder = $.deallocationOrder;
uint256 strategiesCounter = deallocationOrder.length;
for (uint256 i; i < strategiesCounter; ++i) {
if (
($.strategyData[deallocationOrder[i]].status != IConcreteStandardVaultImpl.StrategyStatus.Active)
|| !$.strategies.contains(deallocationOrder[i])
) continue;
uint256 desiredAssets;
unchecked {
desiredAssets = requestedAssets - totalWithdrawableAmount;
}
uint256 withdrawableAmountFromStrategy = IStrategyTemplate(deallocationOrder[i]).maxWithdraw();
uint256 withdrawAmount =
(withdrawableAmountFromStrategy >= desiredAssets) ? desiredAssets : withdrawableAmountFromStrategy;
totalWithdrawableAmount += withdrawAmount;
if (totalWithdrawableAmount >= requestedAssets) break;
}
} else {
totalWithdrawableAmount = requestedAssets;
}
return totalWithdrawableAmount;
}
/**
* @dev Internal function used only by the public convertToShares() function.
* @dev This function returns share amounts NOT inclusive of management or performance fees,
* as required by the ERC4626 specification. The convertToShares() function should return
* the theoretical share amount for a given asset amount without considering fee deductions.
* @param assets The amount of assets to convert to shares
* @param rounding The rounding direction for the conversion
* @return The equivalent amount of shares, NOT inclusive of fees
*/
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual override returns (uint256) {
(uint256 totalPositiveYield, uint256 totalNegativeYield) = _previewYieldNoFees();
return assets.calcConvertToShares(
totalSupply(), cachedTotalAssets() + totalPositiveYield - totalNegativeYield, rounding, false
);
}
/**
* @dev Internal function used only by the public convertToAssets() function.
* @dev This function returns asset amounts NOT inclusive of management or performance fees,
* as required by the ERC4626 specification. The convertToAssets() function should return
* the theoretical asset amount for a given share amount without considering fee deductions.
* @param shares The amount of shares to convert to assets
* @param rounding The rounding direction for the conversion
* @return The equivalent amount of assets, NOT inclusive of fees
*/
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) {
(uint256 totalPositiveYield, uint256 totalNegativeYield) = _previewYieldNoFees();
return shares.calcConvertToAssets(
totalSupply(), cachedTotalAssets() + totalPositiveYield - totalNegativeYield, rounding, false
);
}
function _lockedAssets() internal view virtual returns (uint256) {
return 0;
}
}
"
},
"src/interface/IConcreteStandardVaultImpl.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
import {IUpgradeableVault} from "./IUpgradeableVault.sol";
import {Hooks} from "./IHook.sol";
import {IERC4626} from "@openzeppelin-contracts/interfaces/IERC4626.sol";
import {IAccessControlEnumerable} from "@openzeppelin-contracts/access/extensions/IAccessControlEnumerable.sol";
/**
* @title IConcreteStandardVaultImpl
* @dev Interface for the standard vault implementation that manages multiple investment strategies.
* @dev This interface extends the base tokenized vault functionality with strategy management capabilities.
* @dev Strategies are external contracts that implement the IStrategyTemplate interface and handle
* fund allocation to different yield-generating protocols or investment opportunities.
*/
interface IConcreteStandardVaultImpl is IUpgradeableVault, IERC4626, IAccessControlEnumerable {
/**
* @dev Thrown when attempting to withdraw to the zero address.
*/
error InvalidReceiver();
/**
* @dev Thrown when attempting to add a strategy that uses a different asset than the vault.
*/
error InvalidStrategyAsset();
/**
* @dev Thrown when attempting to add a strategy that has already been added to the vault.
*/
error StrategyAlreadyAdded();
/**
* @dev Thrown when attempting to operate on a strategy that doesn't exist in the vault.
*/
error StrategyDoesNotExist();
/**
* @dev Thrown when attempting to interact with a strategy that is halted.
*/
error StrategyIsHalted();
/**
* @dev Thrown when attempting to halt a strategy that is already halted.
*/
error StrategyAlreadyHalted();
/**
* @dev Thrown when attempting to toggle the status of an inactive strategy.
*/
error CannotToggleInactiveStrategy();
/**
* @dev Thrown when attempting to set a management fee without setting a recipient first.
*/
error FeeRecipientNotSet();
/**
* @dev Thrown when attempting to set a management fee that exceeds the maximum allowed.
*/
error ManagementFeeExceedsMaximum();
/**
* @dev Thrown when attempting to set a performance fee that exceeds the maximum allowed.
*/
error PerformanceFeeExceedsMaximum();
/**
* @dev Thrown when attempting to set an invalid fee recipient address (address(0)).
*/
error InvalidFeeRecipient();
/**
* @dev Thrown when the allocate module is invalid.
*/
error InvalidAllocateModule();
/**
* @dev Thrown when the asset is invalid.
*/
error InvalidAsset();
/**
* @dev Thrown when the initial vault manager is invalid.
*/
error InvalidInitialVaultManager();
/**
* @dev Thrown when the name is invalid.
*/
error InvalidName();
/**
* @dev Thrown when the symbol is invalid.
*/
error InvalidSymbol();
/**
* @dev Thrown when the deposit limits are invalid.
*/
error InvalidDepositLimits();
/**
* @dev Thrown when the withdraw limits are invalid.
*/
error InvalidWithdrawLimits();
/**
* @dev Thrown when the asset amount is out of bounds.
*/
error AssetAmountOutOfBounds(address sender, uint256 assets, uint256 minDepositAmount, uint256 maxDepositAmount);
/**
* @dev Thrown when attempting to remove a strategy that has allocation or is in the deallocation order.
*/
error StrategyHasAllocation();
/**
* @dev Thrown when the vault has insufficient balance to process the epoch.
*/
error InsufficientBalance();
/**
* @dev Thrown when calculated shares are zero.
*/
error InsufficientShares();
/**
* @dev Thrown when calculated assets are zero.
*/
error InsufficientAssets();
/**
* @dev Emitted when a new strategy is successfully added to the vault.
* @param strategy The address of the strategy contract that was added.
*/
event StrategyAdded(address strategy);
/**
* @dev Emitted when a strategy is successfully removed from the vault.
* @param strategy The address of the strategy contract that was removed.
*/
event StrategyRemoved(address strategy);
/**
* @dev Emitted when a strategy is set to Halted status.
* @param strategy The address of the strategy contract that was halted.
*/
event StrategyHalted(address strategy);
/**
* @dev Emitted when a strategy's status is toggled between Active and Halted.
* @param strategy The address of the strategy contract whose status was toggled.
*/
event StrategyStatusToggled(address indexed strategy);
/**
* @dev Emitted when the yield accrual operation is completed across all strategies.
*
* @param totalPositiveYield The total amount of positive yield generated across all strategies.
* @param totalNegativeYield The total amount of losses incurred across all strategies.
*/
event YieldAccrued(uint256 totalPositiveYield, uint256 totalNegativeYield);
/**
* @dev Emitted when management fee is accrued.
* @param recipient The address that received the management fee shares.
* @param shares The number of shares minted as management fee.
* @param feeAmount The asset value of the management fee.
*/
event ManagementFeeAccrued(address indexed recipient, uint256 shares, uint256 feeAmount);
/**
* @dev Emitted when performance fee is accrued.
* @param recipient The address that received the performance fee shares.
* @param shares The number of shares minted as performance fee.
* @param feeAmount The asset value of the performance fee.
*/
event PerformanceFeeAccrued(address indexed recipient, uint256 shares, uint256 feeAmount);
/**
* @dev Emitted when management fee is updated.
* @param managementFee The new management fee rate in basis points.
*/
event ManagementFeeUpdated(uint16 managementFee);
/**
* @dev Emitted when management fee recipient is updated.
* @param managementFeeRecipient The new management fee recipient address.
*/
event ManagementFeeRecipientUpdated(address managementFeeRecipient);
/**
* @dev Emitted when performance fee is updated.
* @param performanceFee The new performance fee rate in basis points.
*/
event PerformanceFeeUpdated(uint16 performanceFee);
/**
* @dev Emitted when performance fee recipient is updated.
* @param performanceFeeRecipient The new performance fee recipient address.
*/
event PerformanceFeeRecipientUpdated(address performanceFeeRecipient);
/**
* @dev Emitted when deposit limits are updated.
* @param maxDepositAmount The new maximum deposit amount.
* @param minDepositAmount The new minimum deposit amount.
*/
event DepositLimitsUpdated(uint256 maxDepositAmount, uint256 minDepositAmount);
/**
* @dev Emitted when withdraw limits are updated.
* @param maxWithdrawAmount The new maximum withdraw amount.
* @param minWithdrawAmount The new minimum withdraw amount.
*/
event WithdrawLimitsUpdated(uint256 maxWithdrawAmount, uint256 minWithdrawAmount);
/**
* @dev Emitted when the deallocation order is updated.
*/
event DeallocationOrderUpdated();
/**
* @dev Emitted when an individual strategy's yield is accrued.
*
* @param strategy The address of the strategy contract whose yield was accrued.
* @param currentTotalAllocatedValue The current total allocated value reported by the strategy.
* @param yield The amount of positive yield generated by this strategy since last accrual.
* @param loss The amount of loss incurred by this strategy since last accrual.
*/
event StrategyYieldAccrued(
address indexed strategy, uint256 currentTotalAllocatedValue, uint256 yield, uint256 loss
);
/**
* @dev Enumeration of possible strategy statuses within the vault.
* @dev Inactive: Strategy is inactive and cannot receive new allocations.
* @dev Active: Strategy is active and can receive allocations and process withdrawals normally.
* @dev Halted: Strategy is halted, typically due to detected issues or failures.
* In this state, the strategy can be removed even if it has allocated funds
*/
enum StrategyStatus {
Inactive,
Active,
Halted
}
/**
* @dev Structure containing metadata and state information for each strategy.
* @dev status: Current operational status of the strategy.
* @dev allocated: Total amount of vault assets currently allocated to this strategy, denominated in the vault's underlying asset token.
*/
struct StrategyData {
StrategyStatus status;
uint120 allocated;
}
/**
* @dev Adds a new strategy to the vault.
* @dev The strategy must implement the IStrategyTemplate interface and use the same underlying asset as the vault.
* @dev Only callable by accounts with the STRATEGY_MANAGER role.
*
* @param strategy The address of the strategy contract to add.
*
* Requirements:
* - The strategy's asset() must match the vault's asset()
* - The strategy must not already be added to the vault
* - Caller must have STRATEGY_MANAGER role
*
* Emits:
* - StrategyAdded event
*
* Reverts:
* - InvalidStrategyAsset if strategy uses different asset
* - StrategyAlreadyAdded if strategy is already in the vault
*/
function addStrategy(address strategy) external;
/**
* @dev Removes a strategy from the vault.
* @dev The strategy can only be removed if it has no allocated funds, unless it's in Halted status.
* @dev Only callable by accounts with the STRATEGY_MANAGER role.
*
* @param strategy The address of the strategy contract to remove.
*
* Requirements:
* - Strategy must exist in the vault
* - Strategy must have zero alloca
Submitted on: 2025-11-05 14:32:18
Comments
Log in to comment.
No comments yet.