Dissecting the Hegic protocol
Hegic protocol is an options trading protocol that allows users to buy call/put options in a permissionless and non-custodial way. Liquidity providers send ETH/WBTC to Hegic’s pool to receive writeETH/writeWBTC, which represents a stake in the pool’s written options. The liquidity is shared among different options and among all liquidity providers to diversify risks. Liquidity providers earn a premium from selling options. When an option expires, options buyers have the option to receive their profits if they are in the money. Contrary to Hegic v1, where put options’ profits are paid in DAI, Hegic v888 pays out put options profits in their underlying assets.
HEGIC token holders earn settlement fees in the underlying assets and have the right to vote on changing protocol parameters.
In this tutorial, we will look at the lifecycle of a ETH call option and a ETH put option.
Buying an ETH call option
Liquidity providers provide ETH as liquidity by calling the contract method
provide
inHegicETHPool.sol
and receivewriteETH
LP tokens.uint supply = totalSupply(); uint balance = totalBalance(); // If the total supply and balance are both greater than 0, // the write tokens to mint is equal to // ETH sent x total supply ÷ (contract's total ETH balance - locked premium - ETH sent) // Otherwise, the write tokens to mint is equal to // ETH sent x 100 if (supply > 0 && balance > 0) mint = msg.value.mul(supply).div(balance.sub(msg.value)); else mint = msg.value.mul(INITIAL_RATE);
The ETH option contract lives in
HegicETHOptions.sol
, it stores the following variables and most of them can be modified by the contract owner.liquidity pool to lock up collaterals
Chainlink Oracle to get ETH price
Settlement fee recipient address
Implied volatility rate - maximum value 1,000
Option collateralization ratio - must be between 50 and 100
To write an option, an option buyer can call the method
create
, which accepts the argumentsperiod
,amount
andstrike
. The following operations are run:Check that the option period must be between 1 day and 4 weeks.
Check that the amount purchased must greater than the calculated strike fee.
The option is purchased with ETH. The ETH amount sent to the contract must be greater than or equal to the calculated total amount (period fee + strike fee + settlement fee).
period fee = ETH amount x √period x implied volatility rate x current price ÷ strike price
strike fee =
// If in the money, // (current price - strike price) x purchase amount ÷ current price if (strike < currentPrice && optionType == OptionType.Call) return currentPrice.sub(strike).mul(amount).div(currentPrice); // If out of the money, return 0
settlement fee = 1% of the purchase
strike amount = total amount - strike fee
locked amount = (strike amount x option collateralization ratio ÷ 100) + strike fee
premium = total amount - settlement fee
Then, it sends the settlement fee to the ETH staking pool and lock the option’s premium as well as the option’s locked amount.
settlementFeeRecipient.sendProfit {value: settlementFee}(); pool.lock {value: option.premium} (optionID, option.lockedAmount);
To exercise an option, the option holder calls the method
exercise
. It checks that the caller owns the option, the option has expired and the option is Active.require(option.expiration >= block.timestamp, "Option has expired"); require(option.holder == msg.sender, "Wrong msg.sender"); require(option.state == State.Active, "Wrong state");
It then calculates the profit to be paid
// Get latest ETH price (, int latestPrice, , , ) = priceProvider.latestRoundData(); uint256 currentPrice = uint256(latestPrice); // Check that the strike price is lower than or equal to the current price require(option.strike <= currentPrice, "Current price is too low"); // profit = (current price - strike price) x purchased amount ÷ current price profit = currentPrice.sub(option.strike).mul(option.amount).div(currentPrice); // profit is capped at the option's locked amount if (profit > option.lockedAmount) profit = option.lockedAmount; // Deduct locked premium and locked amount from the pool and send the profit to the option holder pool.send(optionID, option.holder, profit);
Buying an ETH put option
For buying an ETH put option, the flow is the same as buying an ETH call option with only a few differences:
The period fee’s formula is
// period fee = option amount x √period x implied volatility rate x current price ÷ strike price
amount.mul(sqrt(period)).mul(impliedVolRate).mul(currentPrice).div(strike).div(PRICE_DECIMALS);
The strike fee’s formula is
// If in the money,
// (strike price - current price) x option amount ÷ current price
if (strike > currentPrice && optionType == OptionType.Put)
strike.sub(currentPrice).mul(amount).div(currentPrice);
// If out of the money, return 0
The profit formula is
// (strike price - current price) x option amount ÷ current price
profit = option.strike.sub(currentPrice).mul(option.amount).div(currentPrice);
Withdrawing your liquidity
Liquidity providers can withdraw their liquidity any time they want, but they must have locked up their liquidity for more than the lockup period.
require(
lastProvideTimestamp[msg.sender].add(lockupPeriod) <= block.timestamp,
"Pool: Withdrawal is locked up"
);
The rest is pretty straightforward. It checks the burn token amount’s validity.
burn = divCeil(amount.mul(totalSupply()), totalBalance());
require(burn <= maxBurn, "Pool: Burn limit is too small");
require(burn <= balanceOf(msg.sender), "Pool: Amount is too large");
require(burn > 0, "Pool: Amount is too small");
After validating the burn amount, it sends the underlying asset to the liquidity provider.
_burn(msg.sender, burn);
emit Withdraw(msg.sender, amount, burn);
msg.sender.transfer(amount);
HEGIC Rewards
HEGIC option holders are entitled to rewards in HEGIC. The logic lives in HegicRewards.sol
. Option holders can claim their rewards by calling getReward
.
The reward amount is calculated by the method rewardAmount
.
function rewardAmount(uint optionId) internal view returns (uint) {
(, , , uint _amount, , uint _premium, , ) = hegicOptions.options(optionId);
return _amount.div(100).add(_premium)
.mul(rewardsRate)
.div(REWARD_RATE_ACCURACY);
}
An option’s reward amount is the 1% of the option amount plus the option premium, then multiplying the rewards rate.
The method also makes sure the option is not inactive, the reward has not been claimed and the reward amount is less than the daily reward limit.
require(state != IHegicOptions.State.Inactive, "The option is inactive");
require(!rewardedOptions[optionId], "The option was rewarded");
require(
dailyReward[today] < MAX_DAILY_REWARD,
"Exceeds daily limits"
);
Staking HEGIC
As mentioned previously, HEGIC holders receive fees generating by selling options. The logic lives in HegicStaking.sol
.
When fees are generated, the method sendProfit
is called.
function sendProfit() external payable override {
uint _totalSupply = totalSupply();
if (_totalSupply > 0) {
totalProfit += msg.value.mul(ACCURACY) / _totalSupply;
emit Profit(msg.value);
} else {
FALLBACK_RECIPIENT.transfer(msg.value);
}
}
The total profit is increased by the amount of ETH sent divided by the total supply of staked tokens (sHEGIC).
sHEGIC holders claim their profits by calling claimProfit
.
It first gets the user’s unsaved profit.
// (total profit - account's last claimed profit) x account sHEGIC balance totalProfit.sub(lastProfit[account]).mul(balanceOf(account)).div(ACCURACY);
It then sets the account’s
lastProfit
tototalProfit
. This is to prevent users from claiming more than they are entitled to.lastProfit[account] = totalProfit;
It then adds the unsaved profit to the user’s saved profit.
profit = savedProfit[account].add(unsaved); savedProfit[account] = profit;
It immediately resets
savedProfit
to 0 as it will send the ETH to the user.savedProfit[msg.sender] = 0; msg.sender.transfer(profit);