Dissecting DeFi Protocols

Share this post
Dissecting the Hegic protocol
0xkowloon.substack.com

Dissecting the Hegic protocol

0xkowloon
Mar 19, 2021
Comment
Share
Hegic’s founder and her new book

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

  1. Liquidity providers provide ETH as liquidity by calling the contract method provide in HegicETHPool.sol and receive writeETH 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);
  2. 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

  3. To write an option, an option buyer can call the method create, which accepts the arguments period, amount and strike. 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);
  4. 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.

  1. 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);
  2. It then sets the account’s lastProfit to totalProfit. This is to prevent users from claiming more than they are entitled to.

    lastProfit[account] = totalProfit;
  3. It then adds the unsaved profit to the user’s saved profit.

    profit = savedProfit[account].add(unsaved);
    savedProfit[account] = profit;
  4. It immediately resets savedProfit to 0 as it will send the ETH to the user.

    savedProfit[msg.sender] = 0;
    msg.sender.transfer(profit);

CommentComment
ShareShare

Create your profile

0 subscriptions will be displayed on your profile (edit)

Skip for now

Only paid subscribers can comment on this post

Already a paid subscriber? Sign in

Check your email

For your security, we need to re-authenticate you.

Click the link we sent to , or click here to sign in.

TopNewCommunity

No posts

Ready for more?

© 2022 0xkowloon
Privacy ∙ Terms ∙ Collection notice
Publish on Substack Get the app
Substack is the home for great writing