Build a call option on Opium

Riccardo Biosas
Opium
Published in
7 min readSep 27, 2021

The Opium protocol is a universal protocol to create, settle and trade virtually all derivatives and financial instruments in a professional and trustless way. It allows anyone to build custom exchange-traded products on top of the EVM-Compatible blockchains. Once created, they can be traded freely via a network of relayers and will be priced according to supply and demand.

In our tutorial we will walk through the logic of a basic call option on ETH as an underlying.

If you are not familiar with the concept of call option, have a look at this introductory article.

You can find all the code in this tutorial on github. As our repository uses the hardhat’s forking feature to run our tests against the Opium Protocol’s Ethereum Mainnet contracts, we will need an Infura project ID — or any other node service provider of your choice, you name it. Set your Infura project id and mnemonic in a .env file with INFURA_API_KEY and MNEMONIC as the respective environment variables.

Next, let’s clone the repository and try to run the tests.

git clone git@github.com:RiccardoBiosas/opium-call-option-example.git
cd opium-call-option-example
yarn install
yarn test

Assuming everything has been set up correctly, all unit tests should pass with a green check mark. Congrats!

Code walk-through

At the core of each financial product built on top of the Opium Protocol there are two main components: the derivative recipe and the oracle recipe. The former is responsible for encoding the financial specifications of the product, whereas the latter is responsible for defining the data source that should be used to settle the product at expiry.

The structure of these two components need to adhere respectively to the IDerivativeLogic and IOracleId interfaces. In our example repository the contract implementing the IDerivativeLogic interface is OptionCallSyntheticIdMock and the contract implementing the IOracleId interface is the AdminOracleController.

However, the OptionController contract is where all the interactions with the Opium Protocol’s Core contract reside. The Opium Protocol’s Core contract is responsible for most of the financial logic processed by the Opium Protocol.

Before moving forward, bear in mind that all the code showcased here was written to illustrate the life-cycle of a basic option product on the Opium Protocol, so it goes without saying that it’s not meant to be run in a production environment!

Having said that, let’s start breaking down the OptionController chunk by chunk!

The OptionController inherits LibDerivative, a contract which defines the blueprint of a Derivative and getDerivativeHash — a function to return the footprint of an Opium’s financial product based on the hash of its packed parameters.

In the constructor, we pass the address of the Opium’s Registry contract. The Registry contract allows us to fetch and then store the addresses of all the Opium contracts that we need to implement our basic call option’s workflow. We’ve already introduced the Core contract, the other two contracts are the SyntheticAggregator and the TokenSpender. The SyntheticAggregator contract is responsible for caching and initializing a given financial product’s data, whereas the main responsibility of the TokenSpender contract is to manage the transfer of ERC20 tokens (i.e: at the moment of a derivative’s creation where an ERC20 token represents the product’s collateral or when a derivative is being settled and therefore the payout is being forwarded to the rightful party).

Then we set up our own derivative by calling the setDerivative function, which takes as an argument a LibDerivative.Derivative and stores it on the contract’s scope. Note that normally we wouldn’t need to do that, and that obviously in our example the owner could override the derivative variable in the middle of its life-cycle — but again, for the sake of breaking everything down step by step we will not concern ourselves with any practical design consideration.

Now that we’ve got our derivative, we are ready to mint its positions.

We call the create function. The OptionController’s create function is a wrapper around the Core’s create function. As an argument, it takes the _amount of derivatives (LONG/SHORT positions) that will be minted and the addresses of the buyer and the seller. Do note that in financial literature the buyer is associated with a LONG position and the seller is associated with a SHORT position.

The LONG/SHORT positions being minted are based upon the derivative that we have previously defined with the setDerivative function. In order for our create function call to be successful, the Core contract must be provided with the the appropriate margin. The required margin of our derivative is computed by the _computeMarginRequirement helper function, which adds up the long position’s and short position’s margins and multiplies them by the requested quantities.

And that’s it! We just minted our first positions.

Let’s now have a look at the callOptionWithDAICollateral tests.

First things first, here’s the derivative recipe of our ETH call option:

Here we specify the required margin, the maturity, the strike price, the address of the OracleId — our oracle recipe, the address of the token used as a margin and the address of the SyntheticId — our financial recipe.

In the tests workflow, the optionSeller account locks 400 DAI (derivative recipe’s margin * amount) in the Opium Protocol and receives an equal amount of LONG/SHORT ERC721O tokens. The seller is said to be market neutral until she exchanges her LONG positions with a buyer for 20 DAI.

Subsequently, now the seller holds only SHORT ERC721O tokens and the buyer holds only LONG ERC721O tokens.

The Opium Protocol supports European options. This means that any option created on top of the protocol can only be settled at expiry. As the expiry of our derivative is hardcoded to be 40 minutes in the future since its creation, in our tests we time-travel after the expiry so that our seller and buyer can exercise their positions.

Once again, for the sake of clarity we split the logic pertinent to the execution of the LONG/SHORT positions into two separate functions in the OptionController contract: respectively executeShort and executeLong.

Both functions are a wrapper around core.execute and they take the amount of positions to be exercised as an argument. Within their body, they calculate the hash of our derivative via LibDerivative.getDerivativeHash . Subsequently, they calculate the appropriate token id. For that purpose, we are using the LibPosition library which we attached to the bytes32 type. And now that we have computed on-chain all the necessary parameters, we can finally call core.execute !

Back to our callOptionWithDAICollateral tests, note that since both the seller and buyer accounts are interacting with the OpiumOptionController rather than directly interacting with the Opium Core contract, we need to enable the ‘third-party’ execution of the derivative by calling allowThirdParty. More generally, whenever the msg.sender is different from the EOA that holds a position, then calling allowThirdParty is necessary.

For a derivative to be settled, the data necessary to determine its outcome must be available (i.e: the current market price of the underlying against the strike price). In the Opium Protocol, the source of that data is the OracleAggregator. Normally — and ideally- the OracleAggregator would receive the required data from an on-chain oracle — such as Chainlink, UMA etc.-, but as the OracleAggregator was designed to be implementation-agnostic, in our example workflow we make use of AdminOracleController contract which receives off-chain data from its owner and then pushes it to the OracleAggregator. In our unit test we push a market price that is lower than the strike price, resulting in a favorable outcome for the seller of our ETH call option. The specifics of the payout of an Opium’s option are fully customizable and are defined in the getExecutionPayout of the contract implementing IDerivativeLogic.

In the case of our ETH call option, the seller received back the initial margin allocated to mint the LONG/SHORT positions. As the seller sold her LONG positions for 20 DAI, her profit amounts to 20 DAI. On the other hand, our buyer bought his LONG positions for 20 DAI, which corresponds to his net losses.

Vice versa, if the call option had expired in the money, the buyer’s payout would have amounted to the intrinsic value of the option unless the difference between the underlying’s market price and the strike price is greater than the allocated margin — in which case the buyer’s payout would be the entire option’s margin. As a result, the seller’s net loss would be 380DAI (margin * amount — paymentFromBuyer ) and the buyer would scoop up 380DAI as a profit!

Congrats, you built your first basic option call on top of the Opium Protocol!

Now that you got your feet wet, get in touch with the Opium community on the governance forum, the Opium DAO is eager to give grants to DeFi builders with interesting use-cases!

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?

--

--

Riccardo Biosas
Opium
Writer for

Principal Security Engineer @Procore | Founder @AgorApp | prev. Protocol Engineer@LivepeerOrg & Fullstack/Lead Smart Contract Dev @Opium_Network