owned this note
owned this note
Published
Linked with GitHub
:::info
Author: Dr. Malte Kliemann
Revision: 2
:::
# Introduction
Zeitgeist currently uses the Balancer AMM design (see [MN19]), a geometric mean market maker and a variant of the classical constant product market maker (CPMM), to facilitate trading of outcomes. The liquidity pool for a market contains outcomes as well as the base asset (currently ZTG), and the Balancer AMM calculates prices of tokens as functions of these balances and the weights of these assets.
The previous design was plagued by arbitrage problems (described in previous ZIPs) and ineffective liquidity. This bulletin summarizes research for replacing this market maker. We describe a new market maker, a crypto-friendly implementation of the LMSR originally designed by Robin Hanson and previously considered by Gnosis.
# Theory
## How to Fix Zeitgeist's Arbitrage Problem
On a high level, the problem with Zeitgeist's current design (let's call this CPMMv1) is that it offers two ways to trade collateral for outcomes. One is to buy/sell complete sets, and one is to swap collateral for outcomes using the liquidity pool. The issue is that the two paths may not yield the same result. In fact, this is rarely the case. To use a fashionable word, the two operations are not "path-independent", which allows informants to cycle the AMM. The on-chain arbitrage hotfix executes this cycle in favor of the LPs, which is a good start, but it doesn't resolve the underlying issue, that is, the fact that there are two paths to begin with.
We developed CPMMv2 based on a suggestion from Colin Grove of HydraDX to remove the collateral from the liquidity pool, which eliminates one of the two paths. We discovered later that Gnosis have already used a very similar design referred to as Gnosis Omen or FPMM (fixed product market maker), see [gnosis/conditional-tokens-market-makers]. This type of market maker also appears on some other prediction market platforms. In fact, the LMSR variant we present below uses the same design.
Eliminating the collateral means that buying the $i^{\mathrm{th}}$ outcome for $x$ units of collateral is implemented as follows:
- Buy $x$ complete sets.
- For each $k \neq i$, sell $x$ units of $k$ for more $i$.
Selling can be implemented similarly.
## Definition of the Pool-Based LMSR
The problem with using a cost function is that it's completely unclear how to implement dynamic liquidity provision. On the other hand, the pool-based implementation of the constant product market maker exposes such a feature with ease. By _pool-based implementation_ we mean a market maker which calculates prices not based on the total issuance $q$ of the outcomes like the cost function implementation of LMSR, but rather based on the balances of outcomes held in the market maker's _pool_. Outcome tokens for trading are acquired by the users through the mechanisms of buying/selling complete sets. Buying outcomes of only one type is implemented by buying complete sets and then selling the unwanted tokens to the pool.
### The Invariant
Let $n \geq 2$ be the number of outcomes and let $r = (r_1, \ldots, r_n)$ denote the balances of outcomes in the pool. The pool does not hold any balance of the collateral token. The _invariant_ is defined as
$$
\varphi(r) = \sum_{i = 1}^n e^{-r_i/b},
$$
where $b > 0$ is the _liquidity parameter_. When the pool is created, $b$ is chosen so that $\varphi(r) = 1$. A trade $r \rightarrow r'$ changes the balances of the pool from $r$ to $r'$. The agent executing the trade send/receives the diff $r' - r$; positive (resp. negative) numbers are funds sent to (resp. received from) the pool. A trade is _valid_ if $\varphi(r') = \varphi(r)$. Only valid trades may be executed. This means that the invariant will always equal $1$.
The spot price of the collateral-$i$ swap in this market maker is $p_i(r) = e^{-r_i/b}$. In particular, $\sum_i p_i(r) = \varphi(r) = 1$, so the predictions always add to exactly 100%.
All this means that we're implementing LMSR as a constant function market maker. This appears to have been discovered by Gnosis.
### Buying and Selling
Buying and selling against collateral work as described in the section further above. Only collateral-outcome swaps are allowed for now; outcome-outcome swaps will be implemented later. The allowed swaps are:
- Swap a specified amount $x$ of collateral for an unspecified amount $y$ of $i$.
- Swap a specified amount $y'$ of $i$ for an unspecified amount of collateral $x$.
Swap fees are taken in the form of collateral:
- When swapping collateral in, fees are taken from the amount $x$ going in _before_ the amount $y$ is calculated.
- When swapping collateral out, fees are taken from the amount $x$ going out.
The fees are distributed amongst the liquidity providers according to their share of the pool, but the implementation will move the fees to pool account and only distribute them to the LPs when they close their positions. Meanwhile, the on-chain logic will track how much each LP is owed. Note that this is not tracked through LP tokens alone: A user who joins the liquidity pool will immediately receive LP tokens according to their share but should have no claim to any fees collected before they joined.
The pallet zrml-swaps will allow to hook into fee usage to define burn mechanics or add recipients.
### Dynamic Liquidity
The advantage of the inventory-based implementation is that it has a straightforward method of crowdfunding liquidity. Suppose the pool's current reserves are $(r_1, \ldots, r_n)$ and liquidity parameter is $b$. If you want to provide liquidity, you choose some percentage $t$ and change the balance vector to $r' = (1 + t)r$ to the balances of the pool. The funding parameter changes to $b' = (1 + t)b$. This doesn't change the price of any asset:
$$
e^{-r_i'/b'} = e^{-r_i / b}.
$$
In particular, depositing liquidity doesn't change the invariant. Withdrawing liquidity works the same way.
The [Conditional Tokens Docs] claim that:
> The CPMM can also be easily dynamically crowdfunded similar to Uniswap and Balancer pools. LMSR market makers cannot be easily crowdfunded dynamically though. The funding parameter of the LMSR (derived from a 'liquidity parameter' in the literature), has no elementary closed form depending on the outcome token amounts in an LMSR instance's inventory, even though this parameter is completely determined by the instance's inventory.
It's true that given a balance vector $r$, it is probably difficult to calculate a closed form expression of $b$ in terms of $r$. Nevertheless, dynamic liquidity can be implemented by scaling the pool balances as specified above.
<!-- '> -->
To avoid exploits, an exit fee is charged from LPs who withdraw from the pool before the pool is closed. The exit fee goes directly into the treasury.
# Protocol Changes
## Swapping
This ZIP will implement the following collateral-outcome swaps:
- `buy`: Swaps a specified amount $x$ of collateral for an unspecified amount $y$ of $i$.
- `sell`: Swaps a specified amount $y'$ of $i$ for an unspecified amount of collateral $x$.
Both functions will have slippage control parameters. Outcome-outcome swaps will only be implemented in a later ZIP. The trade fails if the pool is drained. The pool may reject the trade if the liquidity is too low.
### Details
Let $b > 0$ be the liquidity constant of the pool, $r$ the current balance of the pool account and $f$ the fraction of fees taken (for example, $f = 0.03$ for 3% fees).
`buy` has the following side effects:
- Distribute $fx$ units of collateral as fees.
- Let $x \leftarrow x - fx$.
- Transfers $x$ units of collateral from the signer to the market account.
- Mints $x$ units of $k$ for each $k = 1, \ldots, n, k \neq i$ in the pool account.
- Mints $x$ units of $i$ in the signer's account.
- Transfers
$$
y = b \ln \left( \frac{1 - e^{y/b} e^{-r_i/b}}{1 - e^{-r_i/b}} \right)
$$from the pool account to the signer's account.
The effects of this function are equivalent to buying $x$ complete sets and then performing a OT-OT swap $r \rightarrow r'$ where $(r' - r)_k = x$ for $k \neq i$ and $(r' - r)_i = y$. The formula of $y$ is derived by solving
$$
1 = \varphi(r') = \sum_k e^{-r_k'/b} = \sum_{k \neq i} e^{-(r_k + x)/b} + e^{-(r_i - y)/b}
$$
for $y$.
It's easy to see that this process maintains the validity of the market account's reserve (each complete set is backed by one unit of collateral).
`sell` has the following side effects:
- Burns $x$ units of $k$ for all $k \neq i$ in the pool account.
- Burns $y = y' - x$ units of $i$ in the signer's account.
- Transfers $y$ units of $i$ from the signer's account to the pool account.
- Let
$$
x = - b \ln\left( e^{-(r_i + y')/b} + (1 - e^{-r_i/b}) \right).
$$
- Distribute $fx$ units of collateral _from the market account_ as fees.
- Transfers $x - fx$ units of collateral from the market account to the signer.
The effects of this function are equivalent to finding a number $y < y'$ so that selling $y$ units of $i$ to the pool for the same amount $x$ of each $k = 1, \ldots, n, k \neq i$ leaves the signer with exactly $x = y' - y$ complete sets, which they then proceed to sell using `sell_complete_set`. The formula for $x$ is derived by solving
\begin{align*}
x &= y' - y, \\
1 &= \varphi(r) = \sum_k e^{-r_k'/b} = e^{-(r_i + y')/b} + \sum_{k \neq i} e^{-(r_k - x) / b}
\end{align*}
for $x$.
<!-- -->
It's easy to see that this process maintains the validity of the market account's reserve (each complete set is backed by one unit of collateral).
## Liquidity
Users can interact with the liquidity by depositing or withdrawing liquidity. Due to how swaps are implemented, we cannot rely on liquidity tokens alone to track each user's share of the reserve and fees. Instead, we rely on a liquidity tree based on Azuro Protocol's [Liquidity Tree Pool](https://web.archive.org/web/20230202235313/https://docs.azuro.org/azuroprotocol/technical/liquidity-tree-pool), which uses NFTs to to store information about liquidity providers. For details, see below.
### Liquidity Tree
The _liquidity tree_ is a tool for tracking liquidity in a designated system. This tool is essentially a segment tree with a maximum depth of $D = 10$, which allows for a maximum of $N = 2^D$ nodes. Each node $N$ in the tree represents the position of a liquidity provider.
Each node in the tree contains the following information:
- The owner's `account`
- The owner's `stake` in the pool.
- The owner's `fees`.
- The `descendant_stake` of the owner's descendants.
- The `lazy_fees` for lazy distribution.
The `stake` field is so-to-speak equivalent to the pool shares balance in the old AMM design.
A node of the tree is deemed _abandoned_ if the owner's stake is zero. Alongside the tree, two maps are maintained:
- A map which stores abandoned nodes. This map enables nodes abandoned by their owner to be _reassigned_.
- A map which maps owners
#### Lazy Propagation of Fees
Fees are propagated up the tree in a 'lazy' manner, i.e., the propagation happens when liquidity providers deposit or withdraw. The process of lazy propagation at a node $n$ is as follows:
```
If node.descendant_stake == 0 then
node.fees ← node.fees + node.lazy_fees
Else
fees ← (node.descendant_stake / (node.stake + node.descendant_stake)) * node.lazy_fees
node.fees ← node.fees + fees
remaining ← node.lazy_fees - fees
For each child in node.children() do
child.lazy_fees ← child.lazy_fees + (child.stake / child.descendant_stake) * remaining
End For
End If
node.lazy_fees ← 0
```
#### Denial of Service Attack
An attacker might attempt to execute a Denial of Service (DoS) attack. This attack comes in two flavors, both griefing-only vectors: preventing others from joining the tree and degrading its performance.
The first aspect of the attack, preventing others from joining the tree, would involve an attacker adding liquidity repeatedly to fill up all the positions in the tree. Our current structure counters this by increasing the required liquidity for each position by 1%. Therefore, to prevent others from joining the tree, the attacker would need to provide the $1.01^N \approx 26612$ fold of the initial required liquidity at the final position. Given a minimum liquidity of 10 DOT, this would amount to a substantial sum, well over a million dollars at the current market price. Moreover, if the attacker withdraws their liquidity, the abandoned nodes are reassigned, forcing the attacker to remain a liquidity provider to maintain the attack.
The second aspect of the attack, degrading the performance of the tree, is largely mitigated by the design of the liquidity tree. The operations of adding to or withdrawing from the tree have time complexity of $O(log n)$, due to the binary tree structure. Therefore, regardless of the number of nodes in the tree, the time required to perform these operations increases logarithmically, not linearly. This limits the effectiveness of an attack aimed at degrading performance by clogging the tree with nodes.
In summary, although a determined attacker might attempt to clog the liquidity tree, the economic and computational challenges posed by such an attack make it unlikely to be successful or sustained.
#### Limitations
- Can't have more than $N = 1024$ liquidity providers per pool.
- Reading data from the tree instead of an indexer may cause some confusion.
### Liquidity Management
The fundamental rules for managing liquidity largely remain consistent. This ZIP will implement the following extrinsics: `join`, `exit`, `split`, `withdraw_fees`, `register_base_asset`.
Users can deposit liquidity within the following constraints, also known as _joining_ the pool:
- Each position must hold more than a specific minimum _value_. The value of a position is defined as the lowest amount of outcomes it contributed. For instance, if Alice contributes $(10, 20)$ as liquidity to a binary market, the value of her position would be $10$.
- The stake of each position must account for at least 1% of the stake already available in the pool.
- There must be available space left in the liquidity tree or the user is already a liquidity provider (LP) for this pool.
The minimum quantity of liquidity per position is determined as a function of the collateral token (also known as the base asset) to accommodate differences in economic value. The minimum should range between $10 to $100 per position. The only parameter of `join` are the amount the signer wishes to join with. The implementation may add control parameters.
Liquidity providers (LPs) have the option to withdraw liquidity at any point, termed as _exiting_ the pool. However, if the withdrawal occurs before the pool is closed, a .1% _exit fee_ is payable to the treasury, subject to one of the following conditions:
- The LP withdraws their entire position.
- The remaining position matches or exceeds the minimum value (as discussed earlier) and must represent at least 1% of the stake available in the pool.
The only parameter of `join` is the amount of liquidity shares to withdraw. The implementation may add control parameters.
LPs can also _split_ their position, transferring a portion of their stake to another user, under the following circumstances:
- The LP splits the entire position.
- Both the updated previous position and the new position meet or surpass the minimum value (as mentioned earlier) and must constitute at least 1% of the stake available in the pool.
Additionally, one of the following must be true:
- There is available space in the liquidity tree.
- The receiving user already owns a node in the tree.
It is crucial to note that nodes are never removed from the tree. When a user withdraws or splits all their liquidity, their node is simply abandoned.
Fees are distributed according to the following procedure: Before a node undergoes changes (specifically, before a node is added), fees are lazily propagated along a path from the root to the node's (future) sibling. This mechanism ensures that fees are always distributed fairly prior to any changes in stakes. For examples, refer to [this link](https://github.com/Azuro-protocol/LiquidityTree).
Fees are withdrawn when the LP closes their position, but they can be withdrawn with a specific extrinsic, as well.
#### Details
Let $b > 0$ be the liquidity constant of the pool, $r$ the current balance of the pool account and $f$ the fraction of fees taken (for example, $f = 0.03$ for 3% fees).
Calling `join` has the following effects:
- Lazily propagate fees along a path from the root of the tree to the signer's `node`, or the next free leaf or abandoned node. If it's a free leaf, initialize a new default node. If it's an abandoned node, remove it from the list of abandoned nodes.
- Let $m = \max_i r_i$ and $t = x / m$.
- `node.total_stake += x` (`node.total_stake == 0` for free leaf or abandoned node).
- Ensure that the future `total_stake` of the LP is at least 1% of the `total_stake` of the root node and that $r_i \cdot \texttt{total_stake} / (1 + t)$ is at least the minimum value of liquidity. Revert if not.
- Transfer $\frac{r_i}{m} \cdot x$ units of $i$ from the signer to the pool account for all $i = 1,\ldots,n$.
- Add the signer's node to the account-node hash map.
- Propagate the updated `total_stake` down the tree to the root.
Calling `exit` has the following effects (with $s$ denoting the amount of stake removed):
- Lazily propagate fees along a path from the root of the tree to the signer's `node`.
- `node.total_stake -= s`
- Let $T$ be the total stake (including the root's stake) and $t = s / T$.
- Ensure that the remaining `node.total_stake` of the LP is either zero or at least 1% of the `total_stake` of the root node and that $r_i \cdot \texttt{total_stake} / (1 + t)$ is at least the minimum value of liquidity. Revert if not.
- Transfer $tr_i$ units of $i$ from the pool account to the signer for all $i = 1,\ldots,n$.
- Remove the signer's node from the account-node hash map.
- Propagate the updated `total_stake` down the tree to the root.
The effects of `split` are a combinations of `join`/`exit` and are left out for the sake of brevity.
Calling `withdraw_fees` has the following effects:
- Lazily propagate fees along a path from the root of the tree to the signer's `node`.
- Transfer `node.fees` units of collateral from the fee account to the signer's account.
- Set `node.fees` to zero.
### Draining Pools
A pool is _drained_ if all nodes in the liquidity tree are abandoned. Note that nothing in the rules above prevents users from draining a pool that is not closed or from "refilling" a drained pool. When refilling a pool, the new LP must specify initial odds.
### Pool Deployment
~~Every market is automatically equipped with a pool now. This results in no disadvantages, but allows users to provide liquidity even to proposed markets, which improves UX.~~
Every market is automatically equipped with a pool now when it opens. This results in disadvantages, but allows users to completely remove their liquidity, slightly improving UX. Note that proposed markets cannot have pools and LPing (yet), because we wouldn't know how to deal with the complete sets owned by users after the market was rejected and deleted. (One possible solution is to add a new state `Rejected`, move rejected markets into this new state instead of deleting them and let users call `sell_complete_set` on rejected markets.)
Initial prediction are still allowed. Unlike in the old AMM, initial odds are not achieved by manipulating weights (LMSR has no such thing), but rather by adding different amounts of tokens to the pool. If the user wants to deposit liquidity worth $x$ units of collateral with initial probability $p$, they start off by buying $x$ complete sets. The following algorithm is used to calculate how many units of each outcome go into the pool. The LP retains the other tokens.
Let $b = 1$ (larger values may be picked for numerical stability), and let $r_i = - b \ln p_i$ for all $i$. Now let $y = x / \max_i r_i$. Then $y r_i \leq x$ for all $i$ and there exists $i_0$ so that $y r_{i_0} = x$. Set $\tilde r_i = y r_i$ and $\tilde b = yb$. Then
$$
p_i(\tilde r) = e^{-\tilde r_i/\tilde b} = e^{-r_i/b} = p_i
$$
and $\max_i \tilde r_i = x$ (so the future LP uses up at least one of their outcome balances).
In pseudocode:
```
Procedure CalculateBalances(p[1...n], x)
b ← 1 // Initialize b, larger values may be picked for numerical stability
For i from 1 to n do
r[i] ← -b * log(p[i])
End For
y ← x / max(r[1...n])
For i from 1 to n do
r[i] ← y * r[i]
End For
b ← y * b
Return r, b
End Procedure
```
## Other Changes
The following extrinsics are removed with replacement:
- `pool_exit_subsidy`
- `pool_exit_with_exact_asset_amount`
- `pool_exit_with_exact_pool_amount`
- `pool_join`
- `pool_join_subsidy`
- `pool_join_with_exact_asset_amount`
- `pool_join_with_exact_pool_amount`
- `swap_exact_amount_in`
- `swap_exact_amount_out`
- `create_cpmm_market_and_deploy_assets`
- `deploy_swap_pool_and_additional_liquidity`
- `deploy_swap_pool_for_market`
The extrinsic `pool_exit` is kept around as legacy function so that LPs can withdraw their funds from the old pools. It should be deprecated.
The `scoring_rule` field of the `Market` struct will have two fields, `Cda` and `Lmsr`. Until further notice, `create_market` will reject markets with `Cda`.
# Advantages
## Liquidity
- The pool-based implementation of LMSR fixes all arbitrage problems that the old AMM suffered from. In particular, liquidity is now considerably more effective:
![](https://hackmd.io/_uploads/HJ2FN5pu3.png)
- Allowing pool to be drained and refilled fixes some pain points in handling liquidity.
## Combinatorial Markets
LMSR is the only market maker that is _modular_. This means that when trades occur between different outcomes, only the prices of those specific outcomes involved in the trade are affected. The prices of outcomes that are not part of the trade remain unchanged. This is generally desirable in prediction markets in general, but particular in combinatorial markets.
# Pain Points
Our Logarithmic Market Scoring Rule (LMSR) implementation faces the _leftover token problem_, as outlined in the [Maniswap AMM](https://manifoldmarkets.notion.site/Maniswap-ce406e1e897d417cbd491071ea8a0c39) primer. As a pool's balances become unbalanced, more tokens remain in a user's wallet after providing liquidity.
> Suppose you have $100 and are trying to initialize an AMM’s liquidity pool to be at an implied 33% probability using Uniswap.
>
> If you only have $100, then that means that you can put up at most 100 YES shares and 100 NO shares. To get the right probability with this constraint, you must create the liquidity pool with 100 YES shares and 50 NO shares (since 50/(100+50) = 1/3).
>
> But notice that if you started with $100, you still have 50 NO shares left over. This is unfortunate. Ideally, you would prefer to deploy all of your capital to subsidizing the market, not just when the initial probability is 50% (which is the only case when you can deploy 100 YES, 100 NO).
Adding 100 tokens of each type to the pool isn't a solution due to resulting price movements. Manifold Markets, however, uses a binary-market solution of 1) adding equal tokens and 2) adjusting pool weights to prevent price movements. Unfortunately, no successful generalizations for multiple assets exist: [Link](https://manifoldmarkets.notion.site/Multi-CPMM-62fe5b99013c4d5a87dfa84e0b8fa642).
But this problem is mostly cosmetic. Consider Alice wishing to start an 80%-20% prediction binary market with Uniswap/Balancer AMM and $100 liquidity. After buying 100 complete sets, Alice has two options:
- Pool with even weights, 25 YES tokens, and 100 NO tokens. Alice retains 75 YES tokens.
- Pool with 4:1 weights and 100 tokens of each type. Alice has no leftover tokens.
Slippage turns out to be similar in both scenarios:
![](https://hackmd.io/_uploads/Byo5W5Td2.png)
![](https://hackmd.io/_uploads/rJWs-9p_n.png)
So both approaches offer similar liquidity. Manifold Market's method just doesn't confuse users about leftover tokens. In the first scenario, due to lack of weight, the 25 YES tokens create similar price resistance as the 100 YES tokens in the second scenario.
# Deployment
## Frontend
- Trading can remain the same, although we recommend the following info fields: _Estimated price_, _potential return_, _fees_. Spot prices should be displayed _including_ fees.
- Liquidity providing needs to be changed a little bit (minimum amount, etc.)
## Subsquid
- The liquidity tree itself is an implementation detail of Zeitgeist and does not have to be tracked. The `stake` field of a node is equivalent to the number of liquidity tokens in the old iteration.
## Migration
The old CPMM pools are retained and the `pool_exit` function kept around to allow legacy LPs to retrieve their funds. Meanwhile, each market is automatically equipped with a new pool which can be supplied with liquidity.
As the old pools are disabled after applying the changes, this means that trading will halt for a moment after the updates as LPs move their funds from the old pools to the new pools. It should be communicated clearly to the users that `pool_exit` is deprecated and that funds from the old pools should be removed as soon as possible. It should be communicated clearly that governance cannot be used to recover funds from old pools once `pool_exit` is removed.
Note that we have at least one market which uses CPMM and doesn't end before 2025. The advantage of doing this over retaining the CPMM pools is that CPMM can be removed from the code base immediately.
## Audit
An audit should verify that math functions are implemented in a safe and clean fashion.
# Bibliography
- [ACV13] J. Abernethy, Y. Chen, and J. W. Vaughan, "Efficient Market Making via Convex Optimization, and a Connection to Online Learning," ACM Transactions on Economics and Computation, vol. 1, no. 2, pp. 1-39, 2013. [Online]. Available: [https://doi.org/10.1145/2465769.2465777](https://doi.org/10.1145/2465769.2465777)
- [CV10] Y. Chen and J. W. Vaughan, "A new understanding of prediction markets via no-regret learning," in Proceedings of the ACM Conference on Electronic Commerce (EC), pp. 189-198, 2010.
- [DP21] N. De Pablo, "Introducing Zeitgeist’s 'Rikiddo Scoring Rule'," Zeitgeist Blog, 2021. Available: https://blog.zeitgeist.pm/introducing-zeitgeists-rikiddo-scoring-rule/
- [H03a] R. Hanson, "Logarithmic Market Scoring Rules for Modular Combinatorial Information Aggregation," The Journal of Prediction Markets, vol. 1, no. 1, 2003. [Online]. Available: [https://doi.org/10.5750/jpm.v1i1.417](https://doi.org/10.5750/jpm.v1i1.417)
- [H03b] R. Hanson, "Combinatorial Information Market Design," Information Systems Frontiers, vol. 5, no. 1, pp. 107-119, 2003.
- [H13] R. Hanson, "Shall We Vote on Values, But Bet on Beliefs?," The Journal of Political Philosophy, vol. 21, no. 2, pp. 151-178, 2013.
- [MM23] Manifold Markets, Notion Page, 2023. Available: https://manifoldmarkets.notion.site/Home-6878ea9cda834947aa070537154dbf98. Accessed on: Jun. 28, 2023.
- [MN19] F. Martinelli and N. Mushegian, "A non-custodial portfolio manager, liquidity provider, and price sensor," 2019. [Online]. Available: [https://balancer.fi/whitepaper.pdf](https://balancer.fi/whitepaper.pdf)
- [OPRS13] A. Othman, D. M. Pennock, D. M. Reeves, and T. Sandholm, "A practical liquidity-sensitive automated market maker," ACM Trans. Econ. Comp., vol. 1, no. 3, Article 14, Sep. 2013, pp. 1-25. DOI: http://dx.doi.org/10.1145/2509413.2509414
- [P04] D. M. Pennock, "A dynamic pari-mutuel market for hedging, wagering, and information aggregation," in Proceedings of the 5th ACM Conference, 2004. DOI: 10.1145/988772.988799
<!-- Links -->
[ACV13]: https://web.eecs.umich.edu/~jabernet/papers/ACV12.pdf
[CV10]: https://arxiv.org/pdf/1003.0034.pdf
[DP21]: https://blog.zeitgeist.pm/introducing-zeitgeists-rikiddo-scoring-rule/
[Conditional Tokens Docs]: https://docs.gnosis.io/conditionaltokens/docs/introduction3/
[MN19]: https://balancer.fi/whitepaper.pdf
[OPRS13]: https://www.cs.cmu.edu/~sandholm/liquidity-sensitive%20automated%20market%20maker.teac.pdf
[H03a]: https://mason.gmu.edu/~rhanson/mktscore.pdf
[H03b]: https://mason.gmu.edu/~rhanson/combobet.pdf
[H13]: https://mason.gmu.edu/~rhanson/futarchy2013.pdf
[gnosis/conditional-tokens-market-makers]: https://github.com/gnosis/conditional-tokens-market-makers
[P04]: https://www.researchgate.net/publication/279714212_A_dynamic_pari-mutuel_market_for_hedging_wagering_and_information_aggregation