Connect to the decentralized oracle machine Chainlink to feed prices and develop a DeFi call option trading platform example
The broad category of DeFi includes many smart contract application scenarios, such asblockchain voting、Liquidity mining、Liquidity miningChainlink price feedChainlink price feedOracle uses Solidity to develop a simple call option DeFi trading platform on the Ethereum mainnet. Of course, you can also slightly modify this example to develop a put option trading platform. This platform has a powerful function, that is, all value transfers are carried out through smart contracts, and both parties to the transaction can directly conduct transactions bypassing the middle party. Therefore, this process does not involve any third parties, only smart contracts and decentralized Chainlink price feeds, which are the most typical DeFi applications. Developing a decentralized options trading platform will cover the following:
Compare strings in Solidity
converts an integer to a fixed number of decimals
Create and initialize a token interface, such as LINK
Transfer tokens between users/smart contracts
Approve token transfer
SafeMath
Smart contract ABI interface
Execute transaction state with require()
Ethereum msg.Value and its difference from token value transactions
Convert between int and uint
payable address
Finally, use the Chainlink data aggregator interface to obtain DeFi price data
andGitHubandRemixCheck out the relevant code. Before we officially start, let's briefly introduce what an option contract is. Options contracts give you the option to execute a trade at an agreed price by a certain date. Specifically, if the content of the option contract is to buy assets such as stocks or tokens, it is called a call option. In addition, the sample code in this article can be modified slightly to put options. A put option is just the opposite of a call option, and its content is not to buy an asset but to sell it. The following are some proper nouns related to options:
Strike price: the agreed buying/selling price of the asset
Option Fee: The fee paid to the seller when buying the contract
Expiration Date: The time when the contract ends
Strike: The act of a buyer exercising his right to buy or sell an asset at the strike price
Whether you are developing a call or a put option, you need the basic elements of imports, constructors, and global variables.
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 {
// overflow safe operator
using SafeMath for uint;
// price feed interface
AggregatorV3Interface internal ethFeed;
AggregatorV3Interface internal linkFeed;
//LINK token interface
LinkTokenInterface internal LINK;
uint ethPrice;
uint linkPrice;
//Precompute string hash value
bytes32 ethHash = keccak256(abi.encodePacked("ETH"));
bytes32 linkHash = keccak256(abi.encodePacked("LINK"));
address payable contractAddr;
//Options are stored in the form of a structure array
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 feed price: https://docs.chain.link/docs/reference-contracts
constructor() public {
// Kovan price feed for ETH/USD
ethFeed = AggregatorV3Interface(0x9326BFA02ADD2366b30bacB125260Af641031331);
//LINK/USD Kovan feed price
linkFeed = AggregatorV3Interface(0x396c5E36DD0a0F5a5D33dae44368D4193f69a1F0);
//Link token address on Kovan
LINK = LinkTokenInterface(0xa36085F69e2889c224210F603D836748e7dC0088);
contractAddr = payable(address(this));
}
When importing, we need to access Chainlink’s data aggregator interface to realize the price feed function, and access the LINK token interface (Note: Here we need to use LINK to transfer money, so we need to use the ERC20 function of the token contract). Finally, we importSafeMath by OpenZeppelinContracts, which are standard library operations that perform built-in overflow checking, while Solidity does not include overflow checking in its built-in operators.
Chainlink price feed
Chainlink price feed
//Return the latest LINK price
function getLinkPrice() public view returns (uint) {
(
uint80 roundID,
int price,
uint startedAt,
uint timeStamp,
uint80 answeredInRound
) = linkFeed.latestRoundData();
//If this round has not ended yet, timestamp is 0
require(timeStamp > 0, "Round not complete");
//The price will never be negative, so you can convert int to uint
//The price has 8 digits after the decimal point, and then needs to add 10 digits to become 18 digits.
return uint(price);
}
first level title
Write a call option contract
//Allow users to write held call options
// Received token type, exercise price (token is denominated in US dollars, with 18 digits after the decimal point), option fee (same as the token’s decimal point), expiration date (unix), token in the contract quantity
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");
uint latestCost = strike.mul(tknAmt).div(ethPrice.mul(10**10)); //Exercise cost denominated in ether, decimal point adjustment
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)));
}
}
image description
Write a LINK option contract for a LINK token, set the Unix expiration time, the exercise price is $10, and the option fee is 0.1 LINK.
first level title
Contract ABI interface
When you view the contract on Etherscan, there will be two tabs, namely: Read Contract and Write Contract. You can use these two tabs to interact with the contract. For example: LINK token main network contract. Etherscan knows what these functions are and how to call them via the contract's ABI. Use the JSON format to call the ABI and specify the function call parameters. It can be called directly on the main network, but the LINK contract on Kovan needs to import this module. you can inLinkTokenimage description
image description
first level title
buy call options
//To buy a call option, you need token, option ID and payment
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");
//Buyer pays option premium
require(msg.value == ethOpts[ID].premium, "Incorrect amount of ETH sent for premium");
//The seller receives the option fee
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");
//Transfer option premium from buyer to seller
require(LINK.transferFrom(msg.sender, linkOpts[ID].writer, linkOpts[ID].premium), "Incorrect amount of LINK sent for premium");
linkOpts[ID].buyer = msg.sender;
}
}
first level title
exercise option
//Exercise call option, need token, option ID and payment
function exercise(string memory token, uint ID) public payable {
//If the option has not expired and has not been exercised, the option owner is allowed to exercise
//To exercise the option, the buyer needs to pay the seller the amount of exercise price * quantity, and get the number of tokens agreed in the contract
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");
// meet the conditions, make payment
updatePrices();
// Exercise fee
uint exerciseVal = ethOpts[ID].strike*ethOpts[ID].amount;
//Connect to Chainlink to feed the price and convert it into Ethereum
uint equivEth = exerciseVal.div(ethPrice.mul(10**10)); //Convert the 8 decimal places of the feed price to 18
//The buyer pays the ether equivalent to the strike price*quantity to exercise the option.
require(msg.value == equivEth, "Incorrect LINK amount sent to exercise");
//Pay the exercise fee to the seller
ethOpts[ID].writer.transfer(equivEth);
//Pay the buyer the contract amount of ether
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));
//The buyer exercises the option and pays the exercise fee to the seller
require(LINK.transferFrom(msg.sender, linkOpts[ID].writer, equivLink), "Incorrect LINK amount sent to exercise");
//Pay the contract amount of LINK tokens to the seller
require(LINK.transfer(msg.sender, linkOpts[ID].amount), "Error: buyer was not paid");
linkOpts[ID].exercised = true;
}
}
image description
Example: The result of Remix output when the transaction does not meet one or more conditions.
If the conditions are met, the exercise fee will be paid to the seller, and the contract number of tokens will be paid to the buyer. When exercising the option, the buyer needs to purchase each token at the exercise price. However, the strike price is denominated in USD, while the contract size is denominated in Ether or LINK. Therefore, we need to access the Chainlink price feed to calculate the amount of ETH or LINK equivalent to the exercise fee. After converting to equivalent ETH or LINK, we can start the transfer. When transferring money, you need to use the method mentioned above, that is, the ether currency will call the msg.value/address.transfer function, and LINK will call the transferFrom() function.
first level title
Cancel contract/delete funds
// Allow sellers to cancel contracts or get money back from options that did not successfully close the trade.
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");
// must not have been canceled or purchased
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;
}
}
//Allow sellers to redeem funds from expired, outstanding, and non-cancelled options.
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");
//Must be expired, not exercised and not cancelled.
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);
//Modify the cancel flag to true to avoid multiple redemptions
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;
}
}
As the market fluctuates, sellers may cancel the option contract and redeem their funds if the option has not been sold. Likewise, if an option expires without being exercised, the seller will certainly want to redeem the funds in the contract. Therefore, we added the cancelOption() and retrieveExpiredFunds() functions
The most critical point of these two functions is that the redemption conditions must be met to call successfully. Sellers must meet certain conditions in order to redeem funds, and can only redeem once. The seller cannot cancel the contract that has been sold, so we need to confirm that the buyer's address is still the initial value 0. In addition, we also need to confirm that the option has not been cancelled, and then refund. If the funds are redeemed after the option expires, the situation will be slightly different. In this case, the option may have been sold but not exercised and the funds should still be returned to the seller. We want to confirm that the contract has expired and has not been exercised. Then also set the option's cancel flag to true to make a refund if the condition is met.
I hope this article helps you develop Chainlink use cases on mainnet right away, and gives you an understanding of Solidity's unique capabilities. If you want to learn more about Chainlink features, check outChainlink VRFChainlink Fair Ordering ServiceChainlink Fair Ordering Service, learn how Chainlink solves the miner frontrunning problem.
and join us atdeveloper documentationand join us atDiscordcontact us.click herecontact us.


