接入去中心化預言機Chainlink餵價開發DeFi看漲期權交易平台實例
DeFi這個大類下包含許多智能合約應用場景,如Chainlink餵價、流動性挖礦、流動性挖礦Chainlink餵價Chainlink餵價預言機在以太坊主網上用Solidity開發簡單的看漲期權DeFi交易平台。當然,你也可以將這個實例稍作修改,開發一個看跌期權交易平台。這個平台擁有一個強大的功能,那就是所有價值轉移都通過智能合約進行,交易雙方可以繞過中間方直接展開交易。因此,這個過程不包含任何第三方,只包含智能合約和去中心化的Chainlink餵價,這就是最典型的DeFi應用。開發一個去中心化期權交易平台將涵蓋以下內容:
在Solidity中對比字符串
將整數轉換成固定位數的小數
創建並初始化一個通證接口,比如LINK
在用戶/智能合約之間轉移通證
批准通證轉移
SafeMath
智能合約ABI接口
用require()執行交易狀態
以太坊msg.Value及其與通證價值交易的區別
在int和uint之間進行轉換
應付(payable)的地址
和
和GitHub和Remix上查看相關代碼。在我們正式開始前,先來簡單介紹一下什麼是期權合約。期權合約讓你有權選擇在某個期限前以約定的價格執行交易。具體而言,如果期權合約內容是買入股票或通證等資產,則被稱為看漲期權。另外,本文的示例代碼可以稍作修成看跌期權。看跌期權與看漲期權正好相反,其內容不是買入資產而是賣出資產。以下是期權相關的一些專有名詞:
行權價格:約定的資產買進/賣出價格
期權費用:購買合約時支付給賣家的費用
到期日:合約終止的時間
行權:買家行使其權利以行權價格買賣資產的行為
//溢出安全操作符
pragma solidity ^0.6.7;
import "https://github.com/smartcontractkit/chainlink/blob/develop/evm-contracts/src/v0.6/interfaces/LinkTokenInterface.sol";
import "https://github.com/smartcontractkit/chainlink/blob/master/evm-contracts/src/v0.6/interfaces/AggregatorV3Interface.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/SafeMath.sol";
contract chainlinkOptions {
//溢出安全操作符
using SafeMath for uint;
//餵價接口
AggregatorV3Interface internal ethFeed;
AggregatorV3Interface internal linkFeed;
//LINK通證接口
LinkTokenInterface internal LINK;
uint ethPrice;
uint linkPrice;
//預計算字符串哈希值
bytes32 ethHash = keccak256(abi.encodePacked("ETH"));
bytes32 linkHash = keccak256(abi.encodePacked("LINK"));
address payable contractAddr;
//期權以結構數組形式儲存
struct option {
uint strike; //Price in USD (18 decimal places) option allows buyer to purchase tokens at
uint premium; //Fee in contract token that option writer charges
uint expiry; //Unix timestamp of expiration time
uint amount; //Amount of tokens the option contract is for
bool exercised; //Has option been exercised
bool canceled; //Has option been canceled
uint id; //Unique ID of option, also array index
uint latestCost; //Helper to show last updated cost to exercise
address payable writer; //Issuer of option
address payable buyer; //Buyer of option
}
option[] public ethOpts;
option[] public linkOpts;
//Kovan餵價:https://docs.chain.link/docs/reference-contracts
constructor() public {
//以太幣/美元的Kovan餵價
ethFeed = AggregatorV3Interface(0x9326BFA02ADD2366b30bacB125260Af641031331);
//LINK/美元的Kovan餵價
linkFeed = AggregatorV3Interface(0x396c5E36DD0a0F5a5D33dae44368D4193f69a1F0);
//Kovan上的LINK通證地址
LINK = LinkTokenInterface(0xa36085F69e2889c224210F603D836748e7dC0088);
contractAddr = payable(address(this));
}
OpenZeppelin的SafeMathOpenZeppelin的SafeMathChainlink餵價
Chainlink餵價
Chainlink餵價
//返回最新的LINK價格
function getLinkPrice() public view returns (uint) {
(
uint80 roundID,
int price,
uint startedAt,
uint timeStamp,
uint80 answeredInRound
) = linkFeed.latestRoundData();
//如果這輪還沒有結束,則timestamp是0
require(timeStamp > 0, "Round not complete");
//價格永遠不會是負數,因此可以將int轉換成uint
一級標題
return uint(price);
}
一級標題
寫一個看漲期權合約
//允許用戶寫保持看漲期權
//接收的通證類型,行權價格(通證以美元計價,小數點後保留18位),期權費用(與通證小數點位數一樣),到期日(unix),合約中的通證數量
function writeOption(string memory token, uint strike, uint premium, uint expiry, uint tknAmt) public payable {
bytes32 tokenHash = keccak256(abi.encodePacked(token));
require(tokenHash == ethHash || tokenHash == linkHash, "Only ETH and LINK tokens are supported");
updatePrices();
if (tokenHash == ethHash) {
require(msg.value == tknAmt, "Incorrect amount of ETH supplied");
圖片描述
ethOpts.push(option(strike, premium, expiry, tknAmt, false, false, ethOpts.length, latestCost, msg.sender, address(0)));
} else {
require(LINK.transferFrom(msg.sender, contractAddr, tknAmt), "Incorrect amount of LINK supplied");
uint latestCost = strike.mul(tknAmt).div(linkPrice.mul(10**10));
linkOpts.push(option(strike, premium, expiry, tknAmt, false, false, linkOpts.length, latestCost, msg.sender, address(0)));
}
}
圖片描述
一級標題
一級標題
合約ABI接口
一級標題LinkToken圖片描述
圖片描述
一級標題
購買看漲期權
//購買看漲期權,需要通證,期權ID和付款
function buyOption(string memory token, uint ID) public payable {
bytes32 tokenHash = keccak256(abi.encodePacked(token));
require(tokenHash == ethHash || tokenHash == linkHash, "Only ETH and LINK tokens are supported");
updatePrices();
if (tokenHash == ethHash) {
require(!ethOpts[ID].canceled && ethOpts[ID].expiry > now, "Option is canceled/expired and cannot be bought");
//買家支付期權費
require(msg.value == ethOpts[ID].premium, "Incorrect amount of ETH sent for premium");
//賣家收到期權費
ethOpts[ID].writer.transfer(ethOpts[ID].premium);
ethOpts[ID].buyer = msg.sender;
} else {
require(!linkOpts[ID].canceled && linkOpts[ID].expiry > now, "Option is canceled/expired and cannot be bought");
一級標題
require(LINK.transferFrom(msg.sender, linkOpts[ID].writer, linkOpts[ID].premium), "Incorrect amount of LINK sent for premium");
linkOpts[ID].buyer = msg.sender;
}
}
一級標題
行使期權
//行使看漲期權,需要通證,期權ID和付款
function exercise(string memory token, uint ID) public payable {
//如果期權沒到期且還沒有被行使,則允許期權所有者行使
//要行使期權,買家需向賣家支付行權價格*數量的金額,並獲得合約中約定數量的通證
bytes32 tokenHash = keccak256(abi.encodePacked(token));
require(tokenHash == ethHash || tokenHash == linkHash, "Only ETH and LINK tokens are supported");
if (tokenHash == ethHash) {
require(ethOpts[ID].buyer == msg.sender, "You do not own this option");
require(!ethOpts[ID].exercised, "Option has already been exercised");
require(ethOpts[ID].expiry > now, "Option is expired");
//符合條件,進行付款
updatePrices();
//行權費用
uint exerciseVal = ethOpts[ID].strike*ethOpts[ID].amount;
//接入Chainlink餵價換算成以太幣
uint equivEth = exerciseVal.div(ethPrice.mul(10**10)); //將餵價的8位小數轉換成18位
//買家支付與行權價格*數量等值的以太幣,行使期權。
require(msg.value == equivEth, "Incorrect LINK amount sent to exercise");
//向賣家支付行權費
ethOpts[ID].writer.transfer(equivEth);
//向買家支付合約數量的以太幣
msg.sender.transfer(ethOpts[ID].amount);
ethOpts[ID].exercised = true;
} else {
require(linkOpts[ID].buyer == msg.sender, "You do not own this option");
require(!linkOpts[ID].exercised, "Option has already been exercised");
require(linkOpts[ID].expiry > now, "Option is expired");
updatePrices();
uint exerciseVal = linkOpts[ID].strike*linkOpts[ID].amount;
uint equivLink = exerciseVal.div(linkPrice.mul(10**10));
//買家行權,向賣家支付行權費
require(LINK.transferFrom(msg.sender, linkOpts[ID].writer, equivLink), "Incorrect LINK amount sent to exercise");
圖片描述
require(LINK.transfer(msg.sender, linkOpts[ID].amount), "Error: buyer was not paid");
linkOpts[ID].exercised = true;
}
}
圖片描述
示例:交易未滿足一個或以上條件時Remix輸出的結果。
一級標題
一級標題
取消合約/刪除資金
//允許賣家取消合約或從沒有成功達成交易的期權中退回資金。
function cancelOption(string memory token, uint ID) public payable {
bytes32 tokenHash = keccak256(abi.encodePacked(token));
require(tokenHash == ethHash || tokenHash == linkHash, "Only ETH and LINK tokens are supported");
if (tokenHash == ethHash) {
require(msg.sender == ethOpts[ID].writer, "You did not write this option");
//必須還沒有被取消或購買
require(!ethOpts[ID].canceled && ethOpts[ID].buyer == address(0), "This option cannot be canceled");
ethOpts[ID].writer.transfer(ethOpts[ID].amount);
ethOpts[ID].canceled = true;
} else {
require(msg.sender == linkOpts[ID].writer, "You did not write this option");
require(!linkOpts[ID].canceled && linkOpts[ID].buyer == address(0), "This option cannot be canceled");
require(LINK.transferFrom(address(this), linkOpts[ID].writer, linkOpts[ID].amount), "Incorrect amount of LINK sent");
linkOpts[ID].canceled = true;
}
}
//允許賣家從到期、未行使以及未取消的期權中贖回資金。
function retrieveExpiredFunds(string memory token, uint ID) public payable {
bytes32 tokenHash = keccak256(abi.encodePacked(token));
require(tokenHash == ethHash || tokenHash == linkHash, "Only ETH and LINK tokens are supported");
if (tokenHash == ethHash) {
require(msg.sender == ethOpts[ID].writer, "You did not write this option");
//必須是到期、未行使且未取消的狀態。
require(ethOpts[ID].expiry <= now && !ethOpts[ID].exercised && !ethOpts[ID].canceled, "This option is not eligible for withdraw");
ethOpts[ID].writer.transfer(ethOpts[ID].amount);
//將取消標誌修改為true,避免多次贖回
ethOpts[ID].canceled = true;
} else {
require(msg.sender == linkOpts[ID].writer, "You did not write this option");
require(linkOpts[ID].expiry <= now && !linkOpts[ID].exercised && !linkOpts[ID].canceled, "This option is not eligible for withdraw");
require(LINK.transferFrom(address(this), linkOpts[ID].writer, linkOpts[ID].amount), "Incorrect amount of LINK sent");
linkOpts[ID].canceled = true;
}
}
隨著市場波動,如果期權還沒賣出去,賣家可能會取消期權合約並贖回資金。同樣地,期權如果一直未行使就到期了,賣家肯定會想要贖回合約中的資金。因此,我們添加了cancelOption()和retrieveExpiredFunds()函數
這兩個函數最關鍵的一點是必須滿足贖回條件才能調用成功。賣家要贖回資金必須滿足特定的條件,而且只能贖回一次。賣家不能取消已經被賣出的合約,因此我們要確認買家地址仍然是初始值0。另外,我們還要確認期權還未被取消,然後再退款。如果是期權到期後再贖回資金,那情況就會稍有不同。這種情況下,期權可能已經賣出去但沒有行使,資金仍應被退還給賣家。我們要確認合約已經到期並且還未被行使。然後也要將期權的取消標誌設置為true,如果條件滿足則進行退款。
希望本文能幫助各位立刻在主網上開發Chainlink用例,並讓各位了解了Solidity獨特的功能。如果你想了解更多的Chainlink功能,請查看Chainlink VRF(可驗證隨機函數),或查看Chainlink公允排序服務,了解Chainlink如何解決礦工搶跑問題。


