Opium V2: open to all developers and ready for grants!
Programmable money is awesome, but programmable derivatives are the real holy grail of DeFi.
The Opium Protocol was one of the very first DeFi protocols pioneering the options space and, since then, it has provided the foundations to be the settlement layer for many successful financial engineering experiments - including but not limited to realt insurance, bridge insurance, credit default swaps, turbos.
With time, however, comes obsolescence. Some of the architectural choices that did indeed make sense at the time Opium v1 was first developed needed to be modernized with more up-to-date design patterns.
As the Mainnet launch of the Opium Protocol v2 nears, the Opium DAO welcomes all developers to experiment with the protocol and apply for grants if they have an interesting use-case.
Feel free to drop you proposal on the governance forum!
For this very purpose, the present article is the first of a series of tutorials that will showcase how to develop financial use-cases on top of the Opium Protocol v2.
While the present article aims to be just a quick walk-through of the nuts and bolts of the protocol through a quick mock option contract, future tutorials will be focused on the development of more 'real world' use-cases such as covered call and protective put strategies, more complex financial primitives and an introduction to our upcoming SDK.
One of the design choices that was scrapped was the usage of Opium’s very own token standard ERC 721o - a custom flavor of the more notorious ERC721- whose purpose was to allow for the creation of non-fungible derivative portfolios.
Nowadays a similar rationale has been implemented by the ERC1155 standard, however the degree of interoperability and easy-to-reason-about clean API that the good-old-fashioned ERC20 provides are simply undefeated.
As such, following a long-existing DeFi trend that was first popularized by Uniswap, Opium makes use of a factory pattern that mints ERC20 tokens in a measure proportional to the ownership of LONG/SHORT option positions of their respective owners.
However, as the scope of the article is to give enough of a rundown for a developer to get their feet wet with Opium v2, we will postpone a deep dive into the Opium Protocol architecture to a later date.
If you have any questions, feel free to drop by on the governance forum!
A quick options recap
First off, what do we mean by ERC20 LONG/SHORT option positions? Here's a very quick and generic refresher on financial options.
An option contract is a financial agreement between two parties, respectively a seller and a buyer, that enables a buyer to execute a trade at a later time if they so desire - which is to say, assuming that the buyer is a rational actor, they will always exercise their option at a profit and they will let it expire unexercised if worthless.
The trade can either consist in the right to acquire a given asset at a previously agreed-upon price or, similarly, in the right to sell a given asset at a previously agreed-upon price. In the first case the option contract is called call option and in the second case it is called put option. Conversely, a seller of an option contract has the obligation to fulfill a trade if the option contract's buyer decides to exercise the option: specifically, when the buyer exercises their option, the seller has the obligation to buy the option contract’s underlying asset in the context of a call option and the obligation to sell the same asset in the case of a put option.
If you want to learn more about options, check out the following resources:
Khan Academy's option course
Opium Finance academy
Deribit's option course
So, where does the Opium Protocol come into play? Think of Opium as a smart financial escrow that allows to encode the logic of complex financial assets - such as option contracts- in a flexible way which ensures that the parties exchanging those financial assets will fulfill their contractual obligations in a trustless fashion.
But enough financial jargon, we are programmers so let’s talk code.
Now that we know that two of the core properties of an option are strike price and maturity date, how can we describe a financial derivative in Solidity?
// Opium derivative structure (ticker) definition
struct Derivative {
// Margin parameter for syntheticId
uint256 margin;
// Maturity of derivative
uint256 endTime;
// Additional parameters for syntheticId
uint256[] params;
// oracleId of derivative
address oracleId;
// Margin token address of derivative
address token;
// syntheticId of derivative
address syntheticId;
}
The struct above is the schema that we use in the Opium Protocol.
Let’s quickly walk through it:
uint256 margin
: aka the reference collateral that is (usually) parsed by the IDerivativeLogic SyntheticId getMargin
function to set the collateral requirements of the option contract.
uint256 endTime
: aka the maturity date of the contract. It is the point in time at which the buyers will have the right to exercise their contract.
uint256[] params
: option contracts can differ greatly in the complexity of their financial requirements, as such Opium uses a dynamic array to ensure a high degree of flexibility to the consumers of the protocol, as to enable them to encode arbitrary parameters in their LibDerivative asset definition if needed. One convention to keep in mind is that, however, the first item of the params
array is always expected to be the strike price. The strike price is the value that determines the profitability of an option upon expiry: if the market price is above the strike price in case of a call option or the market price is below the strike price in the context of a put option, the option is said to be ‘in the money’ and the buyer stands to make a profit. Vice-versa, the option is said to be out of the money and the buyer stands to lose the premium or the collateral that they had to exchange for the ownership of the option contract.
oracleId
: oracles are the entities that inform the Opium Protocol about the value of an asset upon its maturity date. The Opium Protocol does not enforce any specification as for what can be used as an oracle, be it on-chain or off-chain.
token
: the address of the ERC20 token to be used as a collateral
syntheticId
: the address of the contract where the financial logic of the derivative is encoded. The Opium Protocol expects the syntheticId
contracts to be compliant with the specifications of the IDerivativeLogic
interface.
You might have noticed that the underlying asset of the option itself is not explicitly listed as a property of the struct: this is, yet again, to allow as much freedom as possible as to what can be used as an underlying for a contract. Had we done otherwise, the underlying's definition would have to be forcefully narrowed down to a uniform type that all the consumer SyntheticId contracts would have to adhere to. Furthermore, the strike price of an asset - whatever that asset may be- and the maturity of a contract are sufficient to carry on the execution of a derivative as long as the respective oracleId
is aware of which price feed to use to fetch the information about the relevant underlying after expiry.
Now that we know how to encode the description of a financial asset in the Opium Protocol, the remaining piece of the puzzle is how it's processed.
As a developer, the two main user-facing contracts that you'll be interacting with are Core and OracleAggregator.
Each financial instrument on the Opium protocol consists of two main building blocks: the data layer and the financial layer.
The OracleAggregator manages the data layer, which is responsible for informing the Opium Protocol about the pricing data required for the settlement of a syntheticId
. It does so by receiving the required data from a user-defined oracleId
, saving it in its own storage and then providing it on-demand to the Core contract.
The Core contract, instead, manages the financial logic of a syntheticId
- which includes the creation of new syntheticId's LONG/SHORT positions, their execution and the distribution of payouts according to the syntheticId's own logic and the settlement data provided by the OracleAggregator.
As you can see, each layer consists of a recipe made of a user-defined entity (oracleId
, syntheticId
) and an Opium v2 core contract which processes the related entity. The oracleId
will be processed by the OracleAggregator
and the syntheticId
will be processed by the Core
contract - via the SyntheticAggregator contract which acts as a caching middleware, but as an end developer you don't need to care about the implementation details!).
SyntheticId Life-Cycle
In the Opium v2 main repository, we have set up an example of the above workflow.
In order to run the end-to-end tests: you can follow the instructions in the README to run them on an Ethereum Mainnet fork.
The e2e workflow example mocks an AAVE/ETH PUT option.
At the time of writing AAVE is 0.06782ETH (currently ~212.90usd) and the strike price is set to 0.06819956ETH (currently ~220USD).
Now, for the purpose of the current example, we are using the ERC20 mock daiCollateralMock
as a collateral, which we will assume, similarly to the original DAI, to be worth roughly 1usd. The daiCollateralMock
amount to purchase one position of our PUT option is set to 14e18. If you are interested in the topic of option pricing, a good benchmark to evaluate the fair value of an option is by using the Black-Scholes formula.
The e2e workflow comprises of a OracleId, a SyntheticId and a mock Controller contract to interact with Core.
The derivative recipe sets the oracleId
address to a an oracle contract which uses Chainlink to fetch the AAVE/ETH price-feed.
/// @param _derivativeEndTime uint256 the maturity of the derivative contract that uses ChainlinkOracleSubId as its oracleId
function triggerCallback(uint256 _derivativeEndTime) external {
// fetches the data and the timestamp from the Chainlink pricefeed
(uint256 price, uint256 timestamp) = getLatestPrice();
// fetches the Opium.OracleAggregator from the Opium.Registry
IOracleAggregator oracleAggregator = registry.getProtocolAddresses().oracleAggregator;
// logs the relevant event
emit LogDataProvision(address(priceFeed), address(oracleAggregator), timestamp, price);
// pushes the data into the OracleAggregator
oracleAggregator.__callback(_derivativeEndTime, price);
}
As the test-case shows, the oracleId's responsibility will be to provide the required data to the OracleAggregator upon maturity date - attempting to do it before will result in an error!
it("ensures that the oracleId correctly pushes the required pricefeed data into `Opium.OracleAggregator` upon the derivative's maturity", async () => {
// timetravel slightly after the maturity of the derivative contract
await timeTravel(derivative.endTime + 100);
// the derivative's oracleId pushes the data required to settle the option contract into the OracleAggregator
const tx = await chainlinkOracleSubId.triggerCallback(derivative.endTime);
const receipt = await tx.wait();
const [oracleSubIdEvent] = decodeEvents<ChainlinkOracleSubId>(
chainlinkOracleSubId,
"LogDataProvision",
receipt.events,
);
await oracleAggregator.getData(chainlinkOracleSubId.address, derivative.endTime);
expect(oracleSubIdEvent._data, "wrong oracleAggregator data").to.be.eq(
await oracleAggregator.getData(chainlinkOracleSubId.address, derivative.endTime),
);
});
Similarly, the derivative schema also sets the syntheticId field to a contract that implements the IDerivativeLogic interface.
At the heart of a syntheticId contract there is the getExecutionPayout
function, which is where the custom payout logic is encoded - and can be used to implement all kinds of financial requirements: from vanilla call/put options to more exotic products.
function getExecutionPayout(LibDerivative.Derivative memory _derivative, uint256 _result)
external
view
override
returns (uint256 buyerPayout, uint256 sellerPayout)
{
uint256 strikePrice = _derivative.params[0];
// uint256 ppt = _derivative.params[1]; // Ignore PPT
uint256 fixedPremium = _derivative.params[2];
uint256 nominal = _derivative.margin;
uint256 sellerMargin = (nominal * part) / BASE_PPT;
// If result price is lower than strike price, buyer is paid out
if (_result < strikePrice) {
// buyer payout = margin * (strike price - result) / strike price
buyerPayout = nominal * (strikePrice - _result) / strikePrice;
if (buyerPayout > sellerMargin) {
buyerPayout = sellerMargin;
}
// seller payout = margin - buyer payout
sellerPayout = sellerMargin - buyerPayout;
} else {
// buyer payout = 0
buyerPayout = 0;
// seller payout = margin
sellerPayout = sellerMargin;
}
// Add fixed premium to seller payout
sellerPayout = sellerPayout + fixedPremium;
}
The basic life-cycle of the derivative encompasses the following stages:
- creation/minting of a derivative recipe's LONG/SHORT positions and tests:
- data provision from the OracleId to the OracleAggregator.
- execution of the derivative positions and the distribution of payouts.
If you have reached this far, congratulations!
You now know enough to start building on top of Opium v2 and be eligible for a developer grant.
About Opium
Opium Protocol is a universal and robust DeFi protocol that allows for creating, settling, and trading decentralized derivatives.
Explore Opium Protocol or try out Opium Finance.
Stay informed and follow Opium.Team on Twitter.
Did you know that you can subscribe to our News Channel to get more exciting news delivered to your morning coffee?
Top comments (0)