owned this note
owned this note
Published
Linked with GitHub
###### tags: `zip`
# ZIP-9: Invalid Resolution
<!-- prettier-ignore -->
:::info
Author: Dr. Malte Kliemann
Revision: 2
:::
## Introduction: Two Approaches
When designing prediction markets, we often encounter one of the following issues:
- One of the possible outcomes of the event is a long-shot or only possible in theory. Ties is basketball games, for example.
- The market is rendered invalid by unforeseen events or contains a bad description which doesn't cover all possible outcomes.
These issues can be mitigated by allow a market to resolve _invalidly_. This essentially means that the oracle specifies that the actual outcome is not covered by the market description, either due to being a long-shot or due to a bad market description; the market then resolves in one of two ways.
The _rollback approach_ is to void or roll back the entire market. All parties are made whole again. This is certainly the cleanest approach, but is difficult to implement properly.
The _payout vector approach_ is to specify, either at the creation of the market or when the market is resolved, a payout vector $p$ with $\sum_i p_i = 1$. The $i^{\mathrm{th}}$ outcome of the market then pays $p_i$ units of collateral in case of an invalid resolution. This will generally be less satisfying for traders as it's not a perfect rollback.
Zeitgeist will opt for the second approach to avoid various implementation issues described below.
## Details
### Rollback Approach
Performing a complete rollback on a market in a permissionless setting is difficult to implement. The naive approach that one might employ in a permissioned web2 setting is to record the cash in- and outflow of each user (or even a complete transaction record!) and then, in case of an invalid resolution, use this data to make everybody whole.
This luxury is not available to us. A more sophisticated implementation would involve adding a new outcome token to each market, say _Invalid_. This token is minted/burned using the complete set operations. Whether or not this token should be traded on the market is debatable, especially if an AMM is involved; see below. In case of an invalid resolution, the Invalid token pays $1, while all other tokens pay nothing.
It's unclear how the AMM would best handle the Invalid tokens. If the Invalid token is traded in the market, we run into a couple of pain points:
- The price of Invalid must start at such a low value that short-selling it will not result in profits. This essentially means that the price needs to be sufficiently small in relation to the trading fees of the AMM.
- A low price for Invalid means that the reserve of this token in the pool must be very high in relation to the other tokens in the pool. This is a bit of a problem in terms of capital efficiency, as it means that LPs will mostly use the Invalid tokens of the complete sets they buy for providing liquidity.
- Since buying on the new AMM works by buying complete sets and then selling those tokens you don't want for more of the token you do want, traders now have to specify whether they want to keep or sell the Invalid token, basically deciding if they want the insurance or sell it to slightly increase their potential payoff.
These issues seem like deal breakers on numerous levels: Numerical limits, capital efficiency, UX. It should also be argued that, _if the Invalid token is traded in the pool_, there is no need for changes to the backend. The entire concept is available right now---it would just have to be sold very, very well.
If, on the other hand, the Invalid token is _not_ traded in the pool, we don't fare too much better. We basically get the inverse problem of the issue related to buying described above: Recall that selling an outcome token on neo-swaps works by selling _part_ of your holdings for other outcome tokens so you end up with the same amount of each outcome token, which is then turned into collateral by calling `sell_complete_set`. But you can't buy Invalid from the AMM, so you can only sell enough to recover your initial bet, but never to take a profit before the market resolves.
For example, suppose Alice spends $5 buying some outcome token A. She receives 5 of each outcome token, then sells her balance of each outcome token (except for Invalid) for more A. She's left with $x > 5$ units of A and 5 units of Invalid. Later, A gains in value and Alice wants to sell. So she sells part of her holdings in A until she has $y$ units of each outcome token ($x > y > 5$ because A increased in value) and then she wants to sell $y$ complete sets for $y but... that's not possible because her balance of Invalid is still 5. The best she can do is sell 5 complete sets and retain $y-5$ complete sets which will pay out the remaining $y-5$ _unless_ the market resolves invalidly, in which case Alice will receive no additional funds.
While that's quite intuitive---if we want to maintain the option of making everybody whole (or at least those who've kept their insurance token), then we can't let anyone take winnings before the market is resolved!---this appears to be completely unacceptable in terms of UX.
### Payout Vector Approach
Instead of hoping to make all traders whole in case of an invalid resolution, the market creator may (if they want to) define a _payout vector_ which is used to resolve the market if a long-shot comes in. The payout vector $p$ simply specifies what each outcome token will pay if the market resolves invalidly. In order to not break the prize pool, $\sum_i p_i = 1$ must always hold.
The most common configurations that make sense here are:
- Uniform distributions, i.e. $(\frac{1}{n}, \ldots, \frac{1}{n})$ for a market with $n$ outcomes.
- Values based on initial odds of the market.
This will almost always result in some traders making a profit and others taking losses. It also raises concerns about what the prediction actually means if the spot prices are hovering around the values of the payout vector. On the other hand, the implementation is straightforward.
One might even allow the oracle to specify the payout vector based on some external data that isn't known at the point of market creation. Unfortunately, this raises massive problems when handling disputes about the invalid resolution (remember that scalar markets are already hard enough to handle in court!).
## Implementation Plan
### PR1: Payout Vectors
Implementing payout vectors will help make the resolution system simpler and more flexible. This will be useful not only in implementing invalid resolution, but also when dealing with combinatorial markets. This is the content of the first PR.
The PR adds a `payout` field to the `Market` struct. The implementor may decide if a `BoundedVec<BA, S>` with max size equal to the `MaxAssets` value defined by prediction-markets or a `BoundedBTreeMap<A, BA, S>` is in order. This involves adding another generic `S` to the `Market` struct which is used to set the size of the bounded object. It may also be a custom type in case the implementor wants to get extra fancy and save some on-chain storage. Of course, any of these choices requires a storage migration.
When the market is resolved in `on_resolution`, instead of just writing `resolved_outcome` to the `Market` struct, we also calculate the payout vector and write it to the market as well. This should be abstracted into two new functions, one for scalar markets and one for categorical markets.
The function for categorical markets returns the payout vector $p$ with $p_i = 1$ where $i$ is (the index of) the winning token and $p_k = 0$ for all $k \neq i$. The payout vector of a scalar market with range $[a, b]$ and report $v$ is calculated as follows. Let $w = \min(\max(v, a), b)$ be $v$ clamped into $[a, b]$. Then the payout vector has two entries (the first one corresponding to the Long token, following our usual conventions):
\begin{align*}
p_0 &= \frac{w - a}{b - a}, \\
p_1 &= 1 - p_0.
\end{align*}
A clean approach to calculating these can be found in the current implementation of `redeem_shares`, although the implementation might be considerably simpler and avoid some back and forth using the new fixed point number operations. The additional cost in terms of weight can just be added on to the `total_weight` variable used in `on_resolution`.
The next step is to rewrite the `redeem_shares` extrinsic, which now has a simple job: It iterates over the assets of the market indexed by $i = 1, \ldots, n$, checks how many of each the signer holds, say $x_i$, and redeems them for $p_i \cdot x_i$ _rounded down_. The rounding is important, as rounding up might break the prize pool.
This might prove uncomfortable in terms of benchmarking (caller must pay for the worst-case upfront), but there's no way around it. In case of an invalid resolution or complex combinatorial bets, calling `redeem_shares` may end up requiring $n$ transfers. The good news is that `redeem_shares` is no longer dependent on the type of market.
`redeem_shares` will of course error if the market is not found, it's not a redeemable market, it's not resolved or the payout vector is not set.
:::info
**Note.** Payout vector might even be useful for resolving parimutuel markets.
:::
### PR2: Predefined Invalid Resolution
The second pull request will fully implement the new features. We start by adding a new variant `Invalid` to `OutcomeReport`. This allows the oracle to specify that a long-shot has materialized.
Making this change means that `Market::matches_outcome_report` must be modified to return `true` is the report is `Invalid`, regardless of the market type. This should make the changes compatible with zrml-authorized and zrml-court.
It is up to the implementor whether or not we should add a `payout_on_invalid_resolution` field of type `BoundedVec<BA, S>` or `BoundedBTreeMap<BA, S>` to the `Market` struct which the market creator can use to specify how the market is supposed to pay out in case of an invalid resolution. If we decide to do so, this means that all market creation functions need to add the `payout_on_invalid_resolution` parameter and validate that it satisfies $\sum_i p_i = 1$ and that `payout_on_invalid_resolution.len()` is correct.
As an alternative, we can just always resolve invalidly using a uniform distribution. If that's the way to go, since numbers like $\frac{1}{3}$ cannot be correctly represented on a computer, we shall slightly favor the last outcome. The uniform vector $(\frac{1}{3}, \frac{1}{3}, \frac{1}{3})$ would be represented by `vec![3_333_333_333, 3_333_333_333, 3_333_333_334]` on Zeitgeist.
Regardless of our choice, the frontend should not force a user to specify an invalid resolution. Maybe hiding it for now is the best approach. This is definitely a pro feature and should not slow down market creation.
In `on_resolution` of zrml-prediction-markets, we then need to consider the case where the `outcome_report` matches `OutcomeReport::Invalid` and set the payout vector of the market to be the `payout_on_invalid_resolution`/the uniform vector. That should cover all necessary changes.