Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7390: Vanilla Options for ERC-20 Tokens

An interface for creating, managing, and executing simple time-limited call/put (vanilla) options.

Authors Ewan Humbert (@Xeway) <xeway@protonmail.com>, Lassi Maksimainen (@mlalma) <lassi.maksimainen@gmail.com>
Created 2022-09-02
Discussion Link https://ethereum-magicians.org/t/erc-7390-vanilla-option-standard/15206
Requires EIP-20, EIP-1155

Abstract

This standard defines a comprehensive set of functions and events facilitating seamless interactions (creation, management, exercising, etc.) for vanilla options.

Vanilla options grant the right, without obligation, to buy or sell an asset at a set price within a specified timeframe.

This standard doesn’t represent a simple option that would be useless after the expiration date. Instead, it can store as many issuance as needed. Each issuance is identified by an id, and can be bought, exercised, cancelled, etc., independently of the other issuances.
Every issuance is collateralized, meaning that the writer has to provide the collateral to the contract before the buyer can buy the option. The writer can retrieve the collateral if the buyer hasn’t exercised in the exercise window.
A buyer can decide to buy only a fraction of the issuance (meaning multiple buyers is possible), and will receive accordingly tokens (ERC-1155) that represent the fraction of the issuance. From now, we will call these tokens redeem tokens. These tokens can be exchanged between users, and are used for exercising the option. With this mechanism, a buyer can decide to exercise only a fraction of what he bought.
Also, the writer can decide to cancel the issuance if no option has been bought yet. He also has the right to update the premium price at any time. This doesn’t affect the already bought options.
The underlying token, strike token and premium token are ERC-20 tokens.

In the following, the plural term options will sometimes be used. This can refer to the amount of redeem tokens a buyer purchased and can exercise.

Motivation

Options are widely used financial instruments, and have a true usefulness for investors and traders. It offers versatile risk management tools and speculative opportunities.
In the decentralized finance, many options-selling platform emerged, but each of these protocols implements their own definition of an option. This leads to incompatibilities, which is a pity because options should be interoperable like fungible/non-fungible tokens are.
By introducing a standard interface for vanilla options contracts, we aim to foster a more inclusive and interoperable derivatives ecosystem. This standard will enhance the user experience and facilitate the development of decentralized options platforms, enabling users to seamlessly trade options across different applications. Moreover, this standard is designed to represent vanilla options, which are the most common type of options. This standard can be used as a base for more complex options, such as exotic options.

Specification

Implementations of this proposal MUST also implement ERC-1155 to give the possibility to buy only a fraction of the issuance.

Interface

interface IERC7390 {
    enum Side {
        Call,
        Put
    }

    struct VanillaOptionData {
        Side side;
        address underlyingToken;
        uint256 amount;
        address strikeToken;
        uint256 strike;
        address premiumToken;
        uint256 premium;
        uint256 exerciseWindowStart;
        uint256 exerciseWindowEnd;
        address[] allowed;
    }

    struct OptionIssuance {
        VanillaOptionData data;
        address writer;
        uint256 exercisedAmount;
        uint256 soldAmount;
    }

    error Forbidden();
    error TransferFailed();
    error TimeForbidden();
    error AmountForbidden();
    error InsufficientBalance();

    event Created(uint256 indexed id);
    event Bought(uint256 indexed id, uint256 amount, address indexed buyer);
    event Exercised(uint256 indexed id, uint256 amount);
    event Expired(uint256 indexed id);
    event Canceled(uint256 indexed id);
    event PremiumUpdated(uint256 indexed id, uint256 amount);
    event AllowedUpdated(uint256 indexed id, address[] allowed);

    function create(VanillaOptionData calldata optionData) external returns (uint256);

    function buy(uint256 id, uint256 amount) external;

    function exercise(uint256 id, uint256 amount) external;

    function retrieveExpiredTokens(uint256 id, address receiver) external;

    function cancel(uint256 id, address receiver) external;

    function updatePremium(uint256 id, uint256 amount) external;

    function updateAllowed(uint256 id, address[] memory allowed) external;

    function issuance(uint256 id) external view returns (OptionIssuance memory);
}

State Variable Descriptions

At creation time, user must provide filled instance of VanillaOptionData structure that contains all the key information for initializing the option issuance.

side

Type: enum

Side of the option. Can take the value Call or Put. Call option gives the option buyer right to exercise any acquired option tokens to buy the underlying token at given strike price using strikeToken from option writer. Similarly, Put option gives the option buyer right to sell the underlying token to the option writer at strike price.

underlyingToken

Type: address (ERC-20 contract)

Underlying token.

amount

Type: uint256

Maximum amount of the underlying tokens that can be exercised.

Be aware of token decimals!

strikeToken

Type: address (ERC-20 contract)

Token used as a reference to determine the strike price.

strike

Type: uint256

Strike price. The option buyer MAY be able to exercise only fraction of the issuance and the paid strike price must be adjusted by the contract to reflect it.

Note that strike is meant to represent the price in strikeToken for a single underlyingToken.

Be aware of token decimals!

premiumToken

Type: address (ERC-20 contract)

Premium token.

premium

Type: uint256

Premium price is the price that option buyer has to pay to option writer to compensate for the risk that the writer takes for issuing the option. Option premium changes depending on various factors, most important ones being the volatility of the underlying token, strike price and the time left for exercising the option.

Note that the premium price is set for exercising the total amount of the issuance. The buyer MAY be able to buy only fraction of the option tokens and the paid premium price must be adjusted by the contract to reflect it.

Be aware of token decimals!

exerciseWindowStart

Type: uint256
Format: timestamp as seconds since unix epoch

Option exercising window start time. When current time is greater or equal to exerciseWindowStart and below or equal to exerciseWindowEnd, owner of option(s) can exercise them.

exerciseWindowEnd

Type: uint256
Format: timestamp as seconds since unix epoch

Option exercising window end time. When current time is greater or equal to exerciseWindowStart and below or equal to exerciseWindowEnd, owner of option(s) can exercise them. When current time is greater than exerciseWindowEnd, buyers can’t exercise and writer can retrieve remaining underlying (call) or strike (put) tokens.

allowed

Type: address[]

Addresses that are allowed to buy the issuance. If the array is empty, all addresses are allowed to buy the issuance.

VanillaOptionData is stored in the OptionIssuance struct, which is used to store the option issuance data. It contains other information.

writer

Type: address

Address of the writer meaning the address that created the option.

exercisedAmount

Type: uint256

Amount of underlying tokens that have been exercised.

soldAmount

Type: uint256

Amount of underlying tokens that have been bought for this issuance.

transferredExerciseCost

Type: uint256

Amount of strikeToken tokens that have been transferred to the writer (call) or buyers (put) of the option issuance.
This is an utility variable used to not always have to calculate the total exercise cost transferred. It’s updated at the same time exercisedAmount is updated. The calculation is (amount * selectedIssuance.data.strike) / (10**underlyingToken.decimals()).

exerciseCost

Type: uint256

Exercise cost. It represents the collateral the writer has to deposit to the contract (put), or the amount of strikeToken tokens a writer can receive if all buyers decide to exercise (call).
This is an utility variable used to not always have to calculate the exercise cost. We compute it at the creation of the option. The calculation is (strike * amount) / (10 ** underlyingToken.decimals()).

Function Descriptions

constructor

No constructor is needed for this standard, but the contract MUST implement the ERC-1155 interface. So, the contract MUST call the ERC-1155 constructor.

create

function create(VanillaOptionData calldata optionData) external returns (uint256);

Option writer creates new option tokens and defines the option parameters using create(). As an argument, option writer needs to fill VanillaOptionData data structure instance and pass it to the method. As a part of creating the option tokens, the function transfers the collateral from option writer to the contract.

It is highly preferred that as a part of calling create() the option issuance becomes fully collateralized to prevent increased counterparty risk. For creating a call (put) option issuance, writer needs to allow the amount of amount (strike) tokens of underlyingToken (strikeToken) to be transferred to the option contract before calling create().

Note that this standard does not define functionality for option writer to “re-up” the collateral in case the option contract allows under-collateralization. The contract needs to then adjust its API and implementation accordingly.

MUST revert if underlyingToken or strikeToken is the zero address.
MUST revert if premium is not 0 and premiumToken is the zero address.
MUST revert if amount or strike is 0.
MUST revert if exerciseWindowStart is less than the current time or if exerciseWindowEnd is less than exerciseWindowStart.

Returns an id value that refers to the created option issuance in option contract if option issuance was successful. Emits Created event if option issuance was successful.

buy

function buy(uint256 id, uint256 amount) external;

Allows the buyer to buy amount of option tokens from option issuance with the defined id.

The buyer has to allow the token contract to transfer the (fraction of total) premium in the specified premiumToken to option writer. During the call of the function, the premium is be directly transferred to the writer.

If allowed array is not empty, the buyer’s address MUST be included in this list.
MUST revert if amount is 0 or greater than the remaining options available for purchase.
MUST revert if the current time is greater than exerciseWindowEnd.

Mints amount redeem tokens to the buyer’s address if buying was successful. Emits Bought event if buying was successful.

exercise

function exercise(uint256 id, uint256 amount) external;

Allows the buyer to exercise amount of option tokens from option issuance with the defined id.

  • If the option is a call, buyer pays writer at the specified strike price and gets the specified underlying tokens.
  • If the option is a put, buyer transfers to writer the underlying tokens and gets paid at the specified strike price.

The buyer has to allow the spend of either strikeToken or underlyingToken before calling exercise().

Exercise MUST only take place when exerciseWindowStart <= current time <= exerciseWindowEnd.
MUST revert if amount is 0 or buyer hasn’t the necessary redeem tokens to exercise the option.

Burns amount redeem tokens from the buyer’s address if the exercising was successful. Emits Exercised event if the option exercising was successful.

retrieveExpiredTokens

function retrieveExpiredTokens(uint256 id, address receiver) external;

Allows writer to retrieve the collateral tokens that were not exercised. These tokens are transferred to receiver.
If the option is a call, receiver retrieves the underlying tokens. If the option is a put, receiver retrieves the strike tokens.

MUST revert if the address calling the function is not the writer of the option issuance.
MUST revert if exerciseWindowEnd is greater or equals than the current time.
If equals to the zero address, MUST set receiver to caller’s address.

Transfers the un-exercised collateral to the writer’s address. MAY delete the option issuance from the contract if the retrieval was successful. Emits Expired event if the retrieval was successful.

cancel

function cancel(uint256 id, address receiver) external;

Allows writer to cancel the option and retrieve tokens used as collateral. These tokens are transferred to receiver.
If the option is a call, receiver retrieves the underlying tokens. If the option is a put, receiver retrieves the strike tokens.

MUST revert if the address calling the function is not the writer of the option issuance.
MUST revert if at least one option’s fraction has been bought.
If equals to the zero address, MUST set receiver to caller’s address.

Transfers the un-exercised collateral to the writer’s address. MAY delete the option issuance from the contract if the cancelation was successful. Emits Canceled event if the cancelation was successful.

updatePremium

function updatePremium(uint256 id, uint256 amount) external;

Allows the writer to update the premium that buyers will need to provide for buying the options.

Note that the amount will be for the whole underlying amount, not only for the options that might still be available for purchase.

MUST revert if the address calling the function is not the writer of the option issuance.
MUST revert if the current time is greater than exerciseWindowEnd.

Emits PremiumUpdated event when the function call was handled successfully.

updateAllowed

function updateAllowed(uint256 id, address[] memory allowed) external;

Allows the writer to update the list of allowed addresses that can buy the option issuance.
If a buyer already bought an option and his address is not in the new list, he will still be able to exercise his purchased options.

MUST revert if the address calling the function is not the writer of the option issuance.
MUST revert if the current time is greater than exerciseWindowEnd.

Emits AllowedUpdated event when the function call was handled successfully.

issuance

function issuance(uint256 id) external view returns (OptionIssuance memory);

Returns all the key information for the option issuance with the given id.

Events

Created

event Created(uint256 id);

Emitted when the writer has provided option issuance data successfully (and locked down the collateral to the contract). The given id identifies the particular option issuance.

Bought

event Bought(uint256 indexed id, uint256 amount, address indexed buyer);

Emitted when options have been bought. Provides information about the option issuance id, the address of buyer and the amount of options bought.

Exercised

event Exercised(uint256 indexed id, uint256 amount);

Emitted when the option has been exercised from the option issuance with given id and the given amount.

Expired

event Expired(uint256 indexed id);

Emitted when the writer of the option issuance with id has retrieved the un-exercised collateral.

Canceled

event Canceled(uint256 indexed id);

Emitted when the option issuance with given id has been cancelled by the writer.

PremiumUpdated

event PremiumUpdated(uint256 indexed id, uint256 amount);

Emitted when writer updates the premium to amount for option issuance with given id. Note that the updated premium is for the total issuance.

AllowedUpdated

event AllowedUpdated(uint256 indexed id, address[] allowed);

Emitted when writer updates the list of allowed addresses for option issuance with given id.

Errors

Forbidden

Reverts when the caller is not allowed to perform some actions (general purpose).

TransferFailed

Reverts when the transfer of tokens failed.

TimeForbidden

Reverts when the current time of the execution is invalid.

AmountForbidden

Reverts when the amount is invalid.

InsufficientBalance

Reverts when the caller has insufficient balance to perform the action.

Concrete Examples

Call Option

Let’s say Bob sells a call option.
He gives the right to anyone to buy 8 TokenA at 25 TokenB each between 14th of July 2023 and 16th of July 2023 (at midnight).
For such a contract, he wants to receive a premium of 10 TokenC.

Before creating the option, Bob has to transfer the collateral to the contract. This collateral corresponds to the tokens he will have to give if the option if fully exercised (amount). For this option, he has to give as collateral 8 TokenA. He does that by calling the function approve(address spender, uint256 amount) on the TokenA’s contract and as parameters the contract’s address (spender) and for amount: 8 * 10^(TokenA’s decimals). Then Bob can execute create() on the contract for issuing the option, giving the following parameters:

  • side: Call
  • underlyingToken: TokenA’s address
  • amount: 8 * 10^(TokenA’s decimals)
  • strikeToken: TokenB’s address
  • strike: 25 * 10^(TokenB’s decimals)
  • premiumToken: TokenC’s address
  • premium: 10 * 10^(TokenC’s decimals)
  • exerciseWindowStart: 1689292800 (2023-07-14 timestamp)
  • exerciseWindowEnd: 1689465600 (2023-07-16 timestamp)
  • allowed: [] (open to anyone)

The issuance has ID 88.

Alice wants to be able to buy only 4 TokenA. She will first have to pay the premium (that is proportional to its share) by allowing the spending of his 10 TokenC by calling approve(address spender, uint256 amount) on the TokenC’s contract and give as parameters the contract’s address (spender) and for amount: 4*10^(TokenA’s decimals) * 10*10^(TokenC’s decimals) / 8*10^(TokenA’s decimals) (amountToBuy * premium / amount). She can then execute buy(88, 4 * 10^(TokenA's decimals)) on the contract, and will receive 4*10^(TokenA’s decimals) redeem tokens.

John, for his part, wants to buy 2 TokenA. He does the same thing and receives 2*10^(TokensA’s decimals) redeem tokens.

We’re on the 15th of July and Alice wants to exercise his option because 1 TokenA is traded at 50 TokenB! She needs to allow the contract to transfer 4*10^(TokenA’s decimals) * 25*10^(TokenB’s decimals) / 10^(TokenA’s decimals) (amountToExercise * strike / 10^(TokenA’s decimals)) TokenBs from her account to be able to exercise. When she calls exercise(88, 4 * 10^(TokenA's decimals)) on the contract, it will transfer 4 TokenA to Alice, and 4*25 TokenB to Bob.

John decided to give his right to exercise to his friend Jimmy. He did that simply by transferring his 2*10^(TokensA’s decimals) redeem tokens to Jimmy’s address.
Jimmy decides to only buy 1 TokenA with the option. So he will give to Bob (through the contract) 1*10^(TokenA’s decimals) * 25*10^(TokenB’s decimals) / 10^(TokenA’s decimals).

Put Option

Let’s say Bob sells a put option.
He gives the right to anyone to sell to him 8 TokenA at 25 TokenB each between 14th of July 2023 and 16th of July 2023 (at midnight).
For such a contract, he wants to receive a premium of 10 TokenC.

Before creating the option, Bob has to transfer the collateral to the contract. This collateral corresponds to the tokens he will have to give if the option if fully exercised (exerciseCost). For this option, he has to give as collateral 200 TokenB (8 * 25). He does that by calling the function approve(address spender, uint256 amount) on the TokenB’s contract and as parameters the contract’s address (spender) and for amount: 25*10^(Token B’s decimals) * 8*10^(TokenB’s decimals) / 10^(TokenA’s decimals) (strike * amount / 10^(underlyingToken’s decimals)). Then Bob can execute create() on the contract for issuing the option, giving the following parameters:

  • side: Put
  • underlyingToken: TokenA’s address
  • amount: 8 * 10^(TokenA’s decimals)
  • strikeToken: TokenB’s address
  • strike: 25 * 10^(TokenB’s decimals)
  • premiumToken: TokenC’s address
  • premium: 10 * 10^(TokenC’s decimals)
  • exerciseWindowStart: 1689292800 (2023-07-14 timestamp)
  • exerciseWindowEnd: 1689465600 (2023-07-16 timestamp)
  • allowed: [] (open to anyone)

The issuance has ID 88.

Alice wants to be able to sell only 4 TokenA. She will first have to pay the premium (that is proportional to its share) by allowing the spending of his 10 TokenC by calling approve(address spender, uint256 amount) on the TokenC’s contract and give as parameters the contract’s address (spender) and for amount: 4*10^(TokenA’s decimals) * 10*10^(TokenC’s decimals) / 8*10^(TokenA’s decimals) (amountToSell * premium / amount). She can then execute buy(88, 4 * 10^(TokenA's decimals)) on the contract, and will receive 4*10^(TokenA’s decimals) redeem tokens.

John, for his part, wants to sell 2 TokenA. He does the same thing and receives 2*10^(TokensA’s decimals) redeem tokens.

We’re on the 15th of July and Alice wants to exercise his option because 1 TokenA is traded at only 10 TokenB! She needs to allow the contract to transfer 4 * 10^(TokenA’s decimals) TokenAs from her account to be able to exercise. When she calls exercise(88, 4 * 10^(TokenA's decimals)) on the contract, it will transfer 4*25 TokenB to Alice and 4 TokenA to Bob.

John decided to give his right to exercise to his friend Jimmy. He did that simply by transferring his 2*10^(TokensA’s decimals) redeem tokens to Jimmy’s address.
Jimmy decides to only sell 1 TokenA with the option. So he will give to Bob (through the contract) 1*10^(TokenA’s decimals).

Retrieve collateral

Let’s say Alice never exercised his option because it wasn’t profitable enough for her. To retrieve his collateral, Bob would have to wait for the current time to be greater than exerciseWindowEnd. In the examples, this characteristic is set to 2 days, so he would be able to get back his collateral from the 16th of July by simply calling retrieveExpiredTokens().

Rationale

This contract’s concept is oracle-free, because we assume that a rational buyer will exercise his option only if it’s profitable for him.

The premium is to be determined by the option writer. writer is free to choose how to calculate the premium, e.g. by using Black-Scholes model or something else. writer can update the premium price at will in order to adjust it according to changes on the underlying’s price, volatility, time to option expiry and other such factors. Computing the premium off-chain is better for gas costs purposes.

This ERC is intended to represent vanilla options. However, exotic options can be built on top of this ERC.
Instead of representing a single option that would be useless after the expiration date, this contract can store as many issuances as needed. Each issuance is identified by an id, and can be bought, exercised, cancelled, etc., independently of the other issuances. This is a better approach for gas costs purposes.

It’s designed so that the option can be either European or American, by introduction of the exerciseWindowStart and exerciseWindowEnd data points. A buyer can only exercise between exerciseWindowStart and exerciseWindowEnd.

  • If the option writer considers the option to be European, he can set the exerciseWindowStart in line with the expiration date, and exerciseWindowEnd to the expiration date + a determined time range so that buyers have a period of time to exercise.
  • If the option writer considers the option to be American, he can set the exerciseWindowStart to the current time, and the buyer will be able to exercise the option immediately.

The contract inherently supports multiple buyers for a single option issuance. This is achieved by using ERC-1155 tokens for representing the options. When a buyer buys a fraction of the option issuance, he receives ERC-1155 tokens that represent the fraction of the option issuance. These tokens can be exchanged between users, and are used for exercising the option. With this mechanism, a buyer can decide to exercise only a fraction of what he bought.

The contract implements allowed array, which can be used to restrict the addresses that can buy the option issuance. This can be useful if two users agreed for an option off-chain and they want to create it on-chain. This prevents the risk that between the creation of the contract and the purchase by the second user, an on-chain user has already bought the contract.

This ERC is designed to handle ERC-20 tokens. However, this standard can be used as a good base for handling other types of tokens, such as ERC-721 tokens. Some attributes and functions signatures (to provide an id instead of an amount for instance) would have to be changed, but the general idea would remain the same.

Security Considerations

Contract contains exerciseWindowStart and exerciseWindowEnd data points. These define the determined time range for the buyer to exercise options. When the current time is greater than exerciseWindowEnd, the buyer won’t be able to exercise and the writer will be able to retrieve any remaining collateral.

For preventing clear arbitrage cases when option writer considers the issuance to be of European options, we would strongly advice the option writer to call updatePremium to considerably increase the premium price when exercise window opens. This will make sure that the bots won’t be able to buy any remaining options and immediately exercise them for quick profit. Of course, this standard can be customized and maybe users will find more convenient to update the premium automatically using available tools, instead of doing it manually (especially if the premium is based on specific dynamic metrics like the Black-Scholes model). If the option issuance is considered to be American, such adjustment is of course not needed.

This standard implements the updatePremium function, which allows the writer to update the premium price at any time. This function can lead to security issues for the buyer: a buyer could buy an option, and the writer could front-run buyer’s transaction by updating the premium price to a very high value. To prevent this, we advise the buyer to only allow for the agreed amount of premium to be spent by the contract, not more.

The contract supports multiple buyers for a single option issuance, meaning fractions of the option issuance can be bought. The ecosystem doesn’t really support non-integers, so fractions can sometimes lead to rounding errors. This can lead to unexpected results, especially in the buy function: if the premium is set, the buyer has to pay for only a fraction proportional to the amount of options he wants to buy. If that fraction is not an integer, this will truncate and therefore round to floor. This means that writer will receive less than the expected premium. We consider this risk pretty negligible given that most tokens have a high number of decimals, but it’s important to be aware of it. Some buyer could exploit this by buying repeatedly small fraction, and therefore paying less than the expected premium. However, this probably wouldn’t be profitable given the gas costs.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Ewan Humbert (@Xeway) <xeway@protonmail.com>, Lassi Maksimainen (@mlalma) <lassi.maksimainen@gmail.com>, "ERC-7390: Vanilla Options for ERC-20 Tokens [DRAFT]," Ethereum Improvement Proposals, no. 7390, September 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7390.