-
Notifications
You must be signed in to change notification settings - Fork 1
Home
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.
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.
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):
- Manager checks that enough time has passed since last rebalance and that there isn’t a new proposal currently under way.
- Manager queries its respective Oracles and determines whether price triggers have been met for a new rebalance
- 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.
- 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.
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.
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.
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.
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 |
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.
-
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.
-
Query data from oracles: The Manager pulls the most current price information (represented as an 18 decimal unsigned integer) from its associated oracles.
-
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.
-
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.
-
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.
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:
-
Check Rebalancing Set conditions: Proceeds as outlined in the above section.
-
Query data from oracles: In this case we query the MakerDAO WBTC and WETH oracles.
-
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.
-
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, aDECIMAL_DIFFERENCE_MULTIPLIER
must be applied to the ETH units due to the difference in token decimals between WETH (18) and WBTC (8). The nextSet uses10 ** 10
for a natural unit.In the event that ETH’s price exceeds BTC, a price precision constant (
PRICE_PRECISION
) of100
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 to10 ** 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. -
Determine Proposal Parameters: Proceeds as outlined in the above section.
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:
-
Check Rebalancing Set conditions: Proceeds as outlined in the above section.
-
Query data from oracles: In this case we query the MakerDAO WBTC oracle. Dai is fixed at $1 (represented by a constant
10 ** 18
). -
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).
-
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, aDECIMAL_DIFFERENCE_MULTIPLIER
must be applied to the Dai units due to the difference in token decimals between Dai (18) and WBTC (8). The nextSet uses10 ** 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.
-
Determine Proposal Parameters: Proceeds as outlined in the above section.
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:
- Check Rebalancing Set conditions: Proceeds as outlined in the above section.
-
Query data from oracles: In this case we query the MakerDAO WETH oracle. Dai is fixed at $1 (represented by a constant
10 ** 18
). - 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).