Component: ConcreteMultiStrategyVault
The ConcreteMultiStrategyVault contract is an ERC-4626--compliant vault that can manage multiple yield-generating strategies. The owner of the vault can add or remove strategies and can set the proportion of the vault's underlying asset allocated to each strategy. Users who deposit into the vault receive shares. The vault offers a one-stop solution for shareholders, enabling automatic yield optimization and liquidity protection.
Deposit
When a user deposits assets into the vault via the function deposit
or the function mint
, the vault mints a certain amount of shares for the user based on the amounts of total assets and total shares. However, if the amount of shares to be minted is less than DUST
, the transaction will be reverted.
If the vault is not idle, the vault will allocate the user's deposited assets to all registered strategies according to the configured allocation ratios. Different strategies handle deposits from the vault in different ways; refer to section ref↗ for more details.
function deposit(uint256 assets_, address receiver_)
// [...]
returns (uint256 shares)
{
// [...]
// Handle strategy allocation if vault is not idle
if (!vaultIdle) {
StrategyHelper.depositIntoStrategies(strategies, assets_, address(this), true);
}
emit Deposit(msg.sender, receiver_, assets_, shares);
}
function depositIntoStrategies(
Strategy[] memory strategies,
uint256 assets_,
address vaultAddress,
bool isRoundingFloor
) external {
// [...]
uint256 len = strategies.length;
for (uint256 i; i < len;) {
// [...]
strategies[i].strategy.deposit(
assets_.mulDiv(strategies[i].allocation.amount, MAX_BASIS_POINTS, rounding), vaultAddress
);
unchecked {
i++;
}
}
}
Withdrawal
Users can choose either the function redeem
or withdraw
to burn shares in exchange for the underlying asset when the withdrawal is not paused. If the amount of shares to be burned is less than DUST
, only the function withdraw
will revert the transaction.
Because the underlying assets are distributed in the vault and various strategies, and different strategies handle the underlying assets in different ways, some underlying assets may not be immediately withdrawable. Therefore, there are two possible scenarios when a user withdraws:
If the total withdrawable assets are sufficient to cover the amount the user wants to withdraw, the vault will first use the assets it holds and then withdraw the remaining amount from the strategies according to the configured allocations, before sending the assets to the receiver.
If the total withdrawable assets are insufficient to cover the amount the user wants to withdraw, the vault will not send any assets to the receiver in this transaction. Instead, the vault will create a withdrawal request and place it into the withdrawal queue. Afterwards, when there are sufficient withdrawable assets, the owner of the vault can claim the withdrawal requests. Except for consuming the requests in the withdrawal queue, the method of transferring assets to the receiver is the same as in the first scenario.
For withdrawals involving assets that need to be withdrawn from strategies, the receiver may receive an amount of assets less than expected. For details, please refer to Finding ref↗.
Reward
The owner of the vault can call the function harvestRewards
to harvest rewards on every strategy. The function harvestRewards
of the strategy transfers the rewards to the vault and returns an array recording the reward-token addresses and the effective amounts sent (excluding the underlying asset). The vault then updates the rewardIndex
for each reward token based on the effective reward amount and the total shares of the vault.
function harvestRewards(bytes calldata encodedData) external nonReentrant onlyOwner {
// [...]
for (uint256 i; i < lenStrategies;) {
// [...]
ReturnedRewards[] memory returnedRewards = strategies[i].strategy.harvestRewards(rewardsData);
lenRewards = returnedRewards.length;
for (uint256 j; j < lenRewards;) {
uint256 amount = returnedRewards[j].rewardAmount;
address rewardToken = returnedRewards[j].rewardAddress;
if (amount != 0) {
if (rewardIndex[rewardToken] == 0) {
rewardAddresses.push(rewardToken);
}
if (totalSupply > 0) {
rewardIndex[rewardToken] += amount.mulDiv(PRECISION, totalSupply, Math.Rounding.Floor);
}
}
// [...]
}
Before each user-balance update, rewards are distributed to the user based on the amount of shares they hold and the difference between their userRewardIndex_
and the current rewardIndex_
for each reward token. After distribution, the userRewardIndex_
is updated to the current vault's rewardIndex_
.
function updateUserRewardsToCurrent(
uint256 userBalance_,
address userAddress_,
address[] memory rewardAddresses_,
mapping(address => uint256) storage rewardIndex_,
mapping(address => mapping(address => uint256)) storage userRewardIndex_,
mapping(address => mapping(address => uint256)) storage totalRewardsClaimed_
) external {
uint256 len = rewardAddresses_.length;
for (uint256 i; i < len;) {
uint256 tokenRewardIndex = rewardIndex_[rewardAddresses_[i]];
uint256 _userRewardIndex = userRewardIndex_[userAddress_][rewardAddresses_[i]];
userRewardIndex_[userAddress_][rewardAddresses_[i]] = tokenRewardIndex;
if (userBalance_ != 0) {
uint256 rewardsToTransfer =
(tokenRewardIndex - _userRewardIndex).mulDiv(userBalance_, PRECISION, Math.Rounding.Floor);
if (rewardsToTransfer != 0) {
TokenHelper.attemptSafeTransfer(
address(rewardAddresses_[i]), userAddress_, rewardsToTransfer, false
);
// [...]
}
}