Rebalance

Auction mechanism design

To remain in the profitability range, the strategy performs so-called rebalance auctions - a procedure, during which the strategy contract adjusts its token holding proportion to bring back the desired price exposure.

There a 2 types of rebalance auctions:

  1. Time rebalances - periodical tokens adjustments based on the rebalance time threshold.

  2. Price rebalances - token adjustments to cut the losses in a case of huge price change between the regular time rebalances

Both types have the same auction design, the only difference between them is trigger conditions. Time rebalance auction activates when enough time was elapsed since the previous rebalance (for example, 12h), while the Price rebalances auction activates on huge drops in ETH price (for example, -7%).

Mechanism

To handle rebalance procedure, we will use a value-based Dutch auction, where amounts that need to be exchanged with the bidder are calculated based on the total ETH value (NAV) held by the strategy before and after the rebalance procedure.

Rebalance

The flow is next:

  1. The keeper calls one of the timeRebalance, priceRebalance functions

  2. Contract checks for the rebalance conditions (whether enough time has passed or price deviation is large enough)

  3. Withdraw all the liquidity from Uniswap pools

  4. Calculate the auction parameters

  5. Exchange with the keeper the current set of tokens for the desired one to achieve the target price exposure

  6. Mint new liquidity positions

  7. Record snapshot

1) Price Multiplier

The first step is to calculate the price multiplier based on the auction completion ratio (the time when the bidder has called the rebalance function).

There is an auction time of 10 min during which the keeper can call the rebalance function.

function getPriceMultiplier(uint256 _auctionTriggerTime) external view override
    returns (uint256)
{
    uint256 maxPriceMultiplier = IVaultStorage(vaultStorage).maxPriceMultiplier();
    uint256 minPriceMultiplier = IVaultStorage(vaultStorage).minPriceMultiplier();
    uint256 auctionTime = IVaultStorage(vaultStorage).auctionTime();

        uint256 auctionCompletionRatio = block.timestamp.sub(_auctionTriggerTime) >= auctionTime
        ? 1e18
        : (block.timestamp.sub(_auctionTriggerTime)).div(auctionTime);

        return maxPriceMultiplier.sub(auctionCompletionRatio.mul(maxPriceMultiplier.sub(minPriceMultiplier)));
}
auctionCompletionRatio=block.timestampāˆ’auctionTriggerTimeauctionTimeĀ pm=minPM+auctionCompletionRatioā‹…(maxPMāˆ’minPM)auctionCompletionRatio=\frac{block.timestamp-auctionTriggerTime}{auctionTime} \\ \ \\ pm=minPM+auctionCompletionRatio\cdot(maxPM-minPM)

Where auctionTriggerTime is a timestamp at which the bidder has called the rebalance function, while minPriceMultiplier and maxPriceMultiplier is min = 0.95 and max = 1.05 price multipliers.

So at the beginning of the auction, when the completion ratio will be 0, the price multiplier will be equal to 1.05, while at the end of the auction the multiplier will be equal to 0.95.

In the case that there are no bids during the auction period, the price multiplier will remain at the minimum levels till the execution call by some keeper.

2) Auction prices

The next step is to calculate the auction prices aEthUsdc and aOsqthEth for ETH/USDC and oSQTH/ETH respectively.

aEthUsdc=pmā‹…EthUsdcĀ aOsqthEth=pmā‹…OsqthEthaEthUsdc = pm \cdot EthUsdc \newline \ \newline aOsqthEth = pm \cdot OsqthEth

3) Total Value

Then, we calculate the ETH value that we will use to provide liquidity on Uniswap, as the current ETH value of tokens held by the strategy, calculated at the current prices:

V=pmā‹…(ETH+oSQTHā‹…OsqthEth+USDCEthUsdc)V=pm\cdot(ETH+oSQTH\cdot OsqthEth+\frac{USDC}{EthUsdc})

In order to make sure, that even in the extreme market (or liquidity) conditions there is enough incentive for the keeper to rebalance, we discount total value by the current price multiplier. So there will be up to 4% arbitrage profit.

ā€‹Where, ETH, oSQTH, and USDC are the current amount of tokens, while OsqthEth and EthUsdc are the current market prices of the correspondent tokens.

3) Weight

The next step is weight - a coefficient that is needed to achieve target price exposure at auction prices.

The problem here is that the current prices in pools are different from the auction prices around which we would like our liquidity. So, if we will just naively place the liquidity in a 50:50 proportion, or even after adjusting for the IV, we will get the incorrect value split between the ETH-USDC and oSQTH-ETH LP pools.

Thus, we need again to adjust the value split to that one that will provide us with the target price exposure after the rebalance auction.

wpos=pm1+pm+0.01IVĀ wneg=pm1+pmāˆ’0.01IVw^{pos}=\frac{pm}{1+pm}+\frac{0.01}{IV} \newline \ \newline w^{neg}=\frac{pm}{1+pm}-\frac{0.01}{IV}

Where wposw^{pos} and wnegw^{neg} are weights for the rebalance auction when we expect a positive or negative IV bump respectively.

So, the values for LP will be:

VEthUsdc=wā‹…VĀ VOsqthEth=(1āˆ’w)ā‹…VV_{EthUsdc}=w\cdot V \newline \ \newline V_{OsqthEth} = (1-w)\cdot V

3)ā€‹ Implied Volatility bump

Depending on whether or not we observe a positive or negative IV bump, we calculate the expected IV bump over the next period as:

//current implied volatility
uint256 cIV = IVaultMath(vaultMath).getIV();

//previous implied volatility
uint256 pIV = IVaultStorage(vaultStorage).ivAtLastRebalance();

//is a positive IV bump
bool isPosIVbump = cIV < pIV;

uint256 expIVbump;
if (isPosIVbump) {
    expIVbump = pIV.div(cIV);
    } else {
    expIVbump = cIV.div(pIV);
    }
 expIVbump = expIVbump > uint256(2e18) ? uint256(2e18) : (expIVbump.mul(2e18)).sub(2e18);

With the additional check on its value, because the value of expIVbump > 2 will lead to the negative value of one of the lower or upper boundaries.

4) Boundaries

Based on these auction prices calculate the boundaries of liquidity placement

EthUsdcLower=1e121.0001ln1e12EthUsdcln1.0001+TS+baseThreshold+tickAdjĀ EthUsdcUpper=1e121.0001ln1e12EthUsdcln1.0001āˆ’baseThreshold+tickAdjEthUsdcLower = \frac{1e12}{1.0001^{\frac{ln{\frac{1e12}{EthUsdc}}}{ln{1.0001}}+TS+baseThreshold+tickAdj}} \newline \ \newline EthUsdcUpper = \frac{1e12}{1.0001^{\frac{ln{\frac{1e12}{EthUsdc}}}{ln{1.0001}}-baseThreshold+tickAdj}}

Similarly, for the oSQTH-ETH pool:

OsqthEthLower=11.0001āˆ’lnOsqthEthln1.0001+TS+baseThreshold+tickAdjĀ OsqthEthUpper=11.0001āˆ’lnOsqthEthln1.0001āˆ’baseThreshold+tickAdjOsqthEthLower = \frac{1}{1.0001^{-\frac{ln{OsqthEth}}{ln{}1.0001}+TS+baseThreshold+tickAdj}} \newline \ \newline OsqthEthUpper = \frac{1}{1.0001^{-\frac{ln{OsqthEth}}{ln{}1.0001}-baseThreshold+tickAdj}}

Where baseThreshold is the parameter that defines the base size of the boundaries and tickAdj is boundaries adjustment, which depends on whether we expect a positive or negative IV bump and its amplitude.

baseAdj=expIVbumpadjParamā‹…tickSpacingĀ tickAdj=ifelse(baseAdj<120,60,baseAdj)Ā tickAdj=ifelse(isPosIVbump,tickAdj,āˆ’tickAdj)baseAdj = \frac{expIVbump}{adjParam}\cdot tickSpacing \\ \ \\ tickAdj = ifelse(baseAdj<120, 60, baseAdj) \\ \ \\ tickAdj = ifelse(isPosIVbump, tickAdj, -tickAdj)
int24 tickAdj;
    {
        int24 baseAdj = toInt24(
        int256(
            ((expIVbump.div(IVaultStorage(vaultStorage).adjParam())).floor() *
            uint256(int256(tickSpacing))).div(1e36)
            ));
        tickAdj = baseAdj < int24(120) ? int24(60) : baseAdj;
    }

5) Liquidities

Based on the total LP value calculate the desired liquidities:

LEthUsdc=VEthUsdcā‹…EthUsdc2aEthUsdcāˆ’EthUsdcLowerāˆ’aEthUsdcEthUsdcUpperL_{EthUsdc}=\frac{V_{EthUsdc}\cdot EthUsdc}{2\sqrt{aEthUsdc} - \sqrt{EthUsdcLower} - \frac{aEthUsdc}{\sqrt{EthUsdcUpper}}}
LOsqthEth=VOsqthEth2aOsqthEthāˆ’OsqthEthLowerāˆ’aOsqthEthOsqthEthUpperL_{OsqthEth} = \frac{V_{OsqthEth}}{2\sqrt{aOsqthEth}-\sqrt{OsqthEthLower}-\frac{aOsqthEth}{\sqrt{OsqthEthUpper}}}

Note, that we calculate liquidities around the auction prices aEthUsdcaEthUsdc and aOsqthEthaOsqthEth, rather than around the current prices.ā€‹

7) Amount of tokens

Now, we can get the required amount of tokens that need to be provided to mint desired amount of liquidity:

amountEth0=LEthUsdcā‹…aEthUsdcUpperāˆ’EthUsdcaEthUsdcUpperā‹…EthUsdcĀ amountUsdc=LEthUsdcā‹…(EthUsdcāˆ’aEthUsdcLower)Ā amountEth1=LOsqthEthā‹…(OsqthEthāˆ’aOsqthEthLower)Ā amountOsqth=LOsqthEthā‹…aOsqthEthUpperāˆ’OsqthEthaOsqthEthUpperā‹…OsqthEthamountEth0 = L_{EthUsdc}\cdot \frac{\sqrt{aEthUsdcUpper}-\sqrt{EthUsdc}}{\sqrt{aEthUsdcUpper \cdot EthUsdc}} \newline \ \newline amountUsdc = L_{EthUsdc} \cdot (\sqrt{EthUsdc}-\sqrt{aEthUsdcLower}) \newline \ \newline amountEth1 = L_{OsqthEth}\cdot (\sqrt{OsqthEth}-\sqrt{aOsqthEthLower}) \newline \ \newline amountOsqth = L_{OsqthEth}\cdot \frac{\sqrt{aOsqthEthUpper}-\sqrt{OsqthEth}}{\sqrt{aOsqthEthUpper\cdot OsqthEth}}

The Deltas (amount of tokens) that need to be exchanged with the keeper:

Ī”Eth=amountEth0+amountEth1āˆ’balanceEthĀ Ī”Usdc=amountUsdcāˆ’balanceUsdcĀ Ī”Osqth=amountOsqthāˆ’balanceOsqth\Delta Eth = amountEth0 +amountEth1-balanceEth \newline \ \newline \Delta Usdc = amountUsdc-balanceUsdc \newline \ \newline \Delta Osqth = amount Osqth - balanceOsqth

Where balanceEth, balanceUsdc, and balanceOsqth are the current amounts of assets held by the strategy.

Negative deltas mean that the strategy gives these tokens in exchange for the requested one (with a positive sign).

Last updated