Liquidity Direction

Fully on chain execution, with rebalancing of strategies across multiple asset classes

Automated liquidity Direction occurs in 3 steps:

  1. The automated script offchain submits the target percentage for each asset class.

  2. The VoteExecutorMaster and StrategyHandler calculates the current TVL and uses these percentages to determine how much each strategy for every asset class should hold.

  3. The VoteExecutorMaster withdraw and deposits to strategies to meet this percentage.

Step 1: Submission of percentages

An example of this is: https://vote.alluo.com/#/proposal/0x1d2bf1a7dbb043f4d4264f29201aa28157cd2d815cc2914f36c1a84423641e85

Where 6.7% goes to stETH-ETH and 93.3% goes to ETH-frxETH.

Onchain, you submit the name of the strategy and the percentage where 10000 is 100%.

(”Curve/Convex stETH-ETH”, 670), (”Curve/FraxConvex ETH-frxETH”, 9330)

Would be an example of how it is parsed.

Step 2: Calculation of current TVL

On each strategy, we use oracles to determine the value of each position held in each strategy and sum it up to grab current TVL of the specific asset class. This is done by calling β€œgetDeployedAmount”

CurveConvexStrategyV2
function getDeployedAmountAndRewards(
        bytes calldata data
    ) external onlyRole(DEFAULT_ADMIN_ROLE) returns (uint256) {
        (
            IERC20 lpToken,
            uint256 convexPoolId,
            uint256 assetId
        ) = decodeRewardsParams(data);

        uint256 lpAmount;
        if (convexPoolId != type(uint256).max) {
            ICvxBaseRewardPool rewards = getCvxRewardPool(convexPoolId);
            lpAmount = rewards.balanceOf(address(this));
            rewards.getReward(address(this), true);
        } else {
            lpAmount = lpToken.balanceOf(address(this));
        }

        (uint256 fiatPrice, uint8 fiatDecimals) = IPriceFeedRouterV2(priceFeed)
            .getPriceOfAmount(address(lpToken), lpAmount, assetId);

        return
            IPriceFeedRouterV2(priceFeed).decimalsConverter(
                fiatPrice,
                fiatDecimals,
                18
            );
}

Then, we calculate the positive or negative 'delta' for each strategy.

VoteExecutorMaster
else if (commandIndex == 2) {
        // Handle all withdrawals first and then add all deposit actions to an array to be executed afterwards
        (uint256 directionId, uint256 percent) = abi.decode(
                messages[j].commandData,
                (uint256, uint256)
                );

        (address strategyPrimaryToken, IStrategyHandler.LiquidityDirection memory direction) = handler.getDirectionFullInfoById(directionId);
            if (direction.chainId == currentChain) {
                console.log("directionId", directionId);
                if (percent > 0) {
                    assetIdToDepositPercentages[direction.assetId].push(
                    Deposit(directionId, percent)
                    );
                }

                if (percent == 0) {
                    console.log("full exit");
                    IAlluoStrategyV2(direction.strategyAddress).exitAll(
                        direction.exitData,
                        10000,
                        strategyPrimaryToken,
                        address(this),
                        false,
                        false
                    );
                    handler.removeFromActiveDirections(directionId);
                } else {
                    uint newAmount = (percent *
                    amountsDeployed[direction.assetId]) / 10000;
                    console.log("new", newAmount);
                    console.log("old", direction.latestAmount);
                    if (newAmount < direction.latestAmount) {
                        uint exitPercent = 10000 -
                            (newAmount * 10000) /
                            direction.latestAmount;
                        IAlluoStrategyV2(direction.strategyAddress)
                            .exitAll(
                                        direction.exitData,
                                        exitPercent,
                                        strategyPrimaryToken,
                                        address(this),
                                        false,
                                        false
                                    );
                            } else if (newAmount != direction.latestAmount) {
                                uint depositAmount = newAmount -
                                    direction.latestAmount;
                                assetIdToDepositList[direction.assetId].push(
                                    Deposit(directionId, depositAmount)
                                );
                            }
                        }
                    }

This calculation is done by doing:

Current TVL of asset class * Allocation % = Target value.

Step 3: Withdraw and deposit to each strategy

Withdrawals happen immediately and deposits are queued because there is a chance of reverting in some edge cases. This is executed through executeDeposits() immediately after the vote is executed. For example, say that we need to deposit 100k in each of pools A,B and C. These are added to an array of deposits that are executed by calling executeDeposits(). In pseudocode: ["Deposit 100k in pool A", "Deposit 100k in pool B", "Deposit 100k in pool C"] Once each direction is actioned, they are removed from the array.

IAlluoStrategyV2(direction.strategyAddress).exitAll(
                 direction.exitData,
                 exitPercent,
                 strategyPrimaryToken,
                  address(this),
                  false,
                  false
            );

function _executeDeposits() internal {
        IPriceFeedRouterV2 feed = IPriceFeedRouterV2(priceFeed);
        IStrategyHandler handler = IStrategyHandler(strategyHandler);
        address exchange = exchangeAddress;
        uint8 numberOfAssets = handler.numberOfAssets();
        for (uint256 i; i < numberOfAssets; i++) {
            Deposit[] storage depositList = assetIdToDepositList[i];
            uint depositListLength = depositList.length;
            address strategyPrimaryToken = handler.getPrimaryTokenByAssetId(
                i,
                1
            );
            uint primaryDecimalsMultiplier = 10 **
                (18 -
                    IERC20MetadataUpgradeable(strategyPrimaryToken).decimals());
            while (depositListLength > 0) {
                Deposit memory depositInfo = depositList[depositListLength - 1];
                console.log("dep to directionId", depositInfo.directionId);
                if (depositInfo.directionId != type(uint).max) {
                    IStrategyHandler.LiquidityDirection
                        memory direction = handler.getLiquidityDirectionById(
                            depositInfo.directionId
                        );
                    (uint256 fiatPrice, uint8 fiatDecimals) = feed.getPrice(
                        strategyPrimaryToken,
                        i
                    );
                    uint exactAmount = (depositInfo.amount *
                        10 ** fiatDecimals) / fiatPrice;
                    console.log("exact", exactAmount);
                    uint256 tokenAmount = exactAmount /
                        primaryDecimalsMultiplier;
                    uint256 actualBalance = IERC20MetadataUpgradeable(
                        strategyPrimaryToken
                    ).balanceOf(address(this));
                    if (depositListLength == 1 && actualBalance < tokenAmount) {
                        uint assetAmount = handler.getAssetAmount(i);
                        uint assetMaxSlippageAmount = assetAmount -
                            ((assetAmount * (10000 - slippage)) / 10000);
                        if (
                            tokenAmount - actualBalance <
                            assetMaxSlippageAmount / primaryDecimalsMultiplier
                        ) {
                            tokenAmount = actualBalance;
                            console.log("changed", tokenAmount);
                        } else {
                            revert("slippage");
                        }
                    }
                    if (direction.entryToken != strategyPrimaryToken) {
                        IERC20MetadataUpgradeable(strategyPrimaryToken)
                            .safeApprove(exchange, tokenAmount);
                        tokenAmount = IExchange(exchange).exchange(
                            strategyPrimaryToken,
                            direction.entryToken,
                            tokenAmount,
                            0
                        );
                    }
                    IERC20MetadataUpgradeable(direction.entryToken)
                        .safeTransfer(direction.strategyAddress, tokenAmount);
                    IAlluoStrategyV2(direction.strategyAddress).invest(
                        direction.entryData,
                        tokenAmount
                    );
                    handler.addToActiveDirections(depositInfo.directionId);
                } else {
                    (uint256 fiatPrice, uint8 fiatDecimals) = feed.getPrice(
                        strategyPrimaryToken,
                        i
                    );
                    uint exactAmount = (depositInfo.amount *
                        10 ** fiatDecimals) / fiatPrice;
                    uint256 tokenAmount = exactAmount /
                        primaryDecimalsMultiplier;

                    IERC20MetadataUpgradeable(strategyPrimaryToken)
                        .safeTransfer(gnosis, tokenAmount);
                }
                depositList.pop();
                depositListLength--;
            }
        }
        handler.calculateOnlyLp();
}

Last updated