Automatic boosting with Alluo

How exactly does this looping process work?

Alluo stakes underlying LP deposits into the Vault and carries out all the looping of rewards:

Using the rewards from the underlying LP deposit in the Alluo Vault, on a regular interval, Alluo swaps these rewards to CVX then adds liquidity to the Curve Pool for CVX-ETH LP, then finally stakes these CVX-ETH LPs to boost your yield.

Alluo's gelato resolver calls these two functions regularly:

1. StakeUnderlying()

This function is called so that any accumulated LP deposits are staked often into Convex so that we do not have any idle capital.

This is lowers the gas costs / upfront costs to investment for users as they do not need to pay extra gas fees and it is economically optimal for the community as whether you stake 10 tokens or 100000 tokens costs almost the same gas.

2. farm() in the Alluo Boosting Pool

This function claims all rewards accumulated in the boosted rewards pool, all the rewards in each vault connected to it and then appropriately distributes CVX-ETH lp tokens amongst the vaults

This comes as the vault having 'shares' in the boosted pool so that users are distributed rewards correctly. As multiple vault rewards are simultaneously converted to CVX-ETH LP tokens and then staked into convex in one transaction, this creates massive gas savings so users can enjoy economies of scale.

AlluoVaultPool.sol
 /// @notice Claims all rewards, exchange all rewards for LPs and stake them
/// @dev Exchanges all rewards (including those sent by the vault) for the entryToken, adds liquidity for LP tokens and then stakes them
///      This function is not to be called directly, but rather through the vault contract it is linked to.
function farm() onlyRole(DEFAULT_ADMIN_ROLE) external {
    // 1. Claim all rewards accumulated by booster pool and convert to the entryToken
    claimRewardsFromPool();
    for (uint256 i; i < yieldTokens.length(); i++) {
        address token = yieldTokens.at(i);
        uint256 balance = IERC20MetadataUpgradeable(token).balanceOf(address(this));
        if (token != address(entryToken) && balance > 0) {
            IERC20MetadataUpgradeable(token).safeIncreaseAllowance(address(exchange), balance);
            exchange.exchange(token, address(entryToken), balance, 0);
        }
    }
    uint256 totalPoolEntryTokenYield = entryToken.balanceOf(address(this));

    // 2. Get all the rewards  from the different vaults and keep track of how much entryToken it is worth for each vault
    uint256 totalVaultEntryTokenDeposits;
    uint256[] memory entryTokenDeposits = new uint256[](vaults.length());
    for (uint256 i; i < vaults.length(); i++) {
        address _vault = vaults.at(i);
        uint256 vaultEntryTokenBalance = IAlluoVault(_vault).claimAndConvertToPoolEntryToken(address(entryToken));
        totalVaultEntryTokenDeposits += vaultEntryTokenBalance;
        entryTokenDeposits[i] = vaultEntryTokenBalance;
    }
    // 3. Convert all entryToken balance (rewards by booster + rewards from vault) into reward tokens and then stake into convex
    uint256 entryTokenBalance = entryToken.balanceOf(address(this));
    uint256 newRewardTokens;
    if (entryTokenBalance > 0) {
        entryToken.safeIncreaseAllowance(address(exchange), entryTokenBalance);
        newRewardTokens = exchange.exchange(address(entryToken), address(rewardToken), entryTokenBalance, 0);
        rewardToken.safeIncreaseAllowance(address(cvxBooster), newRewardTokens);
    }

    // 4. Now give shares of the pool to the vaults which deposited entryToken by calculating how much of they own of the rewardToken LP amount that was created
    // 5. Update all vault holder reward balances

    uint256 totalVaultNewRewardTokens = newRewardTokens * totalVaultEntryTokenDeposits / entryTokenBalance;
    uint256 totalPoolShareholdersNewRewardTokens = newRewardTokens * totalPoolEntryTokenYield / entryTokenBalance;

    uint256 totalSharesBefore = totalBalances;
    for (uint256 j; j < vaults.length(); j++) {
        address _vault = vaults.at(j);
        uint256 shareOfRewardTokens = totalVaultNewRewardTokens * entryTokenDeposits[j] / totalVaultEntryTokenDeposits;
        uint256 additionalSharesOfVault = _convertToSharesAfterPoolRewards(shareOfRewardTokens, totalPoolShareholdersNewRewardTokens, totalSharesBefore);
        balances[_vault] += additionalSharesOfVault;
        totalBalances += additionalSharesOfVault;
        // 5. Update all vault holder reward balances
    }
    cvxBooster.deposit(poolId, newRewardTokens, true);
    for (uint256 j; j < vaults.length(); j++) {
        address _vault = vaults.at(j);
        IAlluoVault(_vault).loopRewards();
    }
}

The farm function simply calls these two functions below where rewards are converted to a central entryToken (usually CVX), then correctly notes down vault rewards (which are calculated by the shares the vault has in the boosted pool) before and after so that they can be distributed between vault shareholders.

AlluoVaultUpgradeable.sol
/// @notice Loop called periodically to compound reward tokens into the respective alluo pool
/// @dev Claims rewards, transfers all rewards to the alluoPool. Then, the pool is farmed and rewards are credited accordingly per share.
function loopRewards() external onlyRole(DEFAULT_ADMIN_ROLE) {
    // The vaultRewardsBefore is set in the same call right before the rewardToken balance is increased to allow for this calculation to work.
    uint256 vaultRewardAfter = IAlluoPool(alluoPool).rewardTokenBalance();
    uint256 totalRewards = vaultRewardAfter - vaultRewardsBefore;
    if (totalRewards > 0) {
        uint256 totalFees = totalRewards * adminFee / 10**4;
        uint256 newRewards = totalRewards - totalFees;
        rewards[gnosis] += totalFees;
        rewardsPerShareAccumulated += newRewards * 10**18 / totalSupply();
    }
}

function claimAndConvertToPoolEntryToken(address entryToken) external onlyRole(DEFAULT_ADMIN_ROLE) returns (uint256) {
    claimRewardsFromPool();
    for (uint256 i; i < yieldTokens.length(); i++) {
        address token = yieldTokens.at(i);
        uint256 balance = IERC20MetadataUpgradeable(token).balanceOf(address(this));
        if (token != address(entryToken) && balance > 0) {
            IERC20MetadataUpgradeable(token).safeIncreaseAllowance(address(exchange), balance);
            exchange.exchange(token, address(entryToken), balance, 0);
        }
    }
    vaultRewardsBefore = IAlluoPool(alluoPool).rewardTokenBalance();
    uint256 amount = IERC20MetadataUpgradeable(entryToken).balanceOf(address(this));
    IERC20MetadataUpgradeable(entryToken).safeTransfer(alluoPool, amount);
    return amount;
}

The farm function is permissioned to only work with Gelato to prevent potential flashloan attacks where a malicious actor flash mints shares in the vault, claims all the rewards and then repays the loan.

Last updated