This bulletin details the theory and design behind Forecasting Technologies' implementation of Referendum #502.
Most of this research is based on Gnosis' documentation and implementation. Our implementation will closely follow theirs to ensure testability. All files will therefore include a Gnosis copyright notice and LGPL license note.
As of version <0.6.0
, when a market resolves, it resolves to a particular outcome (in case of categorical markets) or a number (in case of scalar markets). For various reasons, it's easier to think of a resolved market having a payout vector though.
The payout vector of a resolved market with outcomes is a vector of non-negative numbers with . Redeeming one unit of the outcome token yields units of collateral.
It's also convenient to denote the payout of an outcome by .
For prediction markets with outcome sets , and for outcomes , we denote by or the outcome is true if and only if are all true, or, if scalar markets are included, the outcome which has the payoff where is the payoff of .
The combinatorial market derived from can be thought of as a market whose outcome tokens are for .
But there are other types of combinatorial tokens. We can also introduce conjunctions for outcomes of the same market: If are outcomes from the same market, then the conjunction is true if and only if at least one of the outcomes is true, or, if scalar markets are included, the outcome which has the payoff .
Putting it all together, if are outcomes of for all , then the outcome
has the payout
However, in Zeitgeist's implementation, where the resolution of markets and the trading of markets is completely separate, it's better not to think of the markets, but rather the pools as having been combined. In other words, a combinatorial pool for the markets is just a pool that contains, for all , the outcomes . Such a pool facilitates trading of outcomes contingent on other outcomes, but allows Zeitgeist to retain the simple resolution and dispute logic of separate categorical or scalar markets.
Buying individual outcomes of a combinatorial market allows the user to bet on more specialzied outcomes (A and B will happen), but the true strength of combinatorial markets lies is making bets on contingencies (if A occurrs, then so will B).
The following example is taken from [H13]. Let denote the event that the Democratic Party wins the 2016 U.S. Presidential election and let denote the event that Hillary Clinton is nominated as the Dem's candidate. The pairs of securities (i.e. "Pays 1$ if " and "Pays 1$ if not ") and are traded on separate markets, but can be combined into a single market by using splits.
If you know how buying complete sets works on Zeitgeist, you already know one example of a split: The collateral token is split into a set of tokens which, when the markets resolves, will have the same value as the collateral token. By the same reasoning fashion, we can split non-collateral tokens like "Pays 1$ if " into "Pays 1$ if and " and $Pays 1$ if and not ". For the sake of simplicity, we denote these tokens by , and , resp. By combining the two markets above, we create a new market with four assets: , , and .
Using splits, agents can now bet on correlations between the events and . For example, to bet that the Dems win if Hillary is nominated, the agent would split their collateral into units of and , then split these units into units of and , then sell their units of for more . There are three possible outcomes:
The spot price of this swap is equal to the conditional probability predicted by the market. We denote the trade described above by .
To prevent arbitrage opportunities, buying such a composite asset must not move the price of uninvolved outcomes. For example, betting that holds if holds should leave the price of unchanged. After all, the agent is betting on what happens contingent on ; they're not making any claims as to how likely is to occur. This is summarized in the modularity property defined by Hanson in [H03a]:
Consider the case of an agent who bets with a market scoring rule, and bets only on the event given the event . That is, this agent gains assets of the form “Pays $1 if and hold”, in trade for assets of the form “Pays $1 if holds and does not. […] In general, depending on the particular market scoring rule, such a bet might change any probability estimate , and thus change any event probability p© = \sum_{i \in C} p_i$. It seems preferable, however, for this bet to change as little as possible besides (and of course ).
(The market maker implemented in zrml-neo-swaps has that property.)
A fundamental idea is that trades made on prediction markets are not so much absolute bets ("I bet $10 that Hillary will win at 1:1 odds"), but rather that trades are made to "correct" the forecast of the market by changing the prices of the outcome. The agent executing the trade pays a price to do so, but is rewarded if the prediction is closer to the truth that the current one.
One could say that the agent buying claims that is undervalued and is overvalued, and that and are correctly priced or the agent doesn't know how to profitably change their price.
Viewed from this angle, a combinatorial bet divides the assets in a combinatorial pool into three categories: buy (the assets the agent thinks are undervalued), sell (the assets the agent thinks are overvalued) and keep (the assets whose prices the agent can't or won't change).
This module is responsible for handling the minting and burning of tokens representing combinatorial positions as described in ZIP 10.1.
An important distinction which we've so far neglected to make is the distinction between an abstract collection and a position, which is a collection together with a collateral token. Collections are purely abstract and used in the implementation. Positions are actual tokens on the chain.
Every collection and position has an ID, a 256 bit value, which is used to uniquely identify it. The ID need not be human-readable.
Gnosis use the notation (A|B)&X
for the token (that's a collection) and the notation $:(A|B)&X
for the position obtained by taking that collection and using the collateral token $
. We assume that the collateral token is always clear from the context and use the same notation for collections and positions.
Splitting is the process of creating collections and combinatorial positions from collateral or other outcome tokens. Merging is the process of putting them back together to simpler positions or collateral.
There are two types of splits and merges, vertical and horizontal. Vertical splits increase the depth or complexity of the token, while horizontal splits create finer grained tokens of the same depth. We'll describe these types below, considering the special case where collateral is involved separately.
Splits and merges are specified using partitions of the index set of the markets outcomes. If the market has outcomes, the instructions for a split are encoded in a partition of the index set . The exact meaning of the index set will become clear below.
Collateral can only be split vertically. Let be a market with outcomes and be an partition of . Then the vertical split splits collateral into the tokens for all .
For example, if a market has three outcomes and , then splitting collateral results in And .
Merging into collateral is the opposite process. If, for some partition of , the user owns the tokens for all , then they can merge these tokens into collateral.
It is easy to verify that the value under redeeming is invariant under these operations.
Suppose the user owns a combinatorial token and let be a market with outcome tokens . Let be a partition of . Then the vertical split of using is yields the tokens for .
For example, if , has three outcomes and , then splitting with yields the tokens and .
Vertical merging is the opposite process. If, for some partition of , the user owns the tokens for , then they can merge these tokens into .
It is easy to verify that the value under redeeming is invariant under these operations.
Suppose the user owns a combinatorial token which, for a market with outcomes , takes the form where . Then for a partition of (not !), the token can be split into the tokens for all .
For example, given two markets with outcomes and , resp., consider the token . Then . Let . Then can be split into and .
Horizontal merging is the opposite process. If, for some partition of a set , the user owns the tokens for , the user can merge these tokens into .
It is easy to verify that the value under redeeming is invariant under these operations.
It is possible to partially redeem tokens. Suppose a combinatorial position contains multiple markets and is resolved (recall that order doesn't matter, and it's convenient for us to assume that the last market is resolved). Then we can redeem the combinatorial position, but, unless , instead of receiving collateral, we will receive a position over the markets .
If denotes the payout of , then the token redeems for units of . In particular, if , then redeems for units of collateral.
More generally, the outcome token redeems for
units of .
The implementation will happen as substrate pallet.
The pallet's Config
will have an associated type CombinatorialIdManager
which will serve as the equivalent of the CTHelper
class from Gnosis' original implementation:
The standard implementation of this trait is discussed in a separate section.
The pallet will have the following extrinsics:
One important distinction between splits and merges involving collateral and proper combinatorial tokens is that when collateral is split, the collateral, instead of being burned, is moved into the pallet account. This ensures that combinatorial-tokens never changes the total issuance of the collateral token. Conversely, if combinatorial tokens are merged into collateral, the collateral is moved out of the pallet account into the user's wallet.
On the other hand, if proper combinatorial tokens are split or merged, they are simply minted and burned accordingly.
Note that, in a departure from the original design used in prediction-markets for handling the collateral used to back outcome tokens, individual markets don't have their own separate account for storing collateral. This would be impossible, as the following example shows.
Let be a market with outcomes and be a market with outcomes . Suppose now the following happens:
But if separate markets had separate accounts, the DOT would be deposited into the account of but the DOT in the last step would have to be withdrawn from the account of .
This shows that using combinatorial tokens, Zeitgeist actually because one giant omni market of predictions.
CombinatorialIdManager
The standard implementation of CombinatorialIdManager
using elliptic curves was pioneered by Gnosis.
The reason IDs are used in place of the entire data of the collection is that storing the entire collection data is heavy in terms of storage and comparing two collection IDs is heavy in terms of computation. The ID of a collection is defined as a cryptographic hash and designed to be sufficiently collision resistant. Beyond that, it satisfies the property that the order in which tokens are split does not matter. This is achieved by using elliptic curve operations.
alt_bn128
alt_bn128
was defined in (Reitwiessner, 2017) as the elliptic curve defined by the equation
over the Galois field with modulus
It was introduced on Ethereum to be used for zkSNARK verification. A Rust implementation may be found here.
A collection ID is a 256-bit number with the following properties:
alt_bn128
.We denote a collection ID by , where is the second most significant bit and is the big endian unsigned integer defined by 254 less significant bits.
A primitive collection is mapped to its associated collection ID by first mapping it to a point on the elliptic curve alt_bn128
, denoted as , and then compressing this curve point into a collection ID.
The compression algorithm is based on two simple observations:
Let denote the set of 256-bit unsigned integers. The compression uses the second most significant bit to store whether is odd. In pseudo-code:
Unfortunately, not every is the -coordinate of a point on . Thus, decompression of a collection ID is a little more involved. We start using the following observation, which follows from Euler's criterion, which says that is a quadratic residue in if and only if :
Lemma.
If , then is a quadratic residue if and only if satisfies .
Proof. If satisfies , then obviously is a quadratic residue. If, on the other hand, is a quadratic residue, then by Euler's criterion. Thus, we have QED.
Thus equipped, it's very easy for us to define a function which returns a root of its input if is a quadratic residue and None
otherwise:
The algorithm for decompressing a collection ID (odd, x)
is the following:
Mapping a primitive collection (market_id, index_set)
consisting of a market ID and an index set to a point on is done using a variation of the decompress
algorithm and makes use of a cryptographic hasher
with 256 bit digest. The primitive collection is hashed, the result interpreted as 256-bit big endian unsigned integer and the repeatedly incremented until is the -coordinate of a point on alt_bn128
. The excess MSB is used to determine which of the two roots to take:
Note that this ignores the second most significant bit of the hash result h
. This is fine as this only slightly diminishes the collision resistance of the hasher. The fact that Gnosis have chosen to use what's essentially a do-while loop instead of a while loop strikes us as a little odd, but we'll keep this around so we can use Gnosis' implementation to generate test cases for ours.
There is no easily available bound on how many passes the while loop takes until a point of alt_bn128
is found. Statistical testing with random samples of points in gives us the following distribution:
Iterations | Samples |
---|---|
0 | 124995762 |
1 | 62502095 |
2 | 31253303 |
3 | 15627739 |
4 | 7806601 |
5 | 3906045 |
6 | 1955579 |
7 | 976263 |
8 | 488150 |
9 | 244516 |
10 | 121976 |
11 | 61037 |
12 | 30362 |
13 | 15342 |
14 | 7595 |
15 | 3802 |
16 | 1890 |
17 | 974 |
18 | 458 |
19 | 236 |
20 | 130 |
21 | 79 |
22 | 33 |
23 | 19 |
24 | 6 |
25 | 2 |
26 | 3 |
30 | 1 |
31 | 1 |
32 | 1 |
The pattern that emerges is that increasing the number of iterations by 1 halves the number of points that require that many iterations. As exactly half of the points of lie on alt_bn128, we expect no point to require more than iterations.
The runtime implementation will use a for
loop with a maximum number of passes to protect against an outlier to this statistic. The maximum will be a generous overestimation based on the statistic above, subject to the results of the benchmarks.
In particular, if finding the nearest point on-curve takes too many iterations, the function returns an error instead of taking up more resources than allocated. It's worth noting that agents have no control over the hash. Even if they find a particularly bad hash using brute force search off-chain, they won't be able to make any use of it. This leaves a very small likelihood that a user will hit an outlier when trying to split/merge a token.
But these algorithms require us to exponentiate by , which is a giant integer. You'd think it's game over at this point. Enter addition chains.
Let be an integer. An addition chain for is a strictly increasing sequence of integers "starting with and ending with , such that each number in the sequence is the sum of two previous numbers" (see here).
Given an addition chain for , there is an associated algorithm for calculating the powers :
The classical example of an addition chain is calculating the power by calculating , , and finally instead of stubbornly doing , , , etc.
The gist is that addition sequences should bring the computational complexity of calculating down from to .
Determining a sufficiently short addition sequence for a given integer (like bove) is non-trivial, but there's an algorithm (Cohen and Frey, 2005, p. 161-162), although their definition of is flawed and should be
and step 3 in the definition of minchain(n)
should be return chain(
)
. See here for a sample implementation (we have our own prototype implementation). The bets thing is that if is known in advance (which is true in our case), the developers can hardcode the algorithm.
In the case of , the resulting addition chain has length 324, which is only slightly off :
The resulting algorithm can be summarized as follows:
Of course, we will replace the *
operation with a checked Galois field multiplication.
Until now, we've only established how to calculate the IDs of primitive collections. Recall that, as a requirement, the order in which primitive collections are added to the collection should not matter when calculating the ID. So, if are two primitive collections, then and , although they are technically not the same, must have the same ID. This allows users to trade tokens which would technically be different but equivalent in payout.
This is where the elliptic curve comes in. On any elliptic curve, there is a natural commutative pairing , commutative being the key word. To calculate the ID two collections, decompress both, take their sum and then compress them again:
Thanks to the commutativity of , the return value of calculate_collection_id
is independent of the order of its arguments.
The ID of a combinatorial position is a cryptographic hash of the collateral ID and the ID of the collection. The collateral ID is an abstraction of the Asset
object used on Zeitgeist to ensure that changes to the Asset
object doesn't change the expected hashes.
Due to the structure of collection and position IDs, the indexer will have to do some heavy lifting and maintains a bi-directional mapping of collection and position IDs to human readable outcome tokens. This mapping could take the following form:
This means that the position ID [207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139]
represents belongs to the markets with IDs 0
and 5
and represents the token where are the outcome tokens of market 0
and are the outcome tokens of market 5
.
Zeitgeist will use the old system for creating outcome tokens (buy_complete_set
, sell_complete_set
) and redeeming (redeem_shares
) implemented in zrml-prediction-markets (let's call this Zeitgeist 1.0) alongside the new system. The two will not collide in any way (you can use the new system on old markets, etc.). Old and new outcome tokens will not be interchangeable though.
The long-term migration approach will be to officially announce the deprecation of the old system:
base_asset
field - we can just let users split using any collateral they like, provided that it's allowed as base asset for markets. This is not a property of this particular implementation of the split/merge design: We could equally well just have added a collateral
parameter to the buy_complete_set
and sell_complete_set
functions.We've already discussed what combinatorial betting is in ZIP 10.1. We will outline the details here.
Allowing users to make combinatorial bets means allowing them to create pools with combinatorial tokens in them, and to specify arbitrary buy, keep and sell sets (as defined above in ZIP 10.1) when buying and selling.
On the other hand, letting users create pools with arbitrary combinations of combinatorial tokens is bound to be too complex. One straightforward approach that allows capturing all possible combinatorial bets involving a finite set of markets with the same base_asset
field and outcome tokens is to create a single pool containing the following combinatorial positions:
where and as defined above. We call these atoms, as they allow the most fine-grained combinatorial bets on outcomes defined in these markets.
The existing join
and exit
functions for liquidity pools, and the functionality behind then, including liquidity trees, will remain the same for combinatorial pools. Below, we will provide formulas for letting users specify arbitrary buy, keep and sell sets. Thus, we have the two fundamental functions of combinatorial betting covered: Pools and liquidity, as well as opening and closing bets.
Let be a prediction market with outcomes . A combinatorial bet on is a tuple consisting of three mutually disjoint subsets with , and . Note that is allowed to be empty.
The sets , and are called the buy (or back), keep (or void) and sell (or lay) of the bet. The informant making this combinatorial bet wagers that one of the outcomes in will occur if none of the outcomes in occur. If one of the outcomes of occur, their bet is voided.
Combinatorial bets are used to make bets on conditional probabilities. For example, consider two events and which are used to define a market with four outcomes: , , and , where overbar denoted negation ( means " does not occur") and denotes conjunction. If an informant wants to bet that if occurs, then occurs, they make the combinatorial bet , , . If and materialize, the informant will make a profit. If does not materialize, the bet is declared void. If materializes but doesn't, they lose. We denote this bet by .
The execution of a combinatorial bet is implemented in DLMSR by generalizing the buy and sell operations described above to involve arbitrary sets of tokens. Specifically, opening a combinatorial bet for dollars is implemented by buying complete sets and then selling the tokens in for a particular amount of each token in while simply retaining of the tokens in . One can think of the informant's position in as an insurance.
Returning to the example, if and materialize, the informant will make a profit of . If does not materialize, the market resolves to one of the outcomes contained in , which means the informant holds winning tokens, which means they receive back their buy-in of dollars. If materializes but doesn't, they lose their stake in the bet. With this in mind, the notation seems intuitive for arbitrary combinatorial bets ( can be reconstructed as ).
Cancelling a combinatorial bet is implemented by equalizing the informants positions in the tokens of , and by swapping on the market so that they end up holding a certain number of full sets which they can then sell. This is done by performing up to two elementary equalizations, which are described below: We first equalize, say, and , and then equalize and . As DLMSR is path-independent, this particular order can be replaced with any of its permutations.
Considering the implementation described above, note that buying and selling as defined earlier in the document is just a combinatorial bet with and .
As in the previous sections, let denote the trading function. We will fix once and for all and just denote it by for the sake of convenience. For any , we define
and for every subset , we define
Note that
The derivative of is
For the sake of brevity, we also let operate on by translation and write
Note that, using the properties of the exponential function, we have .
Recall that opening (buying) a combinatorial bet for dollars is implemented by buying complete sets and then selling the tokens contained in for more units of each token in . This changes the reserve of the pool to
Thus,
Solving for yields
The argument of the can be rewritten to solve certain numerical issues. For example, the terms and can be replaced by their counterparts and , resp. For example,
Note also that the entire expression can be split using .
As the combinatorial bet consists of different amounts of the tokens from and , there is, strictly speaking, no clearly defined "total amount" received. However, it makes sense to consider the amount of tokens of that the informant receives for spot price calculations.
In fact, the spot price can then be calculated using l'Hopital's rule, as follows.
We have , and, by virtue of a simple calculation,
Thus,
Using the properties of the trading functions, we get
and, thus, finally,
Note that this is compatible with the formula for the spot price of a single outcome token if , .
Suppose we have two non-empty disjoint sets . Let . Further assume that an informant has units of (each token in) and units of (each token in) . Now suppose that and they want to sell units units of for more units of so that they end up with the same amount of and . This process is called equalization.
Let be the amount of received. This means that the informant holds units of and units of after the trade. The reserves of the pool, on the other hand, are
post execution. Note that, as the informant now holds the same amount of and , we have and thus
Using the DLMSR trading function, we get
Solving for yields
The amount of the informant now holds is (in analogy to earlier sections) . Same comments as above apply regarding approaches to solving numerical issues.
Cancelling (selling) a combinatorial bet is implemented by performing one or two equalizations and then selling whatever amount of complete sets this leaves the informant.
Specifically, if , then the combinatorial bet is sold by equalizing and , leaving the informant with an equal amount of both and . But as , these holds are just complete sets, which are then sold.
If , then the sell is implemented by first equalizing and , leaving the informant with the same amount of and (but in general different amounts of and ), and thereafter equalizing and , leaving them with a certain amount of full sets.
The order of these two equalizations can be changed thanks to the path-independence of DLMSR. This could be helpful in creating situations which are numerically well-behaved.
In summary, the naive algorithm for selling a position in pseudocode is the following:
Combinatorial betting will be implemented as an addition to the neo-swaps pallet. The following significant changes are made outside of adding extrinsics.
Config
. Pools previously indexed by their market ID will just keep their index - since we'll just the same type for market IDs and pool IDs, we'll be fine.PoolStorage
interface implements a counted map which doesn't recycle pool IDs.MaxSplits
associated type in Config
specifies how many splits are allowed when creating a combinatorial market.PoolType
struct implements a method that allows iterating over the markets. External fees will be charged for every market associated with the pool.assets
object which contains all assets held in the pool for convenience.The following extrinsics are added:
The old extrinsics are only modified in that they will use the newly implemented combinatorial math for calculations.
One issue when calculating the formulas above is the exponential function and how easily it can over or underflow. We solve this issue simply by rejecting any trade that would result in such in any image of becoming too large or small. This essentially results in us rejecting any trade that pushes the price of any asset in the pool above or below a certain theshold.
This might sound like a problem upon first glimpse, as it looks like this could "deadlock" trading. If a particular asset's price drops very close to the lower limit, it can not effectively be used as the sell
parameter of a buy operation.
But this is actually not an issue, both in terms of UX as well as in terms of effective information aggregation: If an asset's price drops this low, that means the market deems it very unlikely. If an agent adds this asset to the sell
parameter of the upcoming combinatorial bet just means that they wish to provide the information that they think that the outcome is even more unlikely. That's… not very helpful. The LPs, who are basically paying for this information, are right to reject that trade.
But what if the agent has some useful information on some other outcomes and their trade is rejected due to their stance on some very cheap asset? They can, instead of adding it to the sell
parameter, just add it to the keep
parameter. This won't change the conditions of their trade very much, but will allow the trade to go through without any numerical calculations throwing errors.
In addition to creating combinatorial pools, it should also be possible to merge two standard pools into a combinatorial pool. (The terminology is a bit odd here: The pools are merged, but this done by splitting tokens.) The backend is able to accomodate this process, but will not implement it directly. Instead, this is something to be handled on the frontend.
We must assume that an LP owns two liquidity pools and wants to merge them. Suppose the first pool has the outcome tokens and the second pool has the outcome tokens . Denote their prices by and , resp. The process of the merge is the following then:
The choice of price is justified as follows: The event is represented in the pool as the set of events for . That means that the sum of the prices of for should be equal to the original price of . And this is actually the case, as
thanks to the fact that DLMSR guarantees all prices to sum up to . The same argument yields . This means that the pool is created with the exact right prices. Note that this sort of construction is impossible with market makers that don't satisfy the no-arbitrage property.