Derivatives and Options For Bitcoin

Day 23: Rubin's Bitcoin Advent Calendar

Welcome to day 23 of my Bitcoin Advent Calendar. You can see an index of all the posts here or subscribe at judica.org/join to get new posts in your inbox

In today’s post we’re going to talk about derivatives and options. Hoooo Boy!

Let’s define an Option:

An option is a contract that gives the holder the right to take an action to the detriment of a counterparty. Options can be created for payment.

For example, I might say to you, “hey! I heard you’re pretty good at coming up with memes. I’d like to pay you $10 for the option to buy the next meme you make supporting OP_CTV for $100.”1 You might say “OK”, and then make a meme. I think it’s awful and I tell you to go away. I’m out the $10, but not $110! I can’t post the meme though or my friends will think I’m lame for sharing Right Clicked Content. Or, if I think it’s great, I can pay you the $100 and then I’m off to the races.

There are a few different types of Option contract to think about:

Call v.s. Put.

A Call option we get the right to buy something later for a fixed price (like a preorder).

A Put option we get the right to sell something later for a fixed price (like insurance).

Here’s how to remember it:

Think about a nice puppy. You Call the puppy to you, and give him a cookie, head pat, and a “good boy”.

Think about a naughty puppy. You take away your chewed up sneaker, and Put her in her crate. “Bad girl”.

American v. European

American options you can settle at any time before it “expires”.

European options you must settle during a specific window after it expires, and before the window expires.

Think of European options like a restaurant reservation. You can no-show if you want, but you can show up between 7:00 and 7:15 and be seated.

American options are like a hold on jacket you think is beautiful at Saks Fifth Ave. You could come and buy it later today, tomorrow even! But wait a week and someone else will buy it because they put it back on the rack.

Collateralized v. Non Collateralized

A collateralized option means that the asset is actually there.

For example, think of Jerry Seinfeld renting a car. He reserved the car (buying a call option), but they didn’t have a car when he showed up. They knew how to take the reservation, but not keep it.

This is an example of a ‘naked short sale’ of the car rental, because it wasn’t backed by an actual car to rent.

I don’t have a proof, but we can’t really build that kind of thing in Bitcoin.

However, imagine if Jery had to, in order to make his reservation, make a deposit for the entire value of the reservation. Sure, he could get a refund, but then he would have an opportunity cost of which options. Imagine you’re taking your partner out for a suprise dinner that’s going to cost $1000, but you don’t know which one is going to be better so you reserve two restaurants and cancel one you decide not to go to for a 1% penalty. If you had to deposit, it would cost you $10 for the no-show, but you’d have to put up $2000 to hold the reservations! If instead, you paid both restaurants $10 up front, then you would only need to lock up $20 instead. Much more efficient!

These, we will build.

The Optimal Strategy for Pricing Options

Just kidding. I have no idea. It’s a complex subject, but some people are OK at it. This post is just plumbing.

What’s Derivative?

Your humor…. burnnnn. Just kidding.

A derivative is uhhh… well. It’s anything that isn’t the thing?

A derivative is a way of taking a real thing (e.g. a ton of corn, an NFT, an Apple Stock) and then either wrapping it or observing it in some other financial product.

In fact, Options themselves are Derivatives! Wacky, right? It’s a thing (e.g. a car) and then the right to buy that car has a price and a value that is a function of what the car is, but a lot of other factors too. So the option isn’t the car, but it’s connected.

An option is a “Real Derivative” because it is actually connected to the car that is bought or sold. But we can also make “Synthetic Derivatives” that just measure some external quantity (somehow) and then give you some amount of value in return. For example, I could make a synthetic option that mints an NFT of a car instead of the actual car. Or I could make a synthetic derivative that measures the price of the car over the last month and gives me that value in Bitcoin at the end of the month.

For synthetics, they have to be over collateralized to cover all outcomes. E.g., if we expect the car to be $50,000, but it might go to $100,000, we have to lock up $100,000. And if the price is $200,000, well our max profit is $100k then.

The Options we saw earlier were very binary in outcome. Synthetic derivatives like these can emulate any function, discontinuous or continuous. So you could have a contract, for example, that pays out on a sinusoidal wave based on the car price. Trippy.

Wheres the info come from?

Well, multiple places. We could get it from a third party (maybe using an attestation chain of some sort?), or there are certain ways it could be self-referential (like for powswap).

Let’s See Some Code

Synthetic Derivatives

I love the children equally, so let’s start with Derivatives now.

First let’s define an Oracle Interface who provides us data. All the Oracle does is, given a Symbol (some request), gives us a Clause they will help us satisfy if the Symbol is true, and something else if it is false. Imagine a symbol that you can query an oracle for questions such as “is a Bitcoin worth more than $50k”.

/// Placeholder type for a standard way of looking up a stock symbol; can be defined more
/// concretely but should have a human readable string representation.
pub type Symbol = String;
/// Oracle is a generic wrapper for any logic to get a pair of binary clauses.
/// It can be based on hash preimage, federated signers, or key revealing.
/// The Trait Object can be responsible for network requests/caching.
pub trait Oracle {
    /// returns keys (price lo, price hi) for the given query
    fn get_key_lt_gte(&self, t: &Symbol, price: i64) -> (Clause, Clause);
}

Now let’s define a threshold oracle – we wouldn’t want to trust just one lousy oracle, so let’s trust M out of N of them!

/// An Oracle can also be "composed" into a threshold scheme with other
/// oracles quite easily as below...
///
/// Under *certain* circumstances, composition could be optimized (e.g., schnorr keys)
pub struct ThresholdOracle {
    /// the list of price oracles to consult
    pub oracles: Vec<Box<dyn Oracle>>,
    /// how many oracles must agree
    pub thresh: usize,
}

impl Oracle for ThresholdOracle {
    fn get_key_lt_gte(&self, t: &Symbol, price: i64) -> (Clause, Clause) {
        let (l, r) = self
            .oracles
            .iter()
            .map(|o| o.get_key_lt_gte(t, price))
            .unzip();
        (
            Clause::Threshold(self.thresh, l),
            Clause::Threshold(self.thresh, r),
        )
    }
}

The underlying clauses could really be anything… we can even (with some tweaks to Sapio I’d LOVE to get working, but need to engineer) make this represent Discrete Log Oracles with 2 counterparties and an external Oracle. If that means something to you, good, otherwise you can ignore that remark.

Now, let’s define a Generic framework for any outcome. The key insight we need to have is that we can ask the oracle a bunch of greater-than-or-less-than questions and build up a binary tree of transactions to settle at the right price.

To start, let’s define some basic stuff for a ‘GenericBet’.

/// A GenericBet takes a sorted list of outcomes and a cached table of
/// oracle lookups and assembles a binary contract tree for the GenericBet
pub struct GenericBet {
    amount: Amount,
    outcomes: Vec<(i64, Template)>,
    oracle: Rc<HashMap<i64, (Clause, Clause)>>,
    cooperate: Clause,
}
impl Contract for GenericBet {
    declare!(then, Self::pay_gte, Self::pay_lt, Self::oracle_no_show);
}

But where do we get the list of price to outcome and price to oracle clause from?

We need an external data source, right?

We’ll define some arguments and then a way of turning those arguments into a precise GenericBet.

We do it this way so that we can have GenericBetArguments accept a non-deterministic oracle server type, and then GenericBet itself could live in WASM and be fully deterministic.

/// To setup a GenericBet select an amount, a list of outcomes, and an oracle.
/// The outcomes do not need to be sorted but must be unique.
pub struct GenericBetArguments<'a> {
    amount: Amount,
    outcomes: Vec<(i64, Template)>,
    oracle: &'a dyn Oracle,
    cooperate: Clause,
    symbol: Symbol,
}
/// We can then convert the arguments into a specific contract instance
impl<'a> From<GenericBetArguments<'a>> for GenericBet {
    fn from(mut v: GenericBetArguments<'a>) -> GenericBet {
        // Make sure the outcomes are sorted for the binary tree
        v.outcomes.sort_by_key(|(i, _)| *i);
        // Cache locally all calls to the oracle
        let mut h = HashMap::new();
        for (k, _) in v.outcomes.iter() {
            let r = v.oracle.get_key_lt_gte(&v.symbol, *k);
            h.insert(*k, r);
        }
        GenericBet {
            amount: v.amount,
            outcomes: v.outcomes,
            oracle: Rc::new(h),
            cooperate: v.cooperate,
        }
    }
}

Now, we’ll implement the logic behind a generic bet:

Basically, we do a binary search over all the outcomes to find the middle, and if the price is greater, we send to that one. Otherwise, the other one. By winnowing through all of these outcomes recrusively, we are able to resolve a single price:action pair and settle the contract.

impl GenericBet {
    /// The oracle price kyes for this part of the tree is in the middle of the range.
    fn price(&self, b: bool) -> Clause {
        let v = &self.oracle[&self.outcomes[self.outcomes.len() / 2].0];
        if b {
            v.1.clone()
        } else {
            v.0.clone()
        }
    }
    fn recurse_over(
        &self,
        range: std::ops::Range<usize>,
        ctx: sapio::contract::Context,
    ) -> Result<Option<Template>, CompilationError> {
        match &self.outcomes[range] {
            [] => return Ok(None),
            [(_, a)] => Ok(Some(a.clone())),
            sl => Ok(Some(
                ctx.template()
                    .add_output(
                        self.amount.into(),
                        &GenericBet {
                            amount: self.amount,
                            outcomes: sl.into(),
                            oracle: self.oracle.clone(),
                            cooperate: self.cooperate.clone(),
                        },
                        None,
                    )?
                    .into(),
            )),
        }
    }
    /// Action when the price is greater than or equal to the price in the middle
    #[guard]
    fn gte(self, _ctx: Context) {
        self.price(true)
    }
    #[then(guarded_by = "[Self::gte]")]
    fn pay_gte(self, ctx: sapio::Context) {
        if let Some(tmpl) = self.recurse_over(self.outcomes.len() / 2..self.outcomes.len(), ctx)? {
            Ok(Box::new(std::iter::once(Ok(tmpl))))
        } else {
            Ok(Box::new(std::iter::empty()))
        }
    }

    /// Action when the price is less than or equal to the price in the middle
    #[guard]
    fn lt(self, _ctx: Context) {
        self.price(false)
    }
    #[then(guarded_by = "[Self::lt]")]
    fn pay_lt(self, ctx: sapio::Context) {
        if let Some(tmpl) = self.recurse_over(0..self.outcomes.len() / 2, ctx)? {
            Ok(Box::new(std::iter::once(Ok(tmpl))))
        } else {
            Ok(Box::new(std::iter::empty()))
        }
    }
    /// Allow for both parties to cooperative close
    #[guard]
    fn cooperate(self, _ctx: Context) {
        self.cooperate.clone()
    }

    #[then]
    fn oracle_no_show(self, _ctx: Context) {
        // elided for simplicity: unilateral close initiation after certain
        // relative delay if oracle doesn't reveal data
    }
}

This is, by itself, useless. But now that we have it we can implement now any payoff curve we want to. I’ll just show one example and leave it as “homework” for you to build others. We’ll start with the humble risk-reversal, which can be used to stablize a Bitcoin against the dollar. You can think of it as a Bitcoin “low pass” filter: you’ll still see big price swings, but not little ones. See below:

  Value of BTC in Asset
     |            
     |                                 /
     |             a                  /
     |        <------         b      /
     |               -------------> /
     |        ----------------------
     |       /       ^
     |      /        |
     |     /        current price
     |    /
     --------------------------------------------------- price of BTC in Asset
  Amount of BTC
     |            
     |-------
     |       \
     |        \  ^
     |         \  \
     |          \  \
     |           \  \
     |            \  \  a
     |             \  \
     |              \  \
     |               \  \
     |                \  \
     |                 \ <- current price
     |                  \  \
     |                   \  \
     |                    \  \
     |                     \  \ b
     |                      \  \
     |                       \  \
     |                        \  \
     |                         \  \
     |                          \  \
     |                           \  \
     |                            \  \
     |                             \  v
     |                              \
     |                               --------------
     |    
     --------------------------------------------------- price of BTC in Asset

In this case, Operator would be providing enough Bitcoin (Y) for a user’s funds (X) such that:

\((current - a)*(X+Y) = current * X\) or \(Y * current = a * (X + Y)\)

and would be seeing a potential bitcoin gain (Z) of

\((current + b) * (X - Z) = current * X\) or \(Z = b * X / (b + current)\)

or \(Z (current + b)\) dollars.

Operator can profit on the contract by:

  1. selecting carefully parameters a and b
  2. charging a premium
  3. charging a fee (& rehypothecating the position)

Similar to our GenericBetArguments we’ll compile this to a GenericBet to hide all the Network-y stuff.

First, let us define a couple APIs we need for the maker and taker of a contract (e.g., the person offering dollar stabilization and the person needing it).

/// An API for the Operator Service:
pub trait OperatorApi {
    /// Return Operator's Oracle
    fn get_oracle(&self) -> &dyn Oracle;
    /// Get a fresh key clause for Operator signing (could be a multisig etc)
    fn get_key(&self) -> Clause;
    /// Get a contract for a receivable amount. Allows Operator to direct funds to e.g.
    /// cold storage contracts
    fn receive_payment(&self, amount: Amount) -> Compiled;
}

/// An API for the Counterparty
pub trait UserApi {
    /// Get a fresh key clause for user signing (could be a multisig etc)
    fn get_key(&self) -> Clause;
    /// Get a contract for a receivable amount. Allows Userto direct funds to e.g.
    /// cold storage contracts
    fn receive_payment(&self, amount: Amount) -> Compiled;
}

Now, let us define the Arguments to a Risk Reversal:

//! RiskReversal represents a specific contract where we specify a set of price ranges that we
//! want to keep purchasing power flat within.
pub struct RiskReversal<'a> {
    amount: Amount,
    /// the current price in dollars with one_unit precision
    current_price_x_one_unit: u64,
    /// price multipliers rationals (lo, hi) and (a,b) = a/b
    /// e.g. ((7, 91), (1, 10)) computes from price - price*7/91 to price + price*1/10
    range: ((u64, u64), (u64, u64)),
    // ignore the
    operator_api: &'a dyn apis::OperatorApi,
    user_api: &'a dyn apis::UserApi,
    symbol: Symbol,
    ctx: Context,
}

Lastly, a bunch of complicated logic to turn those arguments into a price curve for a GenericBetArguments that then gets turned into a GenericBet:

const ONE_UNIT: u64 = 10_000;
impl<'a> TryFrom<RiskReversal<'a>> for GenericBetArguments<'a> {
    type Error = CompilationError;
    fn try_from(mut v: RiskReversal<'a>) -> Result<Self, Self::Error> {
        let key = v.operator_api.get_key();
        let user = v.user_api.get_key();
        let mut outcomes = vec![];
        let current_price = v.current_price_x_one_unit;
        // TODO: Can Customize this logic to for arbitrary curves or grids
        // bottom and top are floor/ceil for where our contract operates
        let bottom =
            ((current_price - (current_price * v.range.0 .0) / v.range.0 .1) / ONE_UNIT) * ONE_UNIT;
        let top = (((current_price + (current_price * v.range.1 .0) / v.range.1 .1) + ONE_UNIT
            - 1)
            / ONE_UNIT)
            * ONE_UNIT;
        // The max amount of BTC the contract needs to meet obligations
        let max_amount_bitcoin = (v.amount * current_price) / bottom;

        // represents an overflow
        if bottom > current_price || top < current_price {
            return Err(CompilationError::TerminateCompilation);
        }

        let mut strike_ctx = v.ctx.derive_str(Arc::new("strike".into()))?;
        // Increment 1 dollar per step
        for strike in (bottom..=top).step_by(ONE_UNIT as usize) {
            // Value Conservation Property:
            // strike * (amount + delta)  == amount * current price
            // strike * (pay to user)  == amount * current price
            // pay to user  == amount * current price / strike
            let profit = (v.amount * current_price) / strike;
            let refund = max_amount_bitcoin - profit;

            outcomes.push((
                strike as i64,
                strike_ctx
                    .derive_num(strike as u64)?
                    .template()
                    .add_output(profit, &v.user_api.receive_payment(profit), None)?
                    .add_output(refund, &v.operator_api.receive_payment(refund), None)?
                    .into(),
            ));
        }
        // Now that the schedule is constructed, build a contract
        Ok(GenericBetArguments {
            // must send max amount for the contract to be valid!
            amount: max_amount_bitcoin,
            outcomes,
            oracle: v.operator_api.get_oracle(),
            cooperate: Clause::And(vec![key, user]),
            symbol: v.symbol,
        })
    }
}

impl<'a> TryFrom<RiskReversal<'a>> for GenericBet {
    type Error = CompilationError;
    fn try_from(v: RiskReversal<'a>) -> Result<Self, Self::Error> {
        Ok(GenericBetArguments::try_from(v)?.into())
    }
}

Woooooop! Our Risk has been Reversed!

Options

First let’s play with Options.

We need a generic trait interface for all options. The basics are something to happen when it expires, and something to happen when it is paid for (strikes).

/// Generic functionality required for Expiring contracts
pub trait Expires: 'static + Sized {
    decl_then! {
        /// What to do when the timeout expires
        expires
    }
    decl_then! {
        /// what to do when the holder wishes to strike
        strikes
    }
}

First we’ll define an ExpiringOption whereby two parties deposit all the funds required for the contract (full collateral).


/// Wraps a generic option opt with functionality to refund both parties on timeout.
pub struct ExpiringOption<T: 'static> {
    party_one: Amount,
    party_two: Amount,
    key_p1: bitcoin::Address,
    key_p2: bitcoin::Address,
    key_p2_pk: Clause,
    opt: T,
    timeout: AnyAbsTimeLock,
}
impl<T> Contract for ExpiringOption<T>
where
    GenericBet: TryFrom<T, Error = CompilationError>,
    T: Clone + 'static,
{
    declare!(then, Self::expires, Self::strikes);
    declare!(non updatable);
}

impl<T> ExpiringOption<T> {
    /// Party Two is the option holder
    #[guard]
    fn signed(self, _ctx: Context) {
        self.key_p2_pk.clone()
    }
}

Then we’ll implement the functions:

impl<T> Expires for ExpiringOption<T>
where
    GenericBet: TryFrom<T, Error = CompilationError>,
    T: Clone,
{
    #[then]
    fn expires(self, ctx: sapio::Context) {
        // return the money to each party
        ctx.template()
            .add_output(
                self.party_one.into(),
                &Compiled::from_address(self.key_p1.clone(), None),
                None,
            )?
            .add_output(
                self.party_two.into(),
                &Compiled::from_address(self.key_p2.clone(), None),
                None,
            )?
            .set_lock_time(self.timeout)?
            .into()
    }
    /// Only party 2 can strike!
    #[then(guarded_by = "[Self::signed]")]
    fn strikes(self, ctx: sapio::Context) {
        // Send the money to a generic bet...
        ctx.template()
            .add_output(
                (self.party_one + self.party_two).into(),
                &GenericBet::try_from(self.opt.clone())?,
                None,
            )?
            .into()
    }
}

Now we’ll implement similar logic, but where the amount from party_two is not paid until the strike is called:

/// Similar to `ExpiringOption` except that the option requires an additional
/// value amount to be paid in in order to execute, hence being "under funded"
pub struct UnderFundedExpiringOption<T: 'static> {
    party_one: Amount,
    party_two: Amount,
    key_p1: bitcoin::Address,
    opt: T,
    timeout: AnyAbsTimeLock,
}
impl<T> Contract for UnderFundedExpiringOption<T>
where
    GenericBet: TryFrom<T, Error = CompilationError>,
    T: Clone + 'static,
{
    declare!(then, Self::expires, Self::strikes);
    declare!(non updatable);
}



impl<T> Expires for UnderFundedExpiringOption<T>
where
    GenericBet: TryFrom<T, Error = CompilationError>,
    T: Clone,
{
    #[then]
    fn expires(self, ctx: sapio::Context) {
        ctx.template()
            .add_output(
                self.party_one.into(),
                &Compiled::from_address(self.key_p1.clone(), None),
                None,
            )?
            .set_lock_time(self.timeout)?
            .into()
    }

    #[then]
    fn strikes(self, ctx: sapio::Context) {
        ctx.template()
            .add_amount(self.party_two)
            .add_sequence()
            .add_output(
                (self.party_one + self.party_two).into(),
                &GenericBet::try_from(self.opt.clone())?,
                None,
            )?
            .into()
    }
}

NFT Exercises for the reader:

Does this NEED CTV?

No, not in particular. Most of this stuff could be done with online signer server federation between you and counterparty. CTV makes some stuff nicer though, and opens up new possibilities for opening these contracts unilaterally.

Representing Positions as NFTs

Offers to open up a contract could be represented as NFTs! You don’t even need to create the NFT, just bind the NFT interface with an option open as a generic minting parameter, and then you can do price discovery of Option contracts through a dutch auction you thought was just for selling cat pics.

Wen LN?

Well, if you note that we can coop close options and derivatives, and that I claimed we don’t need CTV, these two facts imply that you can put these kinds of contracts inside of the LN no problem :).

What About PowSwap?

I mentioned powswap.com earlier. But you’ll have to wait to read about it, that’s all for today!

  1. I don’t actually have any paid shills, contrary to some people’s beliefs. 

to continue the conversation...

© 2011-2021 Jeremy Rubin. All rights reserved.