概述
加密货币的期权交易平台,目前中心化的如Deribit, 去中心化的则是Opyn和Hegic比较靠前。 这里我们仅对Hegic的V1版本做简要的分析,如果有分析不到位,或者错误的地方还请评论留言一起交流和讨论。
Hegic 的主要理念是提供了一个流动性池(Liquidity Pool),使得流通性的提供者形成收益共享,风险共担的关系。
期权的卖方
流动性的提供者,称为writer,添加完流动性后自动成为卖方。卖方对期权没有任何选择,只是添加完流动性,等待买方购买,收益就是买方的权利金。
卖方的行为
卖方可以有两个动作,一个是添加流动性(合约访问为provide),就是把资产交出去,另外一个是提款(合约方法为withdraw),也就是把资产拿回来,这里权利金的收益也会与添加的资产合并在一起。 这里我们选取HegicETHPool.sol来说明一下
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);
}
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的数量。
- 方法第一步记录了添加流动性时的区块时间。(Hegic的期权时间使用的都是相对时间这点也是和Opyn与Deribit的不一样的地方)
- 计算writeToken的流通量totalSupply(),和资产池中的剩余量 totalBalance()(这个剩余量是当前合约的ETH的数量-因合约卖出被锁定的资产数量),其中资产的剩余量是包含了买家的行权金的。
根据这两个值,来计算出当前添加的资产数量,应该铸造出多少的writeToken的数量。 如果是第一次,会乘以一个默认的初始值 uint256 public constant INITIAL_RATE = 1e3; 如果不是第一次,就是msg.value*目前writeToken的流通量/目前ETH的流动性。 - 计算出这个最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 换取池子中相应比例的流动性资产。
- 取出的第一个条件是锁定周期要大于两周
uint256 public lockupPeriod = 2 weeks; - 第二个条件是取出的金额要小于当前流动性所能提供的金额availableBalance(),这个的数值是减去锁定的权利金和锁定的底层资产的数量。
- 第三是计算本次提取要销毁的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的绑定关系从一个账户换到另外一个账户。
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);
}
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;
}
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);
}
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,这些都是由买方决定,合约会给买家根据这些参数给出一个期权的价格,如果买家觉得合适就买,觉得不合适就不买。
- 第一步就是进行fees的计算,settlementFee是收取amout的百分之1,这个就是手续费,收完之后添加到了质押池子里,用于给提供流动性的卖家分代币,以作为项目代币的价格支撑。
strikeFee 通常是0。 还有一个premium,也就是权利金 total - settlementFee,除了手续费,就是就是权利金了,这部分会发生到pool合约,作为后面给卖方提取的部分。 - 接下来就是校验一下参数有没有不合法的。
- 将参数封装进Option,创建一个期权。
- 将权利金发送给池合约,并且在锁定池中amount数量的质押资产。
以上期权合约就购买完成了。这里的核心是fees公式的计算,如果这个公式计算的金额小了,则会对买方有益,如果计算的大了,那么则会对卖方有益。这个公式就像一个天平,它的是否公平决定这个项目是否公平。我不是金融专业的,fee的计算公式也看不懂,这里就不多说了。
exercise
行权方法就是到了行权日,买方觉得赚了就去行权,亏了就不行权。
- 行权时合约会调用外部的预言机
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 的优点是对买方操作相对简单,选择性自由度也高,但是代价也很严重。 其中有两个主要的问题:
- 对于流动性提供者,也就是卖方来说,面临退出困难,只要还有合约没有到期,退出就会有额外的损失。除非等到所有的期权都到期,但是如果这个参与的人数很多的情况下,而且期权是按照相对时间行权的,基本上很难做到。
- 对于期权的买方来说,看似有很高的灵活性,但是期权的定价权确是由项目方的公式决定的,但凡公式有一些偏向于卖方,在大量的数据情况下,买方都会面临不公平的风险。对于非专业人士还是谨慎考虑。
|