Skip to content
This repository has been archived by the owner on Jan 18, 2023. It is now read-only.

Strategies

bweick edited this page Jul 15, 2019 · 1 revision

Strategies

Manager contracts encode the logic for determining new allocations. Each manager has its own quirks based on the assets and strategies involved but in general maintain a similar proposal process. The steps found in each proposal process can be found below, for some Managers certain parts will be repeated, the math may vary, or additional checks may be required, you can find those deviations illuminated in each Manager's section.

General Proposal Implementation

  1. Check Rebalancing Set conditions: A user calls the propose function on the Manager with the address of the associated Rebalancing Set Token. The Manager validates 1) the rebalanceInterval has elapsed since the last rebalance and 2) that the rebalancing Set address is tracked by Core.

  2. Query data from oracles: The Manager pulls the most current price information (represented as an 18 decimal unsigned integer) from its associated oracles.

  3. Verify price triggers met: Once the price data has been received, a series of calculations are run to verify that the percent allocations of the current Set are outside the allocation bounds.

  4. Determine next allocation: Following the allocation verification, the nextSet is created by determining the units and naturalUnit (for more on unit and naturalUnits see our white paper) required to meet the target allocation. Once this math is completed, the nextSet address to rebalance into is created by calling Core’s external createSet function with the SetTokenFactory address.

  5. Determine proposal parameters: The Manager next calculates the auction price parameters. The auctionTimeToPivot is hard-coded into the contract upon instantiation. The auctionStartPrice and the auctionPivotPrice are determined as a function of the “fair price” of the auction and the auctionTimeToPivot. fairValue is the ratio of the total cost of the nextSet to the total cost of the currentSet. If the nextSet is worth $1,500 and the currentSet SetToken is worth $1,200, then fairValue = 1500 / 1200 = 1.25.

    To maintain granularity a priceDenominator from the LinearAuctionPriceCurve, is applied to the fairValue, thus if the priceDenominator is 1000, fairValue = 1.25 * 1000 = 1250.

    In order to aid price discovery there must be ample granularity in auction prices. Most Managers target 30 minutes spent within every 1% range. Using this target the total price range of the auction is calculated with the formula:

    priceRange = (auctionTimeToPivot/THIRTY_MINUTES_IN_SECONDS) * (fairValue/100)

    By placing fairValue at the center of the range, the auctionStartPrice and the auctionPivotPrice can be calculated. With the returned address from the nextSet creation, all the parameters can be are gathered for the proposal.

BTCETHRebalancingManager

The BTCETHRebalancingManager contains logic for managing rebalances of Sets containing BTC and ETH. These Sets can be used to implement any buy/sell strategy with the two but generally are used for Buy and Hold strategies. Its functions are as follows:

constructor(see params below)

Property Type Description
_coreAddress address Address of Core in the Set Protocol system.
_btcPriceFeedAddress address Address of the MakerDAO Bitcoin-USD medianizer contract. Data from this feed will be used to create the nextSet address, and calculate proposal parameters.
_ethPriceFeedAddress address Address of the MakerDAO Ether-USD medianizer contract. Data from this feed will be used to create the nextSet address, and calculate proposal parameters.
_btcAddress address Address of Wrapped Bitcoin, an ERC20 compliant Bitcoin token, used to create the nextSet during manager proposals.
_ethAddress address Address of Wrapped Ethereum, an ERC20 compliant Ether token, used to create the nextSet during manager proposals.
_setTokenFactory address Address of the SetTokenFactory tracked by Core, which will be used to deploy the nextSet address.
_auctionLibrary address Address of the auction price library to be used for Dutch Auction pricing. The Auction Library must be tracked by Core.
_auctionTimeToPivot uint256 The time in seconds after the auction start time in which the price pivots from a Linear to Exponential curve. The time to pivot must be within the range specified on the Rebalancing Set Token's associated Rebalancing Set Token factory.
_multipliers uint256[2] Token multipliers used to determine relative allocations between Bitcoin and Ether. Target allocation is determined by a simple equation, btcAllocation = btcMultiplier / (btcMultiplier + ethMultiplier). The input should correspond to [btcMultiplier, ethMultiplier].
_allocationBounds uint256[2] Minimum percent deviation from expected allocation before a proposal can be triggered. The input should correspond to [lowerBound, upperBound].

propose(address _rebalancingSetTokenAddress): The implementation for propose is as follows:

  1. Check Rebalancing Set conditions: Proceeds as outlined in the above section.

  2. Query data from oracles: In this case we query the MakerDAO WBTC and WETH oracles.

  3. Verify Price Triggers Met: For the BTCETHRebalancingManager the current USD allocation of BTC is calculated. If the BTC allocation is outside of the allocationBounds defined on contract creation then the rebalance moves forward.

  4. Determine next allocation: Following the allocation verification, the nextSet is created by determining the units required to meet the target allocation. In order to simplify the math, the nextSet targets a USD value equal to:

    (ethMultiplier + btcMultiplier) * max(ETH price, BTC price)

    The units of the more expensive token (“maxToken”) is set to 1 * assetMultiplier and the other token (“minToken”) units is calculated as the exchange rate of (maxToken/minToken) * assetMultiplier. Additionally, a DECIMAL_DIFFERENCE_MULTIPLIER must be applied to the ETH units due to the difference in token decimals between WETH (18) and WBTC (8). The nextSet uses 10 ** 10 for a natural unit.

    In the event that ETH’s price exceeds BTC, a price precision constant (PRICE_PRECISION) of 100 is multiplied to both units so that there are not big rounding errors for the BTC units. Additionally, the naturalUnit of the resulting nextSet is bumped to 10 ** 12 in order to keep it’s dollar valuation close to the currentSet.

         uint256 nextSetNaturalUnit;
         uint256[] memory nextSetUnits = new uint256[](2);      
         if (_btcPrice >= _ethPrice) {
             // Calculate ethereum nextSetUnits, determined by the following equation:
             // (btcPrice / ethPrice) * (10 ** (ethDecimal - btcDecimal))
             uint256 ethUnits = _btcPrice.mul(DECIMAL_DIFF_MULTIPLIER).div(_ethPrice);
    
             // Create unit array and define natural unit
             nextSetUnits[0] = btcMultiplier;
             nextSetUnits[1] = ethUnits.mul(ethMultiplier);
             nextSetNaturalUnit = 10 ** 10;
         } else {
             // Calculate btc nextSetUnits as (ethPrice / btcPrice) * 100. 100 is used to add
             // precision. The increase in unit amounts is offset by increasing the
             // nextSetNaturalUnit by two orders of magnitude so that issuance cost is still
             // roughly the same
             uint256 ethBtcPrice = _ethPrice.mul(PRICE_PRECISION).div(_btcPrice);
    
             // Create unit array and define natural unit
             nextSetUnits[0] = ethBtcPrice.mul(btcMultiplier);
             nextSetUnits[1] = PRICE_PRECISION.mul(DECIMAL_DIFF_MULTIPLIER).mul(ethMultiplier);
             nextSetNaturalUnit = 10 ** 12;
         }
    

    Once this math is completed, the nextSet address to rebalance into is created by calling Core’s external createSet function with the SetTokenFactory address.

  5. Determine Proposal Parameters: Proceeds as outlined in the above section.

BTCDAIRebalancingManager

The BTCDAIRebalancingManager contains logic for managing rebalances of Sets containing BTC and DAI. These Sets can be used to implement any buy/sell strategy with the two but generally are used for Range Bound strategies. Its functions are as follows:

constructor(see below for params):

Property Type Description
_coreAddress address Address of Core in the Set Protocol system.
_btcPriceFeedAddress address Address of the MakerDAO Bitcoin-USD medianizer contract. Data from this feed will be used to create the nextSet address, and calculate proposal parameters.
_daiAddress address Address of Dai, an ERC20 compliant stable token, used to create the nextSet during manager proposals.
_btcAddress address Address of Wrapped Bitcoin, an ERC20 compliant Bitcoin token, used to create the nextSet during manager proposals.
_setTokenFactory address Address of the SetTokenFactory tracked by Core, which will be used to deploy the nextSet address.
_auctionLibrary address Address of the auction price library to be used for Dutch Auction pricing. The Auction Library must be tracked by Core.
_auctionTimeToPivot uint256 The time in seconds after the auction start time in which the price pivots from a Linear to Exponential curve. The time to pivot must be within the range specified on the Rebalancing Set Token's associated Rebalancing Set Token factory.
_multipliers uint256[2] Token multipliers used to determine relative allocations between Bitcoin and Ether. Target allocation is determined by a simple equation, daiAllocation = daiMultiplier / (btcMultiplier + daiMultiplier). The input should correspond to [daiMultiplier, btcMultiplier].
_allocationBounds uint256[2] Minimum percent deviation from expected allocation before a proposal can be triggered. The input should correspond to [lowerBound, upperBound].

propose(address _rebalancingSetTokenAddress): The implementation for propose is as follows:

  1. Check Rebalancing Set conditions: Proceeds as outlined in the above section.

  2. Query data from oracles: In this case we query the MakerDAO WBTC oracle. Dai is fixed at $1 (represented by a constant 10 ** 18).

  3. Verify Price Triggers Met: For the BTCDAIRebalancingManager the current USD allocation of Dai is calculated. If the Dai allocation is outside of the allocationBounds defined on contract creation then the rebalance moves forward. (Note: This means that the allocation lowerBound represents the required BTC price increase to rebalance).

  4. Determine next allocation: Following the allocation verification, the nextSet is created by determining the units required to meet the target allocation. In order to simplify the math, the nextSet targets a USD value equal to:

    (daiMultiplier + btcMultiplier) * max(Dai price, BTC price)

    The units of the more expensive token (“maxToken”) is set to 1 * assetMultiplier * PRICE_PRECISION and the other token (“minToken”) units is calculated as the exchange rate of (maxToken/minToken) * assetMultiplier * PRICE_PRECISION (Note: PRICE_PRECISION is hard-coded to 1). Additionally, a DECIMAL_DIFFERENCE_MULTIPLIER must be applied to the Dai units due to the difference in token decimals between Dai (18) and WBTC (8). The nextSet uses 10 ** 10 for a natural unit.

         uint256[] memory nextSetUnits = new uint256[](2);
         uint256 nextSetNaturalUnit = DECIMAL_DIFF_MULTIPLIER.mul(PRICE_PRECISION);
         if (_btcPrice >= DAI_PRICE) {
             // Dai nextSetUnits is equal the USD Bitcoin price
             uint256 daiUnits = _btcPrice.mul(DECIMAL_DIFF_MULTIPLIER).div(DAI_PRICE);
    
             // Create unit array and define natural unit
             nextSetUnits[0] = daiUnits.mul(daiMultiplier).mul(PRICE_PRECISION);
             nextSetUnits[1] = btcMultiplier.mul(PRICE_PRECISION);          
         } else {
             // Calculate btc nextSetUnits as (daiPrice/btcPrice)*100. 100 is used to add 
             // precision.
             uint256 btcDaiPrice = DAI_PRICE.mul(PRICE_PRECISION).div(_btcPrice);
    
             // Create unit array and define natural unit
             nextSetUnits[0] = daiMultiplier.mul(DECIMAL_DIFF_MULTIPLIER).mul(PRICE_PRECISION); 
             nextSetUnits[1] = btcDaiPrice.mul(btcMultiplier);        
         }
    

    Once this math is completed, the nextSet address to rebalance into is created by calling Core’s external createSet function with the SetTokenFactory address.

  5. Determine Proposal Parameters: Proceeds as outlined in the above section.

ETHDAIRebalancingManager

The ETHDAIRebalancingManager contains logic for managing rebalances of Sets containing ETH and DAI. These Sets can be used to implement any buy/sell strategy with the two but generally are used for Range Bound strategies. Its functions are as follows:

constructor(see below for params):

Property Type Description
_coreAddress address Address of Core in the Set Protocol system.
_ethPriceFeedAddress address Address of the MakerDAO Ethereum-USD medianizer contract. Data from this feed will be used to create the nextSet address, and calculate proposal parameters.
_daiAddress address Address of Dai, an ERC20 compliant stable token, used to create the nextSet during manager proposals.
_ethAddress address Address of Wrapped Ethereum, an ERC20 compliant Ethereum token, used to create the nextSet during manager proposals.
_setTokenFactory address Address of the SetTokenFactory tracked by Core, which will be used to deploy the nextSet address.
_auctionLibrary address Address of the auction price library to be used for Dutch Auction pricing. The Auction Library must be tracked by Core.
_auctionTimeToPivot uint256 The time in seconds after the auction start time in which the price pivots from a Linear to Exponential curve. The time to pivot must be within the range specified on the Rebalancing Set Token's associated Rebalancing Set Token factory.
_multipliers uint256[2] Token multipliers used to determine relative allocations between Bitcoin and Ether. Target allocation is determined by a simple equation, daiAllocation = daiMultiplier / (ethMultiplier + daiMultiplier). The input should correspond to [daiMultiplier, ethMultiplier].
_allocationBounds uint256[2] Minimum percent deviation from expected allocation before a proposal can be triggered. The input should correspond to [lowerBound, upperBound].

propose(address _rebalancingSetTokenAddress): The implementation for propose is as follows:

  1. Check Rebalancing Set conditions: Proceeds as outlined in the above section.

  2. Query data from oracles: In this case we query the MakerDAO WETH oracle. Dai is fixed at $1 (represented by a constant 10 ** 18).

  3. Verify Price Triggers Met: For the ETHDAIRebalancingManager the current USD allocation of Dai is calculated. If the Dai allocation is outside of the allocationBounds defined on contract creation then the rebalance moves forward. (Note: This means that the allocation lowerBound represents the required ETH price increase to rebalance).

  4. Determine next allocation: Following the allocation verification, the nextSet is created by determining the units required to meet the target allocation. In order to simplify the math, the nextSet targets a USD value equal to:

    (daiMultiplier + ethMultiplier) * max(Dai price, ETH price)

    The units of the more expensive token (“maxToken”) is set to 1 * assetMultiplier * PRICE_PRECISION and the other token (“minToken”) units is calculated as the exchange rate of (maxToken/minToken) * assetMultiplier * PRICE_PRECISION (Note: PRICE_PRECISION is hard-coded to 100). The nextSet uses 10 ** 10 for a natural unit.

         uint256[] memory nextSetUnits = new uint256[](2);
         uint256 nextSetNaturalUnit = PRICE_PRECISION;
    
         if (_ethPrice >= DAI_PRICE) {
             // Dai nextSetUnits is equal the USD Ethereum price
             uint256 daiUnits = _ethPrice.mul(PRICE_PRECISION).div(DAI_PRICE);
    
             // Create unit array and define natural unit
             nextSetUnits[0] = daiUnits.mul(daiMultiplier);
             nextSetUnits[1] = ethMultiplier.mul(PRICE_PRECISION);          
         } else {
             // Calculate dai nextSetUnits as (daiPrice/ethPrice)*100. 100 is used to add 
             // precision.
             uint256 ethDaiPrice = DAI_PRICE.mul(PRICE_PRECISION).div(_ethPrice);
    
             // Create unit array and define natural unit
             nextSetUnits[0] = daiMultiplier.mul(PRICE_PRECISION); 
             nextSetUnits[1] = ethDaiPrice.mul(ethMultiplier);         
         }
    

    Once this math is completed, the nextSet address to rebalance into is created by calling Core’s external createSet function with the SetTokenFactory address.

  5. Determine Proposal Parameters: Proceeds as outlined in the above section.

MACOStrategyManager

The MACOStrategyManager contains logic to execute a moving average crossover (MACO) strategy for any asset. The strategy flips entirely between a volatile asset and a stable asset unlike either ETHDAI or BTCDAI which only reallocate back to a set percentage. Additionally, the proposal process requires two function calls instead of the usual one in order for the signal to be confirmed. Consequently, there will be additional logic required to execute proposals.

** constructor(see below for params):**

Property Type Description
_coreAddress address Address of Core in the Set Protocol system.
_movingAveragePriceFeed address Address of the MovingAverageOracle defined in the previous section. The underlying asset’s oracle can be retrieved from the MovingAverageOracle.
_stableAssetAddress address Address of stable asset used in strategy. Could be Dai, USDC, etc.
_riskAssetAddress address Address of risk asset used in strategy.
_initialStableCollateralAddress address Address of the Set Token representing the strategy’s stable collateral.
_initialRiskCollateralAddress address Address of the Set Token representing the strategy’s risk collateral.
_setTokenFactory address Address of the SetTokenFactory tracked by Core, which will be used to deploy the nextSet address.
_auctionLibrary address Address of the auction price library to be used for Dutch Auction pricing. The Auction Library must be tracked by Core.
_movingAverageDays uint256 The amount of days to use in moving average calculation.
_auctionTimeToPivot uint256 The time in seconds after the auction start time in which the price pivots from a Linear to Exponential curve. The time to pivot must be within the range specified on the Rebalancing Set Token's associated Rebalancing Set Token factory.
_crossoverConfirmationBounds uint256[2] The minimum and maximum time in seconds confirm confirmation can be called after the last initial crossover confirmation.

initialize(address _rebalancingSetTokenAddress): This function sets the Rebalancing Set Token address that the manager is associated with. This function can only be called once during initialization by the contract deployer, after which no address has the ability to change the Rebalancing Set Token address.

initialPropose(): initialPropose is called to kick off the rebalance process. If conditions are met, the block time stamp is logged and begins a "confirmation period" where the manager checks to see that the price remains crossed over the moving average. The implementation for initialPropose is as follows:

  1. Check Rebalancing Set conditions: Proceeds as outlined in the above section however a check is made to make sure that the manager is not within crossoverConfirmationMaxTime of last successful initialPropose call, without this check someone could continually call initalPropose and never allow the manager to create a proposal.

  2. Query data from oracles: In this case we query the riskAsset's price oracle and the risk asset's MovingAverageOracle associated with the manager. The MovingAverageOracle is queried for the movingAverageDays moving average.

  3. Verify Price Triggers Met: The manager checks to see what collateral the Rebalancing Set is currently using. If currently using risk collateral then checks to see if the risk asset price has crossed below the moving average (and vice versa).

         if (usingRiskCollateral()) {
             // If currently holding risk asset (riskOn) check to see if price is below MA, otherwise revert.
             require(
                 _movingAveragePrice > _riskAssetPrice,
                 "MACOStrategyManager.checkPriceTriggerMet: Risk asset price must be below moving average price"
             );
         } else {
             // If currently holding stable asset (not riskOn) check to see if price is above MA, otherwise revert.
             require(
                 _movingAveragePrice < _riskAssetPrice,
                 "MACOStrategyManager.checkPriceTriggerMet: Risk asset price must be above moving average price"
             );
         } 
    

    If price trigger met then lastCrossoverConfirmationTimestamp is logged and the manager waits to confirm the signal.

confirmPropose(): confirmPropose is called to confirm the signal first seen when initialPropose was called. The crossover must be reconfirmed once between the time frame passed into the manager as _crossoverConfirmationBounds in the constructor. Once confirmed a proposal will be generated and sent to the Rebalancing Set Token. If the crossover does not reoccur within that time period then initialPropose must be called to reset the time stamp and restart the proposal process, any confirmPropose calls before or after the confirmation time period will revert. The implementation for confirmPropose is as follows:

  1. Check Rebalancing Set conditions: Proceeds as outlined in the above section however a check is made to make sure that the manager is currently in a "confirmation period" (block.timestamp >= lastCrossoverConfirmationTimestamp.add(crossoverConfirmationMinTime) && block.timestamp <= lastCrossoverConfirmationTimestamp.add(crossoverConfirmationMaxTime)).

  2. Query data from oracles: See same step in initialPropose for logic.

  3. Verify Price Triggers Met: See same step in initialPropose for logic.

  4. Determine next allocation: In most scenarios no new collateral needs to be created, the manager just flips between proposing stableCollateralAddress when currently using risk collateral and vice versa:

         (
             address nextSetAddress,
             uint256 currentSetDollarValue,
             uint256 nextSetDollarValue
         ) = usingRiskCollateral() ? (stableCollateralAddress, riskCollateralDollarValue, stableCollateralDollarValue) : 
             (riskCollateralAddress, stableCollateralDollarValue, riskCollateralDollarValue);
    

    However, due to limitations put in place on the auction library if the one of the collateral's value ever exceeds the other by 4x, new collateral must be created. In this scenario, the new collateral being created is set equal in USD value to the current collateral (using the same naturalUnit as the current collateral). In order to avoid the edge case where the new collateral set would have 0 units due to rounding, the new potential units are checked to see if they are equal to 0. If so, then the units are recalculated with an underlying naturalUnit 10x greater than the previous calculation (learn about relationship between units and naturalUnits in our white paper), this continues until the new units are greater than 0.

         // Calculate nextSetUnits such that the USD value of new Set is equal to the USD value of the Set
         // being rebalanced out of
         uint256[] memory nextSetUnits = new uint256[](1);
    
         uint256 potentialNextUnit = 0;
         uint256 naturalUnitMultiplier = 1;
         uint256 nextNaturalUnit;
    
         // Calculate next units. If nextUnit is 0 then bump natural unit (and thus units) by factor of
         // ten until unit is greater than 0
         while (potentialNextUnit == 0) {
             nextNaturalUnit = _replacementCollateralNaturalUnit.mul(naturalUnitMultiplier);
             potentialNextUnit = calculateNextSetUnits(
                 _currentCollateralUSDValue,
                 _replacementUnderlyingPrice,
                 _replacementUnderlyingDecimals,
                 nextNaturalUnit
             );
             naturalUnitMultiplier = naturalUnitMultiplier.mul(10);            
         }
    
         nextSetUnits[0] = potentialNextUnit;
         return (nextSetUnits, nextNaturalUnit);
    
  5. Determine Proposal Parameters: Proceeds mostly outlined in the above section, however due to the more price-sensitive nature of the strategy the auction duration is shortened and price change is sped up to 1% move every 10 minutes. This means that a similar range of prices are still explored during the auction, however this comes with a loss in price granularity. The subsequent formula for priceRange remains the same just with a different constant in the divisor:

    priceRange = (auctionTimeToPivot/TEN_MINUTES_IN_SECONDS) * (fairValue/100)