How to Calculate Price Volatility for DeFi Variance Swaps

For his prize-winning Chainlink hackathon submission, smart contract developer Max Feldman used Chainlink Price Feeds to calculate on-chain price volatility for a variance swap DeFi product. In this tutorial, Max explains how to calculate volatility from Chainlink Price Feeds and use this data to trigger smart contract derivatives, showcasing yet another example of how decentralized infrastructure is expanding to support traditional financial instruments and applications.

By Max Feldman

A variance swap is a financial instrument commonly traded over the counter among traditional financial institutions. It is an agreement to pay out or receive the annualised variance of a given asset pair over a given period of time. Variance is a measure of volatility. (Here’s a helpful video walkthrough of variance swap mechanics.) During the 2020 Chainlink Virtual Hackathon, I leveraged the power of Chainlink Price Feeds to build Feldmex Variance Swaps, one of the first-ever on-chain pure volatility products.

Unlike variance swaps in traditional finance, Feldmex variance swaps are much more uniform and fit into an easy-to-use ERC20 token wrapper. Rather than trading over the counter, Feldmex variance swaps may be traded directly from a user’s Ethereum wallet, empowering both individuals and firms to leverage decentralized infrastructure for hedging volatility risk.

DeFi Variance Swap Architecture

Minting Swaps

Feldmex variance swaps have a maximum payout cap beyond which the swap will not yield more. In traditional finance, these are referred to as capped variance swaps. Payouts are capped because there is no upward bound on the possible value of variance, and we must make sure that our smart contracts are not liable to pay out more funds than they hold.

Furthermore, to prevent our system from being liable for withdrawals which it cannot withstand, we must ensure that all open positions have sufficient collateral. We can take this a step further and design the system such that whenever users deposit an amount of the payout asset equal to the maximum payout of x number of swaps, the protocol mints x number of long variance tokens (tokens that pay out the annualised variance at the end of the period), as well as x number of short variance tokens (tokens that pay out the payout cap minus the payout of the long variance tokens). Thus each position is sufficiently collateralised by users when they mint swaps and the total number of long positions will equal the total number of short positions.

Feldmex variance token minting process.
How the Feldmex system mints long and short variance tokens based on Chainlink oracle-supplied historical market data.

Users are charged a percentage fee set by the Feldmex team upon minting swaps, which is not to exceed 2.55%. We will get into the strategic purpose of these fees later on.

Mint Swaps in Solidity

The following function may be found in the “varianceSwapHandler” contract.

The following function may be found in the varianceSwapHandler contract.

function mintVariance(address _to, uint _amount, bool _transfer) public {
IERC20 pa = IERC20(payoutAssetAddress);
uint _feeAdjustedCap = feeAdjustedCap;
uint _subUnits = subUnits;
uint _payoutAssetReserves = payoutAssetReserves;
if (_transfer) {
uint transferAmount = _amount.mul(_feeAdjustedCap);
transferAmount = transferAmount.div(_subUnits).add(transferAmount%_subUnits==0 ? 0 : 1);
pa.transferFrom(msg.sender, address(this), transferAmount);
}
uint newReserves = pa.balanceOf(address(this)).sub(_payoutAssetReserves);
//requiredNewReserves == amount*_feeAdjCap/subUnitsVarSwaps
//maxAmount == newReserves*_subUnitsVarSwaps/_feeAdjCap
uint maxAmt = newReserves.mul(_subUnits).div(_feeAdjustedCap);
require(maxAmt >= _amount, "you attempted to mint too many swaps on too little collateral");
uint _fee = _amount.mul(_feeAdjustedCap).sub(_amount.mul(cap)).div(_subUnits);
pa.transfer(sendFeeTo, _fee);
payoutAssetReserves = newReserves.sub(_fee).add(_payoutAssetReserves);
balanceLong[_to] = maxAmt.add(balanceLong[_to]);
balanceShort[_to] = maxAmt.add(balanceShort[_to]);
totalSupplyLong = maxAmt.add(totalSupplyLong);
totalSupplyShort = maxAmt.add(totalSupplyShort);
emit Mint(_to, maxAmt);
}

When minting swaps, we first initialise a few local variables with the value of storage variables to save gas later on in the function. We then check if the user would like for the contract to call transferFrom to send funds to the contract. Next, we check the amount of new reserves of the payout asset in the contract and find the corresponding amount of swaps which may be minted with the increased balance. These checks ensure that open positions have sufficient collateral for payouts and that there are an equal number of long and short positions. Now we can finally credit the _to address with the newly minted swaps.

Burning Swaps

Burning swaps is the reverse process of minting swaps. Users may redeem x amount of long variance tokens and x amount of short variance tokens for x times the maximum payout of the swaps. This is all made possible by the fact that one long variance token plus one short variance token will always yield the maximum payout of an individual swap.

Burn Swaps in Solidity
function burnVariance(uint _amount, address _to) public {
require(balanceLong[msg.sender] >= _amount && balanceShort[msg.sender] >= _amount);
balanceLong[msg.sender] = balanceLong[msg.sender].sub(_amount);
balanceShort[msg.sender] = balanceShort[msg.sender].sub(_amount);
uint transferAmount = cap.mul(_amount).div(subUnits);
payoutAssetReserves = payoutAssetReserves.sub(transferAmount);
IERC20(payoutAssetAddress).transfer(_to, transferAmount);
totalSupplyLong = totalSupplyLong.sub(_amount);
totalSupplyShort = totalSupplyShort.sub(_amount);
emit Burn(msg.sender, _amount);
}

When burning swaps, we first ensure that the amount the user wishes to burn is less than or equal to the user’s balance of long and short variance tokens. Next, we deduct _amount from the user’s long and short variance token balances. We then transfer the corresponding value of the swaps in the payout asset to the _to address. Finally we deduct _amount from totalSupplyLong and totalSupplyShort. This ensures that users cannot claim payout on the same swap tokens multiple times.

Liquidity Incentives

In order to offer users access to liquidity and to optimise for market efficiency, the Feldmex variance swap protocol is designed to reward liquidity providers with fees generated by the platform.

Harnessing Chainlink to Find Variance

An asset pair’s variance is defined as the variance of daily percentage returns over a given period. This means that when we want to find variance, we need to fetch historical daily prices for the asset pair from the beginning of the period to the end of the period. Chainlink Price Feeds allow for us to access historical prices. To find historical prices from Chainlink’s price oracles, I created an oracle contract that interacts with the Chainlink Network. So how exactly did I get historical prices previous to each timestamp from my oracle?

Let’s break down the code.

Finding Historical Price Using Chainlink

First, let’s look at the oracle constructor.

constructor (address _aggregatorAddress) public {
aggregatorAddress = _aggregatorAddress;
fluxAggregatorAddress = address(AggregatorFacade(_aggregatorAddress).aggregator());
ai2 = AggregatorInterface(fluxAggregatorAddress);
}

First, we pass in the address of the Chainlink reference contract (find the addresses at https://docs.chain.link/docs/reference-contracts). We then store the address and fetch the flux aggregator address from the original aggregator contract. We do this because the flux aggregator contract implements AggregatorInterface (now known as AggregatorV3Interface) which we will use later on. The original aggregator contract is just a proxy and by interacting with the flux aggregator we have the added benefit of saving on gas.

Introducing some helper functions
function fetchRoundBehind(uint80 _roundId) public view returns (uint, uint80) {
uint timestamp = ai2.getTimestamp(_roundId);
while (timestamp == 0 && _roundId != 0) {
_roundId--;
timestamp = ai2.getTimestamp(_roundId);
}
return (timestamp, _roundId);
}

fetchRoundBehind ensures that the round with id _roundId is not filled with null data, and if it is, it finds the round previous to it with initialised data. Once it has found the round previous with initialised data it returns two values. First is the round’s timestamp, which is retrieved from the Chainlink aggregator, ai2, and second is the round ID of the final round. It should be noted that if the round with ID _roundId is not filled with null data its timestamp and ID will be returned.

function foremostRoundWithSameTimestamp(uint _roundId) internal view returns (uint) {
uint timestamp = ai2.getTimestamp(_roundId);
_roundId++;
while (timestamp == ai2.getTimestamp(_roundId)) _roundId++;
return _roundId-1;
}

foremostRoundWithSameTimestamp is self-explanatory. It returns the id of the last round with a timestamp equal to the timestamp of the round with id _roundId.

Now onto the meat of the oracle contract

The fun part is the function used to get the round ID of the non null round previous to any timestamp. Let’s take a look.

function fetchRoundAtTimestamp(uint timestamp) public view returns (uint) {
uint80 latest = uint80(ai2.latestRound());
(uint fetchedTime, uint80 fetchedRound) = fetchRoundBehind(latest);
if (timestamp >= fetchedTime) return fetchedRound;
latest = fetchedRound;
uint80 back; uint80 next;
uint80 round = latest >> 1;
do {
(fetchedTime, fetchedRound) = fetchRoundBehind(round);
if (fetchedTime > timestamp) {
latest = fetchedRound;
next = (back+fetchedRound)>>1;
}
else if (fetchedTime < timestamp) {
back = round;
next = (latest+round)>>1;
}
else return foremostRoundWithSameTimestamp(fetchedRound);
round = next;
} while (next != back);
(,back) = fetchRoundBehind(back);
return foremostRoundWithSameTimestamp(back);
}

At the beginning of the function, we initialise some variables and return the most recent round if the timestamp passed is from the future. Otherwise we will do a binary search through the rounds each time checking the timestamp of the round against our desired timestamp. In this search, we keep track of the furthest forward round that has a timestamp less than the target timestamp and the furthest back round that has a timestamp greater than the target timestamp. The first line of Solidity after we exit the loop is (,back) = fetchRoundBehind(back); The purpose of this line is to make sure that the round with ID back is not filled with null data. Now all we need to do is make sure that there are no more rounds ahead of back that have the same timestamp as back.

To do this, we can return returnRoundWithSameTimestamp(back). We have now successfully returned the ID of the round previous to the timestamp parameter. This simple function gives us access to ALL on-chain historical price data provided by Chainlink!

Calculating Variance

Now that we have a method that allows us to easily access historical price data via Chainlink, we can build any number of interesting applications that use this data.

Calculating the variance is now quite easy! We can set up a function that writes daily percentage return data to storage within our varianceSwapHandler contract. We then simply need to make a function in another contract which can be called via delegatecall to find the variance of the daily percentage returns and write the result to storage.

function seriesVariance() public {
uint _seriesTermInflator = seriesTermInflator;
uint seriesLength = dailyReturns.length; //gas savings
int meanDailyReturn = summationDailyReturns.div(seriesLength.toInt());
uint summationVariance;
int inner;
for (uint i = 0; i < seriesLength; i++) {
inner = dailyReturns[i]-meanDailyReturn;
summationVariance += inner.mul(inner).toUint().div(_seriesTermInflator);
}
result = summationVariance.div(seriesLength.sub(1)).mul(payoutAtVarianceOf1).mul(annualizer).div(_seriesTermInflator).div(annualizerInflator);
}

seriesVariance() can be found in the BigMath contract. This function is called via delegatecall and calculates the variance of daily percentage returns. It first finds the mean daily percentage return. It then calculates sample variance according to the formula:

Sample Variance Formula
The function then sets the result storage variable to the uncapped payout of long variance tokens.

Finishing UpNow you have a working decentralized variance swap contract. To see how I implemented this for the Chainlink Virtual Hackathon, you can follow along with this video.

You can interact with the app on Kovan testnet at the following link: https://feldmex.com

The Future of Feldmex

The first order of business is getting to mainnet and improving the UI. There is also great opportunity for Feldmex to expand into various other kinds of derivatives, namely other types of swap contracts. This hackathon build helped us realize the broad horizons for translating traditional financial instruments into decentralized protocols that are highly automated and transparent. We hope this system design serves as a helpful mechanism for hedging volatility risk in the rapidly growing DeFi landscape.

More on this Topic

If you’re a developer interested in using Chainlink to power DeFi smart contracts, reach out here or visit the developer documentation. You can also subscribe to the Chainlink Newsletter to stay up to date with everything in the Chainlink stack.

Website | Twitter | Discord | Reddit | YouTube | Telegram | Events | GitHub | Price Feeds | DeFi