初始設定完成並接入喂價後,我們接下來就可以呼叫函式了,先來寫一個期權合約。賣家呼叫writeOption函式,並填入期權具體的引數,小數點後保留18位。這裡必須要明確小數點位數,以確保合約中使用的所有引數都格式統一。比如,整數777沒有小數點,但是如果我們規定的邏輯是保留兩位小數,則表示成7.77。我們這裡的規則是小數點後保留18位,因為以太幣和LINK都是18位小數。如果小數點後不到18位,則可以新增0變成18位。接下來,我們就可以第一次使用之前計算出的以太幣和LINK字串雜湊值。為了明確賣家的期權合約針對的是什麼通證,我們需要比較字串。然而Solidity不支援在字串之間進行==操作,因為其長度是動態的。我們不需要寫一個函式一個個位元組地比較字串,而只需用keccak256雜湊函式計算每個字串的32位雜湊值,並直接對比。只要雜湊值一樣,字串就一樣。現在我們知道賣家用的是哪種通證,就可以有的放矢了。如果是以太幣,我們就可以用msg.value確認轉賬到期權合約的以太幣數量是否正確。我們可以用require()函式嚴格執行。如果require的第一個欄位為false,則交易會被拒絕,無法進行下去。這樣一來,我們可以確保所有期權合約的轉賬都完全符合之前約定的金額(tknAmnt)。檢查透過後,我們就可以建立期權合約,提供所有必須的欄位生成結構。基於當前以太幣價格,使用SafeMath函式而非內建運算子計算當前行使期權的費用(LatestCost)。使用Chainlink的updatePrices() helper函式獲取當前價格,這個函式會更新全域性以太幣和LINK價格。注意ethPrice要乘以10的10次方。這樣做是因為Chainlink喂價返回的是8位小數的美元價格,但正如上文所述,我們現在的標準是18位小數。所以新增10個零可以將其調整成18位小數,符合以太幣和LINK通證的格式。最後,我們將期權的結構壓入ethOpts的陣列中,這樣期權合約就寫完了,而且裡面有足夠的資金。
針對一枚LINK通證寫一個LINK期權合約,設定Unix到期時間,行權價格為10美元,期權費用為0.1個LINK。
如果是LINK期權合約,那麼就需要做一些修改了。Msg.value只提供交易中以太幣的金額。因此如果要確保LINK的金額充足,我們需要直接接入LINK通證合約。我們之前已經匯入並初始化了LINK通證介面,因此可以訪問所有LINK通證函式,其中一個是transferFrom(),這個函式可以將LINK從一個地址轉移到另一個地址。當然,我們不能讓任何合約都可以隨便轉移你的LINK資產,所以必須首先呼叫LINK的approve()函式,並具體說明允許轉移的LINK數量以及轉移到的合約地址。
合約ABI介面
當你在Etherscan上檢視合約時,會出現兩個tab,即:Read Contract和Write Contract。你可以用這兩個tab與合約進行互動。比如:LINK通證主網合約。Etherscan知道這些函式是什麼以及如何透過合約的ABI呼叫函式。使用JSON格式呼叫ABI,規定函式呼叫引數。在主網上可以直接呼叫,但是在Kovan上的LINK合約需要匯入這個模組。各位可以在LinkToken的Github上檢視ABI。所幸,在生產系統中,這些都可以用web3js的介面來處理,使用者可以用一個簡單的MetaMask請求來進行批准。但在我們這個開發例項中,暫時需要手動操作。
購買看漲期權
//購買看漲期權,需要通證,期權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;
}
}
現在期權合約建立完成且資金充足。接下來就等人來買了!買家只需表明購買以太幣或LINK期權的意願以及期權ID即可。由於期權陣列被定義成公開的,因此可以直接檢視,無需支付gas費,買家可以檢視所有期權合約及其ID欄位。選擇完期權合約後,我們再次呼叫require()函式驗證期權費用的支付金額是否正確。這次,我們不僅需要確認msg.value(僅針對以太幣),還需要將期權費用轉給賣家。Solidity中的所有以太幣地址都有一個address.transfer()函式,我們呼叫這個函式將期權費用從合約轉賬給賣家。然後設定期權合約的買家地址欄位,就完成購買了!如果是LINK的話,操作就稍微簡單一些。可以用transferFrom函式直接將買家的期權費轉賬給賣家(注:需要先批准)。如果是以太幣的話,期權費則需要先經過合約再到賣家地址。
行使期權
//行使看漲期權,需要通證,期權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");
//向賣家支付合約數量的LINK通證
require(LINK.transfer(msg.sender, linkOpts[ID].amount), "Error: buyer was not paid");
linkOpts[ID].exercised = true;
}
}
對於期權所有者來說,以太幣或LINK的價格如果超過行權價格,就能獲利。這樣一來,他們便願意行使期權,以行權價購買通證。這次我們必須先確認幾個條件,即:合約由訊息傳送者所有;合約還未行權;以及現在期權還沒到期。如果以上任何一個條件不滿足,則撤回交易。
如果條件都滿足,則向賣家支付行權費,並向買家支付合約數量的通證。行權時,買家需以行權價購買每一個通證。然而,行權價是以美元計價,而合約數量是以以太幣或LINK計價。因此我們需要接入Chainlink喂價計算與行權費等值的以太幣或LINK數量。換算成等值的以太幣或LINK後,我們就可以開始轉賬了。轉賬時需使用之前提過的方法,即以太幣會呼叫msg.value/address.transfer函式,LINK則呼叫transferFrom()函式。
以上就是成功行使期權的完整交易過程。LINK價格是11.56美元,合約行權價格是10美元,數量1個LINK。也就是說,買家只需要花10美元而不是11.56美元購便可購買一個LINK。10/11.56 = 0.86,即買家只需要花0.86個LINK就可以獲得1個LINK。算上0.1LINK的期權費用,總共獲利0.04LINK。
取消合約/刪除資金
//允許賣家取消合約或從沒有成功達成交易的期權中退回資金。
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如何解決礦工搶跑問題。