期权交易平台Hegic项目合约分析

概述

加密货币的期权交易平台,目前中心化的如Deribit, 去中心化的则是Opyn和Hegic比较靠前。
这里我们仅对Hegic的V1版本做简要的分析,如果有分析不到位,或者错误的地方还请评论留言一起交流和讨论。

Hegic 的主要理念是提供了一个流动性池(Liquidity Pool),使得流通性的提供者形成收益共享,风险共担的关系。

期权的卖方

流动性的提供者,称为writer,添加完流动性后自动成为卖方。卖方对期权没有任何选择,只是添加完流动性,等待买方购买,收益就是买方的权利金。

卖方的行为

卖方可以有两个动作,一个是添加流动性(合约访问为provide),就是把资产交出去,另外一个是提款(合约方法为withdraw),也就是把资产拿回来,这里权利金的收益也会与添加的资产合并在一起。
这里我们选取HegicETHPool.sol来说明一下

 /*
     * @nonce A provider supplies ETH to the pool and receives writeETH tokens
     * @param minMint Minimum amount of tokens that should be received by a provider.
                      Calling the provide function will require the minimum amount of tokens to be minted.
                      The actual amount that will be minted could vary but can only be higher (not lower) than the minimum value.
     * @return mint Amount of tokens to be received
     */
    function provide(uint256 minMint) external payable returns (uint256 mint) {
    
    
        lastProvideTimestamp[msg.sender] = block.timestamp;
        uint supply = totalSupply();
        uint balance = totalBalance();
        if (supply > 0 && balance > 0)
            mint = msg.value.mul(supply).div(balance.sub(msg.value));
        else
            mint = msg.value.mul(INITIAL_RATE);

        require(mint >= minMint, "Pool: Mint limit is too large");
        require(mint > 0, "Pool: Amount is too small");

        _mint(msg.sender, mint);
        emit Provide(msg.sender, msg.value, mint);
    }

    /*
     * @nonce Provider burns writeETH and receives ETH from the pool
     * @param amount Amount of ETH to receive
     * @return burn Amount of tokens to be burnt
     */
    function withdraw(uint256 amount, uint256 maxBurn) external returns (uint256 burn) {
    
    
        require(
            lastProvideTimestamp[msg.sender].add(lockupPeriod) <= block.timestamp,
            "Pool: Withdrawal is locked up"
        );
        require(
            amount <= availableBalance(),
            "Pool Error: Not enough funds on the pool contract. Please lower the amount."
        );

        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");

        _burn(msg.sender, burn);
        emit Withdraw(msg.sender, amount, burn);
        msg.sender.transfer(amount);
    }

这里我们简单分析一下合约中的这两个方法。

provide 流动性添加

provide 方法有一个入参,并且由于是ETH的池子,所以这个方法会收取卖方添加的ETH资产。返回的是一种ERC20的凭证Token,这里称之为writeToken。入参和出参都是指的这个writeToken的数量。

  1. 方法第一步记录了添加流动性时的区块时间。(Hegic的期权时间使用的都是相对时间这点也是和Opyn与Deribit的不一样的地方)
  2. 计算writeToken的流通量totalSupply(),和资产池中的剩余量 totalBalance()(这个剩余量是当前合约的ETH的数量-因合约卖出被锁定的资产数量),其中资产的剩余量是包含了买家的行权金的。
    根据这两个值,来计算出当前添加的资产数量,应该铸造出多少的writeToken的数量。
    如果是第一次,会乘以一个默认的初始值 uint256 public constant INITIAL_RATE = 1e3;
    如果不是第一次,就是msg.value*目前writeToken的流通量/目前ETH的流动性。
  3. 计算出这个最mint的数量之后,如果大于等于流动性提供者的期望值且大于0,那么就会给流动性提供者铸造mint个writeToken。

provide的方法结束。这里要注意的是,totalBalance()这个方法

function totalBalance() public override view returns (uint256 balance) {
        return address(this).balance.sub(lockedPremium);
    }

执行这个方法时,卖方添加的msg.value也被计算到balance里了,因此要减去msg.value才能得到当前池子中的ETH流动性。

withdraw 提取

withdraw 是流动性提供这个进行,收益提取或者是撤出的方法。其做法是支付writeToke 换取池子中相应比例的流动性资产。

  1. 取出的第一个条件是锁定周期要大于两周 uint256 public lockupPeriod = 2 weeks;
  2. 第二个条件是取出的金额要小于当前流动性所能提供的金额availableBalance(),这个的数值是减去锁定的权利金和锁定的底层资产的数量。
  3. 第三是计算本次提取要销毁的writeToken的数量,只有提供的数量大于这个数,才能够成功提取。
    如果上述条件都满足,则可以提现成功。

注意:这里有个很大的问题,就是availableBalance()的计算,合约中如下:

    function availableBalance() public view returns (uint256 balance) {
        return totalBalance().sub(lockedAmount);
    }
    function totalBalance() public override view returns (uint256 balance) {
        return address(this).balance.sub(lockedPremium);
    }

这里的是只能把没有被锁定的部分提供出来给卖方,即lock的那部分不算在可取的余额。也就是除非所有的期权都进行了解锁,但这个的期权的行权日又是由买方决定的,很难保证,或者说无法保证有所有的期权都解锁的时间窗口,否则始终会有一部分资产被锁定,那么流动性的添加者的钱也就无法保证无损的取出

举个例子:
A 向池子中提供了1个ETH的流动性,得到了1000个writeToken.
现在有买方买了0.5个,支付0.001个ETH的权利金,那么会锁定池中的0.5个ETH.
那么B也想池子中注入1个ETH,也得到1000个个writeToken.

此时,无论A还B,都无法用1000个writeToken换到1个ETH.
根据上述公式可以得方程

x*2000/1.5=1000
x=0.75

也就是现在拿1000个writeToken 只能换到0.75个ETH。作为B的话就很不好了,啥都没弄呢上去就被锁了一部分,而且如果被行权了,B也会遭到和A一样的损失,每个人损失0.025个ETH。但是如果没有被行权则会得到权利金的一半。
这个也就是所谓的 风险共担,收益共享

OK,上述就是卖方的动作,下面来说说买方。

期权的买方

期权的买方,称之为buyer,可以选择期权的行权日(period),数量(amount),行权价(strike),期权的类型( OptionType)(call或者是put)。
由此可见给买方的自由度相对较高。但是有个最为重要的参数是:期权的价格,这个是由系统的算法决定的。

买方的行为

买方有买入期权,行权和期权转移的操作。
其中买入期权是创建了一个期权对象,每个期权都有自己的ID,创建时会和创建人进行个绑定,以此来标识期权的所属关系。
行权一般情况下是买方获取了收益,手动执行行权操作,计算收益。Hegic使用的现金结算,即计算完买方的收益差价,直接将收益以当前标的资产的形式退还给买方。
期权转移很简单就是把期权的ID的绑定关系从一个账户换到另外一个账户。

 /**
     * @notice Creates a new option
     * @param period Option period in seconds (1 days <= period <= 4 weeks)
     * @param amount Option amount
     * @param strike Strike price of the option
     * @param optionType Call or Put option type
     * @return optionID Created option's ID
     */
    function create(
        uint256 period,
        uint256 amount,
        uint256 strike,
        OptionType optionType
    )
        external
        payable
        returns (uint256 optionID)
    {
    
    
        (uint256 total, uint256 settlementFee, uint256 strikeFee, ) = fees(
            period,
            amount,
            strike,
            optionType
        );
        require(period >= 1 days, "Period is too short");
        require(period <= 4 weeks, "Period is too long");
        require(amount > strikeFee, "Price difference is too large");
        require(msg.value >= total, "Wrong value");
        if (msg.value > total) msg.sender.transfer(msg.value - total);

        uint256 strikeAmount = amount.sub(strikeFee);
        optionID = options.length;
        Option memory option = Option(
            State.Active,
            msg.sender,
            strike,
            amount,
            strikeAmount.mul(optionCollateralizationRatio).div(100).add(strikeFee),
            total.sub(settlementFee),
            block.timestamp + period,
            optionType
        );

        options.push(option);
        settlementFeeRecipient.sendProfit {
    
    value: settlementFee}();
        pool.lock {
    
    value: option.premium} (optionID, option.lockedAmount);
        emit Create(optionID, msg.sender, settlementFee, total);
    }

    /**
     * @notice Transfers an active option
     * @param optionID ID of your option
     * @param newHolder Address of new option holder
     */
    function transfer(uint256 optionID, address payable newHolder) external {
    
    
        Option storage option = options[optionID];

        require(newHolder != address(0), "new holder address is zero");
        require(option.expiration >= block.timestamp, "Option has expired");
        require(option.holder == msg.sender, "Wrong msg.sender");
        require(option.state == State.Active, "Only active options could be transferred");

        option.holder = newHolder;
    }

    /**
     * @notice Exercises an active option
     * @param optionID ID of your option
     */
    function exercise(uint256 optionID) external {
    
    
        Option storage option = options[optionID];

        require(option.expiration >= block.timestamp, "Option has expired");
        require(option.holder == msg.sender, "Wrong msg.sender");
        require(option.state == State.Active, "Wrong state");

        option.state = State.Exercised;
        uint256 profit = payProfit(optionID);

        emit Exercise(optionID, profit);
    }
   
    /**
     * @notice Sends profits in ETH from the ETH pool to an option holder's address
     * @param optionID A specific option contract id
     */
    function payProfit(uint optionID)
        internal
        returns (uint profit)
    {
    
    
        Option memory option = options[optionID];
        (, int latestPrice, , , ) = priceProvider.latestRoundData();
        uint256 currentPrice = uint256(latestPrice);
        if (option.optionType == OptionType.Call) {
    
    
            require(option.strike <= currentPrice, "Current price is too low");
            profit = currentPrice.sub(option.strike).mul(option.amount).div(currentPrice);
        } else {
    
    
            require(option.strike >= currentPrice, "Current price is too high");
            profit = option.strike.sub(currentPrice).mul(option.amount).div(currentPrice);
        }
        if (profit > option.lockedAmount)
            profit = option.lockedAmount;
        pool.send(optionID, option.holder, profit);
    }

create 期权的创建

期权创建有四个主要的参数,period, amount, strike, optionType,这些都是由买方决定,合约会给买家根据这些参数给出一个期权的价格,如果买家觉得合适就买,觉得不合适就不买。

  1. 第一步就是进行fees的计算,settlementFee是收取amout的百分之1,这个就是手续费,收完之后添加到了质押池子里,用于给提供流动性的卖家分代币,以作为项目代币的价格支撑。
    strikeFee 通常是0。
    还有一个premium,也就是权利金 total - settlementFee,除了手续费,就是就是权利金了,这部分会发生到pool合约,作为后面给卖方提取的部分。
  2. 接下来就是校验一下参数有没有不合法的。
  3. 将参数封装进Option,创建一个期权。
  4. 将权利金发送给池合约,并且在锁定池中amount数量的质押资产。

以上期权合约就购买完成了。这里的核心是fees公式的计算,如果这个公式计算的金额小了,则会对买方有益,如果计算的大了,那么则会对卖方有益。这个公式就像一个天平,它的是否公平决定这个项目是否公平。我不是金融专业的,fee的计算公式也看不懂,这里就不多说了。

exercise

行权方法就是到了行权日,买方觉得赚了就去行权,亏了就不行权。

  1. 行权时合约会调用外部的预言机priceProvider.latestRoundData();,获取价格,比较和行权价的差异,如果买方赚到钱了,就会算出价差调用pool合约从锁定的资产中取出给到买方,并解锁剩余的权利金和质押资产。 下面是池合约的发送收益给买方的方法。
   function send(uint id, address payable to, uint256 amount)
        external
        override
        onlyOwner
    {
    
    
        LockedLiquidity storage ll = lockedLiquidity[id];
        require(ll.locked, "LockedLiquidity with such id has already unlocked");
        require(to != address(0));

        ll.locked = false;
        lockedPremium = lockedPremium.sub(ll.premium);
        lockedAmount = lockedAmount.sub(ll.amount);

        uint transferAmount = amount > ll.amount ? ll.amount : amount;
        to.transfer(transferAmount);

        if (transferAmount <= ll.premium)
            emit Profit(id, ll.premium - transferAmount);
        else
            emit Loss(id, transferAmount - ll.premium);
    }

总结

以上就是Hegic V1中买方和卖方的主要流程和方法。Hegic 的优点是对买方操作相对简单,选择性自由度也高,但是代价也很严重。
其中有两个主要的问题:

  1. 对于流动性提供者,也就是卖方来说,面临退出困难,只要还有合约没有到期,退出就会有额外的损失。除非等到所有的期权都到期,但是如果这个参与的人数很多的情况下,而且期权是按照相对时间行权的,基本上很难做到。
  2. 对于期权的买方来说,看似有很高的灵活性,但是期权的定价权确是由项目方的公式决定的,但凡公式有一些偏向于卖方,在大量的数据情况下,买方都会面临不公平的风险。对于非专业人士还是谨慎考虑。

猜你喜欢

转载自blog.csdn.net/qq_36838406/article/details/118657390