We are excited to announce the implementation of option contracts on chain, utilizing the power of Bitcoin covenants. This allows the execution of sophisticated financial contracts such as call and put options directly on a blockchain, as long as the chain supports Bitcoin-style covenants such as on Bitcoin SV and MVC. Unlike traditional financial platforms that often require multiple intermediaries, on-chain option contracts can reduce settlement times and operational cost, making them more secure, efficient and transparent.
For illustrative purposes, we have implemented both a call and a put option contract that grants the holder the right to buy or sell a certain amount of BSV-20 Ordinals tokens at a set price.
Option Contracts
Option contracts offer traders the right, but not the obligation, to buy (call option) or sell (put option) an asset at a predetermined price within a specific time frame. The buyer pays a premium to the seller for this right. Option contracts are commonly used in financial markets for hedging, speculation, and risk management.
A call option example
Let’s consider the following example:
Suppose you believe that the stock of a company, let’s call it XYZ Corp, will increase in value over the next three months. Currently, the stock is trading at $50 per share. You decide to purchase a call option on XYZ Corp with a strike price of $55 and an expiration date three months from now.
In this scenario:
- Underlying Asset: XYZ Corp stock
- Current Stock Price: $50 per share
- Strike Price of the Call Option: $55 per share
- Expiration Date: Three months from today
You pay a premium for the call option, let’s say $3 per share (the premium is the cost of buying the option). By purchasing this call option:
- If, three months from now, XYZ Corp’s stock price is above $55 per share, you can exercise your option and buy the stock at the lower strike price of $55. This could allow you to make a profit by immediately selling the stock at its higher market price.
- If the stock price is below $55 per share after three months, you are not obligated to exercise the option. You can let the option expire, and your loss is limited to the premium you paid.
Implementation
Similar to limit orders on chain, we implement options using covenants. In the context of option contracts, a grantor can lock assets in a covenant contract, which can then be claimed by the grantee if the conditions of the option are met within the stipulated time frame.
Call Option
For instance, in a call option, a grantor might lock 100 BSV-20 tokens in a smart contract, allowing a grantee to purchase these tokens at a set price, say, 5000 satoshis per token, before the expiry date.
If the market price exceeds the strike price, the holder can exercise the option, buying BSV-20 tokens at the agreed-upon price, benefiting from the price difference. If the option is not exercised before the expiry date, the contract allows the grantor to reclaim the locked BSV-20 tokens.
The smart contract has the following public methods:
- exercise: this method allows the grantee to exercise the option. It requires the grantee’s signature and performs checks to ensure the validity of the transaction, including the exchange of tokens and payment between the grantee and the grantor.
- transfer: allows the current holder of the option to transfer it to a new grantee.
- expire: used by the grantor to handle the contract upon its expiration. It requires the grantor’s signature and checks if the option has expired.
class Bsv20CallOption extends BSV20V2 {
@prop()
grantor: PubKey
@prop(true)
grantee: PubKey
@prop()
tokenAmt: bigint
@prop()
strikePrice: bigint
@prop()
expirationTime: bigint
...
@method()
public exercise(sigGrantee: Sig) {
// Check grantee sig.
assert(this.checkSig(sigGrantee, this.grantee), 'invalid sig grantee')
// Ensure grantee gets payed tokens.
let outputs = BSV20V2.buildTransferOutput(
pubKey2Addr(this.grantee),
this.id,
this.tokenAmt
)
// Ensure grantor gets payed satoshis.
const satAmt = this.strikePrice * this.tokenAmt
outputs += Utils.buildAddressOutput(pubKey2Addr(this.grantor), satAmt)
outputs += this.buildChangeOutput()
// Enforce outputs in call tx.
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}
@method()
public transfer(sigGrantee: Sig, newGrantee: PubKey) {
// Check grantee sig.
assert(this.checkSig(sigGrantee, this.grantee), 'invalid sig grantee')
// Set new grantee.
this.grantee = newGrantee
// Propagate contract.
let outputs = this.buildStateOutputFT(this.tokenAmt)
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}
@method()
public expire(sigGrantor: Sig) {
// Check grantor sig.
assert(this.checkSig(sigGrantor, this.grantor), 'invalid sig grantor')
// Check if expired.
assert(this.timeLock(this.expirationTime), 'option has not yet expired')
}
}
Put Option
The structure of a put option contract is quite similar to that of a call option. The primary distinction lies in the fact that the grantor locks the payment in satoshis into the smart contract, rather than the BSV-20 tokens. The grantee can then exercise the option by transferring the tokens to the seller at the predetermined strike price.
However, since the smart contract itself cannot directly verify the validity of the UTXO holding the tokens, which the grantee presents upon exercising the option, we utilize an oracle. This oracle is responsible for verifying and validating the UTXO’s legitimacy through a signature.
@method()
public exercise(sigGrantee: Sig, oracleSig: RabinSig, oracleMsg: ByteString) {
// Check oracle signature.
assert(
RabinVerifier.verifySig(oracleMsg, oracleSig, this.oraclePubKey),
'oracle sig verify failed'
)
// Check that we're unlocking the UTXO specified in the oracles message.
assert(
slice(this.prevouts, 0n, 36n) == slice(oracleMsg, 0n, 36n),
'first input is not spending specified ordinal UTXO'
)
// Get token amount held by the UTXO from oracle message.
const utxoTokenAmt = byteString2Int(slice(oracleMsg, 36n, 44n))
// Check token amount is correct.
assert(utxoTokenAmt == this.tokenAmt, 'invalid token amount')
// Check grantee sig.
assert(this.checkSig(sigGrantee, this.grantee), 'invalid sig grantee')
// Ensure grantor gets payed tokens.
let outputs = BSV20V2.buildTransferOutput(
pubKey2Addr(this.grantor),
this.id,
this.tokenAmt
)
// Ensure grantee gets payed satoshis.
const satAmt = this.strikePrice * this.tokenAmt
outputs += Utils.buildAddressOutput(pubKey2Addr(this.grantee), satAmt)
outputs += this.buildChangeOutput()
// Enforce outputs in call tx.
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}
Trustlessly Trading Options
The smart contracts above have only a simple transfer method which allows the current holder to assign a new holder to the option, effectively trading the option in a secondary market.
But since the transfer of an option usually involves the payment of a premium, we can add a sale mechanism directly into the smart contracts above.
We add two public methods:
- listForSale: allows the current grantee to list the option for sale with a specified premium. This method requires the grantee’s signature and updates the contract’s sale status and premium amount.
- buy: enables a new grantee to buy the option if it is for sale. It updates the grantee of the contract and handles the payment of the premium to the previous grantee.
@method()
public listForSale(sigGrantee: Sig, premium: bigint) {
// Check grantee sig.
assert(this.checkSig(sigGrantee, this.grantee), 'invalid sig grantee')
// Store premium value in property.
this.premium = premium
// Toggle for sale flag.
this.forSale = true
// Propagate contract.
let outputs = this.buildStateOutputFT(this.tokenAmt)
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}
@method()
public buy(newGrantee: PubKey) {
// Check if option is up for sale.
assert(this.forSale, 'option is not up for sale')
// Set new grantee.
const prevGrantee = this.grantee
this.grantee = newGrantee
// Toggle for sale flag.
this.forSale = false
// Propagate contract.
let outputs = this.buildStateOutputFT(this.tokenAmt)
// Make sure premium is payed to previous grantee / holder.
outputs += Utils.buildAddressOutput(
pubKey2Addr(prevGrantee),
this.premium
)
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}
The full code of both the call option and put option smart contract is available on GitHub.