Skip to content
This repository has been archived by the owner on Jan 18, 2023. It is now read-only.
bweick edited this page Jul 12, 2019 · 19 revisions

Introduction

Welcome to the set-protocol-strategies wiki!

The Strategies repo contains information about Set Protocol’s Smart Contract Managed Rebalancing Sets and any supporting smart contracts (i.e. oracles). Smart Contract Managed Rebalancing Sets are tokenized portfolio management strategies, built as an extension to Set Protocol. Rebalancing Sets enable strategies to be programmed into ERC20 compliant tokens. For the end user, no action is required to have underlying allocations periodically updated based on the predefined strategy. Anyone with an Ethereum public/private key pair can create, issue, redeem, purchase, and rebalance a Rebalancing Set. To learn more about Managed Sets, see the whitepaper here.

General Architecture

Each Smart Contract Managed Rebalancing Set system is made of three distinct parts: 1) Manager Contract that generates rebalancing proposals based off on-chain data, 2) Oracles that are used to provide data to the Manager Contract and, 3) the Rebalancing Set Token that stores state (balances, allocations, etc.) regarding the strategy.

Implementation

The implementation of this system follows the same general pattern for all Smart Contract Managed Rebalancing Sets with some differences based on strategies (see specific strategy implementations below):

  1. Manager checks that enough time has passed since last rebalance and that there isn’t a new proposal currently under way.
  2. Manager queries its respective Oracles and determines whether price triggers have been met for a new rebalance
  3. Manager selects new allocations for the strategy (represented by Set Tokens) by either creating a new Set Token or selecting between previously created Set Tokens.
  4. Manager calculates auction parameters based off the difference in value between the old collateral and new collateral. 5.Manager sends proposal to Rebalancing Set Token which puts Rebalancing Set Token into Proposal state and starts the countdown to the beginning of the auction.

Contracts

Oracles

Maker Oracles

Currently all of Set’s oracles are in one way or another reliant on oracles used by MakerDAO (Maker) to secure their system, at present Set uses WETH and WBTC oracles deployed by Maker. Set uses Maker’s Oracles in two ways, 1) to read current price data for use in the Manager contracts and 2) to log data for use in “Meta” oracles that use historic price data to calculate various price indicators used in the Manager contracts.

At a high level, Maker’s oracles (also known as medianizer) read from a set of approved price feeds and choose the median price from those feeds. Price feeds are meant to be updated 1) when the source price feed is greater than 1% away from current price, or 2) 6 hours has passed since last update. It should be noted, as a consequence, that Maker’s oracles do not provide instantaneous “live” prices and as a result there can be a lag or delay in accounting for price movements in Set’s system. For more on Maker's oracles read here.

HistoricalPriceFeed

Currently, HistoricalPriceFeeds use data from Maker’s oracles to build a price history for the underlying asset, but any oracle adhering to Maker's interface could be used. The HistoricalPriceFeed is intended to represent the historical prices of one asset at consistent time intervals. Additionally there is a defined upper limit to the amount of historical data, after which old data is overwritten. The HistoricalPriceFeed’s functions are as follows:

constructor(uint256 _updateFrequency, address _medianizerAddress, string _dataDescription, uint256[] _seededArray): Upon instantiation the parameters of the price feed are set. The _updateFrequency sets the minimum amount of time allowed to pass between data points and the _medianizerAddress gives what oracle to read data from (thus implying the underlying asset it’s gathering data for). Additionally a _dataDescription is passed so that one can easily tell the parameters of the HistoricalPriceFeed off-chain, and the price feed can be seeded with values in order for immediate use in production.

poke(): Requires lastUpdatedAt + updateFrequency < current block timestamp. Assuming condition is met the underlying oracle’s price is logged and the lastUpdatedAt timestamp is updated. Anyone can call this function to update the HistoricalPriceFeed.

read(uint256 _dataDays): Returns array of _dataDays amount of historical price data. The returned array starts with the most recent price. If not enough data has been logged to satisfy _dataDays then the call will revert.

changeMedianizer(address _newMedianizerAddress): Allow for an update to the underlying medianizer address in case old medianizer is corrupted or not maintained. Only owner of price feed can call.

MovingAverageOracle

The MovingAverageOracle leverages data available from the HistoricalPriceFeed to calculate a simple moving average of a desired amount of data points. The MovingAverageOracle’s functions are as follows:

constructor(address _priceFeed, string _dataDescription): Sets the address of the HistoricalPriceFeed to read from and _dataDescription is passed so that one can easily tell the parameters of the MovingAverageOracle off-chain.

read(uint256 _dataPoints): Queries array of data from HistoricalPriceFeed of length _dataPoints and calculates the simple moving average of those data points.

getSourceMedianizer(): Returns the medianizer that the HistoricalPriceFeed underlying the MovingAverageOracle uses.

Current Deployed Contracts

HistoricalPriceFeed

Network Address updateFrequency Asset medianizerAddress
Mainnet 0x7956cE4fbA992987A11bd44ff0Ddb62504711Be8 Daily Wrapped Ethereum 0x729D19f657BD0614b4985Cf1D82531c67569197B
Kovan 0x44Fd7B522255d6Ff8DF234488F9031e5c91373d7 Daily Wrapped Ethereum 0x9Fe3077E4468e503a304909B6005E69D5C5B8907

MovingAverageOracle

Network Address Asset priceFeedAddress
Mainnet 0x90b242eDd278E636E02C2054C861Fd46A7B96271 Wrapped Ethereum 0x7956cE4fbA992987A11bd44ff0Ddb62504711Be8
Kovan 0x813F19236b7d241A6a3b42384DEd85bc159549Da Wrapped Ethereum 0x44Fd7B522255d6Ff8DF234488F9031e5c91373d7

Strategies

Manager contracts encode the logic for determining new allocations. Each manager has its own little 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 below.

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. “Fair price” 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, the “fair value” is 1500/1200=1.25.

    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:

    By placing the “fair value” 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 derived 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).