# ZIP-4: AMM 2.0-alpha (Architecture)
We present an architecture design for a watered-down version of the original AMM 2.0 design. We call this AMM 2.0-alpha.
## What's AMM 2.0-alpha?
AMM 2.0-alpha implements all features defined by the AMM 2.0 proposal, except for the following:
1. LMSR pools can only be deployed for markets with two outcomes (binary and scalar).
1. If the price of an outcome reaches $.99$, then buying is prohibited; if the price of an outcome reaches $.01$, then selling is prohibited.
1. LMSR pools must be manually deployed and AMM 1.0 pools are not automatically disabled; LMSR pools can be deployed even if there is already an AMM 1.0 pool.
1. The liquidity tree is not implemented; instead, only the market creator can deploy liquidity to a pool.
The first two points are a simplification which eliminates numerical issues when calculating token amounts. The old pools will remain the only choice for multi-markets. Leaving the implementation of the liquidity tree to AMM 2.0 reduces development time.
## Notes on Frontend Design
### Buying and Selling
Buying and selling on AMM 2.0-alpha and AMM 2.0 has a similar interface to buying on AMM 1.0. We expect that almost change need to be made to the UI/UX (with the exception of enforcing the price limits on the frontend), but the suggest to display the following info to the user in the trade slip when buying:
- Cost (_including fees_)
- Fees
- Estimated price after execution
- Estimated potential return
And the following info when selling:
- Estimated proceeds (with fees already deducted)
- Fees
- Price after execution
Spot prices should be displayed _including_ fees. We will specify formulas for each of these values.
### Providing Liquidity and Deploying Pools
Liquidity providing should take into account the following:
- Users can now withdraw all of their liquidity. If a pool has no liquidity left, it is destroyed. A new pool can be deployed at any time.
- The base asset is no longer part of the pool.
- There is no extrinsic that combines creating a market with deploying a pool. Use `utility.batch_all` for this.
- LPs have to manually withdraw their fees before withdrawing their liquidity. Use `utility.batch_all` for this.
### Migrating from AMM 1.0 to AMM 2.0
- Deploying new AMM 1.0 for pools with two outcomes should no longer be supported on the app (it will remain possible on-chain for now).
- If there are AMM 1.0 and AMM 2.0-alpha pools deployed for a market, then the AMM 2.0-alpha pool should be displayed by the app. However, there must be a button that allows users to withdraw their funds from the old pool. This functionality will be required for _all_ old pools when AMM 2.0 is deployed until we can be reasonably sure that all users have withdrawn their funds or we find a way to safely withdraw all funds from AMM 1.0 pools during a migration.
## Details
### Formulas
In this section, we consider a pool with reserve $r = (r_1, \ldots, r_n)$, which means that there are $r_i$ units of outcome $i$ in the pool, and a liquidity parameter $b$. Recall that the function
$$
\varphi(b, r) = \sum_k e^{-r_k/b}
$$
satisfies $\varphi(b, r) = 1$ for all _valid_ reserves $r$. (Here $e^x$ denotes the exponential function.)
#### Buying
Alice wants to swap $x$ units of collateral for units of outcome $i$. This is done by buying $x$ units of collateral and then selling all outcomes $k \neq i$ for more $i$. The swap yields $y(x)$ units of $i$. _Ignoring swap fees_, this modifies the reserve to $r'$, where $r_k' = r_k + x$ for $k \neq i$ and $r_i' = r_i - y(x)$. As trades don't change the invariant, we have $\varphi(b, r) = \sum_k e^{-r_k'/b}$. Thus, using $1 = \varphi(b, r) = \sum_k e^{-r_k/b}$, \begin{align*} 1 &= \sum*k e^{-r_k'/b} \\ &= \sum*{k \neq i} e^{-(r*k + x)/b} + e^{-(r_i-y(x))/b} \\ &= e^{-x/b} \sum*{k \neq i} e^{-r_k/b} + e^{y(x)/b} e^{-r_i/b}. &= e^{-x/b} (1 - e^{-r_i/b}) + e^{y(x)/b} e^{-r_i/b}. \end{align*}
Rearranging these terms gives
$$
e^{y(x)/b} = e^{r_i/b} (1 - e^{-x/b}(1 - e^{-r_i/b})),
$$
and, thus, \begin{align*} y(x) &= b \ln(e^{r_i/b} (1 - e^{-x/b}(1 - e^{-r_i/b}))) \\ &= b \ln (1 - e^{-x/b}(1 - e^{-r_i/b})) + r_i \\ &= b \ln (e^{x/b} - 1 + e^{-r_i/b}) + r_i - x. \end{align*}
(Here $\ln$ denotes the natural logarithm.)
_With swap fees_ $f$ (specified as fractional, so $1%$ means $f = .01$), the swap fees are deducted from the initial amount $x$, so Alice is left with $\hat x = (1-f)x$ units of collateral, which are then plugged into the formula, so the amount received is $\hat y(x) = y(\hat x) = y((1-f)x)$.
:::info _Note._ $y(x)$ does not include the $x$ units of $i$ that Alice receives from buying $x$ complete sets. The total amount of $i$ that Alice receives is
$$
z(x) = y(x) + x = b \ln (e^{x/b} - 1 + e^{-r_i/b}) + r_i.
$$
:::
#### Selling
Alice wants to swap $x$ units of $i$ for units of collateral. This is done by selling $x'$ units of $i$ for $z(x) = x - x'$ units of all outcomes $k \neq i$ and then selling $x''$ units of complete sets, which yields $z(x)$ units of collateral. Using $1 = \varphi(b, r')$ and $x' = x - z(x)$, we get \begin{align*} 1 &= \sum*k e^{-r_k'/b} \\ &= \sum*{k \neq i} e^{-(r*i - z(x))/b} + e^{-(r_i + x')/b} \\ &= e^{z(x)/b} \sum*{k \neq i} e^{-r_i/b} + e^{-x'/b} e^{-r_i/b} \\ &= e^{z(x)/b} (1 - e^{-r_i/b}) + e^{-x/b} e^{z(x)/b} e^{-r_i/b} \\ &= e^{z(x)/b} ( 1 - e^{-r_i/b} + e^{-(r_i + x)/b} ). \end{align*}
Thus, we get
$$
e^{-z(x)/b} = 1 - e^{-r_i/b} + e^{-(r_i + x)/b},
$$
which in turn yields \begin{align*} z(x) &= - b \ln (1 - e^{-r_i/b} + e^{-(x+r_i)/b} \\ &= -b \ln (e^{r_i/b} - 1 + e^{-x/b}) + r_i \\ &= -b \ln (e^{(x + r_i)/b} - e^{x/b} + 1) + r_i + x. \end{align*}
_With swap fees_, we now deduct the fees from $z(x)$, so the actual amount received by Alice is only $(1-f)z(x)$ and $fz(x)$ goes to the LPs.
#### Spot Price
Recall that the amount received when buying for $x$ units of collateral _ignoring swap fees_ is
$$
z(x) = b \ln (e^{x/b} - 1 + e^{-r_i/b}) + r_i.
$$
The spot price of outcome $i$ is $s_i(b, r) = \lim_{x \rightarrow 0} x/z(x)$. Using L’Hôpital’s rule, we get
$$
s_i(b, r) = \lim_{x \rightarrow 0} \frac{x}{z(x)} = \frac{1}{z'(0)} = \frac{1}{y'(x) - 1}.
$$
Calculating $y'(0) yields $y'(0) = e^{r_i/b} - 1$, and thus $z'(0) = e^{r/b}$ and $s_i(b, r) = e^{-r_i/b}$.
Including swap fees, we replace $z$ with $\hat z(x) = z((1-f)x)$ and get
$$
s_i(f, b, r) = \frac{1}{\hat z'(0)} = \frac{1}{(1-f)z'(0)} = \frac{1}{1-f} e^{-r_i/b}.
$$
#### Estimated Cost When Buying
Estimated cost when buying is just $x$ (including fees).
#### Estimated Price After Execution
After executing a buy for $x$ units of collateral for outcome $i$, the new reserve is
$$
r_i' = r_i - y(\hat x) = -b \ln (1 - e^{-x/b}(1 - e^{-r_i/b})).
$$
Thus, the new price is
$$
s_i(f, b, r') = \frac{1}{1-f} (1 - e^{-x/b}(1 - e^{-r_i/b})).
$$
After executing a sell of $x$ units of outcome $i$ for collateral, the new reserve is
$$
r_i' = r_i + x' = r_i + x - z(x) = b \ln (e^{(x + r_i)/b} - e^{x/b} + 1).
$$
The new price is therefore
$$
s_ib(f, b, r') = \frac{1}{1-f} (e^{(x + r_i)/b} - e^{x/b} + 1).
$$
#### Estimated Potential Return/Proceeds
The potential return of a buy (measured in units of collateral) is $z(x) - x = y(x)$.
Estimated proceeds when selling is $z(x)$ as defined in the section _Selling_ (I know I'm using $z$ twice, I'm sorry, I can't be bothered to fix this).
#### Fees
When buying for $x$ units of collateral, Alice pays $fx$ units of collateral in fees.
When selling $x$ units of collateral, Alice pays $fz(x)$ units of collateral in fees.
### Architecture
The new functionality is implemented as a new pallet zrml-neo-swaps. Some changes to the existing architecture are required.
#### `clean_up_pool`
zrml-swaps uses a couple of API functions to "clean up" the pools after the market is resolved. This hasd two effects:
- Delete the losing outcomes from the market pool.
- Distribute Rikiddo rewards.
The latter will no longer be required now that we're replacing Rikiddo. The former seems completely unnecessary anyways: Why delete the losing outcome only for this particular account? The outcomes should be deleted in the general clean up instead.
#### `scoring_rule`
The field/enum `scoring_rule`/`ScoringRule` will be renamed to `trading_mechanism`/`TradingMechanism` (the term _scoring rule_ is not correct in this context) and will contain the following variants: `Cpmm` and `Lmsr`. If the scoring rule is `Lmsr`, then the new pools can be used. Rikiddo has never been in use on mainnet or testnet, so it can be replaced without concerns.
#### Expose `buy_complete_set` to Other Pallets
The `buy_complete_set` is the only mechanism by which outcome tokens are created, and it should remain so to ensure the validity of the market account. In order to allow other pallets to make use of this mechanism, it should be exposed using an API:
```rust
trait CompleteSetOperationsApi<A, M, B> {
fn buy_complete_set(who: A, market_id: M, amount: B) -> Result<Weight, DispatchError>;
fn sell_complete_set(who: A, market_id: M, amount: B) -> Result<Weight, DispatchError>;
}
```
#### Opening and Closing Pools
LMSR pools use the status of their market to decide whether to allow trading or not. If the market's status is `Active`, trades are accepted. Otherwise, they are rejected.
:::info
_Note._ We currently allow users to buy/sell complete sets while the market status is `Active`. But a market is `Active` when it is created, even if its `period.start` lies in the future. Whether this is a bug or a feature is hard to tell; we don't make use of any of this in the app. Regardless, this means that LMSR pools open as soon as a market is deployed.
:::
#### Math Functions
We'll be using HydraDX's [implementation of transcendental functions](https://github.com/galacticcouncil/HydraDX-node/blob/master/math/src/transcendental.rs) for calculating $\exp$ and $\ln$. They are modifications of the original `substrate-fixed` implementation which doesn't implement negative arguments for $\exp$. In particular, we will use `fixed` to represent numbers in the internal logic.
#### zrml-neo-swaps
The new pallet has the following config parameters:
```rust
#[pallet::config]
pub trait Config: frame_system::Config {
type AssetManager: ZeitgeistAssetManager<Self::AccountId, CurrencyId = Asset<MarketIdOf<Self>>>;
type MarketCommons: MarketCommonsPalletApi<AccountId = Self::AccountId, BlockNumber = Self::BlockNumber>;
type CompleteSetOperations: CompleteSetOperations<Self::AccountId, MarketIdOf<Self>, BalanceOf<Self>>;
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type DistributeFees: DistributeFeesApi<...>;
#[pallet::constant]
type ExitFee: Get<BalanceOf<Self>>;
#[pallet::constant]
type MaxAssets: Get<AssetType>;
#[pallet::constant]
type MaxSwapFee: Get<BalanceOf<Self>>;
#[pallet::constant]
type PalletId: Get<PalletId>;
}
```
The `DistributeFees` object is used to handle additional fees. For example, a part of the swap fees might go to the market creator.
It offers the following extrinsics:
```rust
#[pallet::call]
impl<T: Config> Pallet<T> {
#[transactional]
pub fn buy(
origin: OriginFor<T>,
#[pallet::compact] market_id: MarketIdOf<T>,
asset_count: AssetType,
asset_out: AssetType,
#[pallet::compact] amount_in: BalanceOf<T>,
#[pallet::compact] min_amount_out: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let weight =
Self::do_buy(who, market_id, asset_count, asset_out, amount_in, min_amount_out)?;
Ok(Some(weight).into())
}
#[transactional]
pub fn sell(
origin: OriginFor<T>,
#[pallet::compact] market_id: MarketIdOf<T>,
asset_count: AssetType,
asset_in: AssetType,
#[pallet::compact] amount_in: BalanceOf<T>,
#[pallet::compact] min_amount_out: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let weight =
Self::do_sell(who, market_id, asset_count, asset_in, amount_in, min_amount_out)?;
Ok(Some(weight).into())
}
#[transactional]
pub fn join(
origin: OriginFor<T>,
#[pallet::compact] market_id: MarketIdOf<T>,
#[pallet::compact] pool_amount_in: BalanceOf<T>,
min_amounts_in: Vec<BalanceOf<T>>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let weight = Self::do_join(who, market_id, pool_amount_in, min_amounts_out)?;
Ok(Some(weight).into())
}
#[transactional]
pub fn exit(
origin: OriginFor<T>,
#[pallet::compact] market_id: MarketIdOf<T>,
#[pallet::compact] pool_amount_out: BalanceOf<T>,
min_amounts_out: Vec<BalanceOf<T>>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let weight = Self::do_exit(who, market_id, pool_amount_out, min_amounts_out)?;
Ok(Some(weight).into())
}
#[transactional]
pub fn split(
origin: OriginFor<T>,
#[pallet::compact] market_id: MarketIdOf<T>,
receiver: T::AccountId,
#[pallet::compact] pool_amount: BalanceOf<T>,
) -> DispatchResult {
Error::<T>::NotImplementedError
}
#[transactional]
pub fn withdraw_fees(
origin: OriginFor<T>,
#[pallet::compact] market_id: MarketIdOf<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::do_withdraw_fees(who, market_id)?;
Ok(())
}
#[transactional]
pub fn deploy_pool(
origin: OriginFor<T>,
#[pallet::compact] market_id: MarketIdOf<T>,
#[pallet::compact] amount: BalanceOf<T>,
swap_prices: Vec<BalanceOf<T>>,
swap_fee: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let weight = Self::do_deploy_pool(who, market_id, amount, swap_prices, swap_fee)?;
Ok(Some(weight).into())
}
}
```
The `asset_count` parameter is required for charging a sufficient amount of fees.
#### Liquidity Manager
The liquidity manager tracks LP's stake and fees. It will implement the following interface:
```rust
trait LiquidityManager<AccountId, Balance> {
// Adds `amount` stake for `who` and returns the percentage of the pool to deposit.
fn join(self, who: AccountId, amount: Balance) -> Result<Balance, LiquidityManagerError>;
// Removes `amount` stake for `who` and returns the percentage of the pool to withdraw.
fn exit(self, who: AccountId, amount: Balance) -> Result<Balance, LiquidityManagerError>;
// Transfers `amount` stake from `sender` to `receiver` and returns the percentage of funds to
transfer.
fn split(
self,
sender: AccountId,
receiver: AccountId,
amount: Balance,
) -> Result<Balance, LiquidityManagerError>;
fn distribute_fees(self, amount: Balance);
// Removes fees of `who` and returns the amount of fees
fn withdraw_fees(self, who: AccountId) -> Result<Balance, LiquidityManagerError>;
}
```
It's important to note that the `LiquidityManager` only manages the distribution of stake and fees between LPs. The actual funds are managed elsehwere.
The proposal describes a _liquidity tree_ for tracking this data, but AMM 2.0-alpha will only support one LP per pool. This interface will be implemented by the following struct, which manages a single LP (the market creator) and rejects attempts of others to join:
```rust
struct SoloLp<AccountId, Balance> {
owner: AccountId
total_shares: Balance
fees: Balance
}
```
In AMM 2.0, `LiquidityManager` will be implemented by the liquidity tree struct described in the proposal. It's straightforward to migrate `SoloLp` into a liquidity tree by turning the solo LP into the root of the tree.
### Vista: Numerical Considerations
Special care must be taken to avoid over- and underflows. The problem, for example when buying, is the following: The amount received $y(x)$, written as a function of the amount spent $x$, may be calculated as follows:
$$
y(x) = b \ln (e^{x/b} - 1 + e^{-r_i/b}) + r_i - x.
$$
This is a perfectly fine formula, and the magnitude of $y(x)$ is the same as $x$. But when calculating this formula directly using fixed point numbers with ten digits after the decimal point, the $e^{-r_i/b}$ will underflow to zero if $r_i \geq 25 \cdot b$; similarly, $e^{x/b}$ will overflow if $x \geq 25 \cdot b$. And even before that, a good deal of precision will be lost, resulting in bad results.
#### Buying
Swapping $x$ units of collateral for $y(x)$ units of outcome $i$. This modifies the reserve to $r'$, where $r_k' = r_k + x$ for $k \neq i$ and $r_i' = r_i - y(x)$. As trades don't change the invariant, we have $\varphi(b, r) = \sum_k e^{-r_k'/b}$. Thus, using $1 = \varphi(b, r) = \sum_k e^{-r_k/b}$, \begin{align*} 1 &= \sum*k e^{-r_k'/b} \\ &= \sum*{k \neq i} e^{-(r*k + x)/b} + e^{-(r_i-y(x))/b} \\ &= e^{-x/b} \sum*{k \neq i} e^{-r_k/b} + e^{y(x)/b} e^{-r_i/b}. \end{align*}
Rearranging these terms gives
$$
e^{y(x)/b} = e^{r_i/b} (1 - e^{-x/b}(1 - e^{-r_i/b})),
$$
and, thus, \begin{align*} y(x) &= b \ln(e^{r_i/b} (1 - e^{-x/b}(1 - e^{-r_i/b}))) \\ &= b \ln (1 - e^{-x/b}(1 - e^{-r_i/b})) + r_i \\ &= b \ln (e^{x/b} - 1 + e^{-r_i/b}) + r_i - x. \end{align*}
Let $\bar x = x/b$, $\bar r = r_i/b$. Let $L = 20$. Python calculates $e^L = 485165195.4097903$ and $e^-L = 2.061153622438558 \cdot 10^{-9} \approx 10^{-10}$. Using fixed point numbers with 20 decimals, these numbers can both be well represented. It is therefore safe to calculate $y(x)$ if $\bar x \leq L$ and $\bar r \leq L$ directly.
This boils down to the following restrictions:
1. The price of outcome $i$ must satisfy $p_i(b, r) = e^{-r_i/b} \geq e^{-L} \approx 10^{-10}$.
2. The amount spent $x$ must satisfy $x \leq bL$.
Let's start with the second restriction. We will show that this restriction is completely irrelevant. Suppose a user executes a trade of $y(x)$ units of outcome $i$ for $x = bL$ units of collateral. Let $q = 1 - e^{-r_i/b} \in (0, 1)$. Then
$$
\ln(e^L) - \ln(e^L - q) = \ln\left(\frac{e^L}{e^L - q}\right) \leq \ln\left(\frac{e^L}{e^L - 1}\right) \approx 2.0611536900435727 \cdot 10^{-9} \leq 10^{-9}.
$$
Let $\varepsilon = 10^{-9}$. Then we have \begin{align*} y(x) &= b\ln(e^L - 1 + e^{-r_i/b}) + r_i - x \\ &\geq b(\ln(e^L) - \varepsilon) + r_i - x \\ &= bL - b\varepsilon + r_i - x \\ &= r_i - b\varepsilon. \end{align*}
Thus, if a user executes this trade, they receive almost all units of outcome $i$ from the pool. _Less than $b\varepsilon$ remain in the pool._
Now let's get a feel for how large $b$ is. When a new pool with $n$ outcomes is created with even odds, then $b = F / \ln(n)$ where $F$ is the amount of collateral posted by the creator. Therefore, executing a trade with $x = bL$ on a pool like this will leave less than $\varepsilon F / \ln(n)$ units of collateral in the pool. Note that
$$
\frac{\varepsilon F}{\ln(n)} \leq 2\varepsilon F.
$$
Thus, unless a user adds $\varepsilon^{-1} = 10^9$ (one billion) units of collateral, there's less than one unit of $i$ left in the pool. If the user desperately wants that last unit, they will have to execute another trade (they can't get _all_ of it).
Therefore, it's fair to say that this isn't much of a restriction.
Let's now look at the price limit $p_i(b, r) \geq 10^{-10}$. On binary markets, swap fees or a limit of the buying/selling price will prevent this from occurring (which is why we are making this restriction in AMM 2.0-alpha). But if $n = 3$ and traders buy outcomes $1$ and $2$ in even amounts, then the price $p_3(b, r)$ converges to zero.
Therefore, it's important to realize how serious this restriction is unless other ways of calculating $y(x)$ are developed: If $n \geq 3$, then it is possible to move the pool into a state where buying a very cheap outcome is no longer possible, although it could be argued that unless the outcome is already trivialized with absolute certainty, it will not reach a price of 10^{-10}. Case in point: TODO Screenshot of Aliens market (Screenshot taken August 10)
Nevertheless, not allowing traders to buy a very cheap outcome is a griefing vector. This can be mitigated as follows. Assume that $\bar r > L$. Then $e^{-r_i/b} \approx \varepsilon$ is significantly smaller than $1$. We want to use this fact to approximate the value of $f(x, b) = \ln(e^{x/b} - 1 + e^{-r_i/b})$ up to an absolute error of $\delta > 0$. We'll discuss which $\delta$ to pick later.
The rough idea is that $e^{-r_i/b}$ is so small that it just doesn't matter anyways:
$$
f(x, b) = \ln(e^{x/b} - 1 + e^{-r_i/b}) \approx \tilde f(x, b) = \ln(e^{x/b} - 1).
$$
This if, of course, nonsense unless $e^{x/b}$ is sufficiently large. If $x$ is very small, then $e^x - 1$ can be arbitrarily small, even smaller than $e^{-r_i/b}$, and therefore the latter might very well matter. So we have to define a number $\varepsilon > 0$ (as a function of $\delta$) and require that $x > \varepsilon$ to ensure that $e^{x/b}$ is sufficiently large compared to $e^{-r_i/b}$.
How large? We want the approximation to have an absolute error of less than $\delta$. Let $\lambda = \frac{1}{e^{\delta} - 1}$. It's easy to verify that for _all_ numbers $z > 0$, we have
$$
\ln((\lambda + 1)z) - \ln(\lambda z) = \delta.
$$
Let $p = \lambda e^{-r_i/b}$. Now suppose that $e^{x/b} - 1 > \lambda e^{-r_i/b} = \lambda p$. It follows that \begin{align*} |f(x, b) - \tilde f(x, b)| &= |\ln(e^{x/b} - 1 + e^{-r_i/b}) - \ln(e^{x/b} - 1) \\ &= \ln(e^{x/b} - 1 + e^{-r_i/b}) - \ln(e^{x/b} - 1) \\ &< \ln(\lambda p + e^{-r_i/b}) - \ln(\lambda p) \\ &= \ln((\lambda + 1)p) - \ln(\lambda p) \\ &= \delta. \end{align*}
What this means is that if $e^{x/b} - 1$ is at least $\lambda = \frac{1}{e^\delta - 1}$ times $e^{-r_i/b}$, then
$$
|f(x, b) - \tilde f(x, b)| < \delta,
$$
i.e. the expression $\tilde f(x, b) = \ln(e^{x/b} - 1)$ approximates $f(x, b) = \ln(e^{x/b} - 1 + e^{-r_i/b})$ up to an absolute error of $\delta$.
The original assumption was that $p_i(b, r) = e^{-r_i/b} \geq 10^{-10)$. If we want the error to be less than $\delta = 10^{-k}$, then using the exponential function's Maclaurin series, we can approximate $e^\delta \approximate 1 + \delta$ and get $\lambda = \delta^{-1} = 10^k$ and thus get the requirement $e^{x/b} \geq \lambda p_i(b, r) \geq 10^{k - 10}$. For $k = 5$, this results in $e^{x/b} - 1 \geq 10^{-5}$, or, equivalently, $x \geq b \ln(1 + 10^{-5}) \approx x \geq b \cdot 10^{-5}$.
We already have an idea that $b = F / \ln(n)$ is about the same magnitude as the amount of collateral in an even-odds pool. Assuming $n \leq 128$ and using $\ln(128) \approx 5 \leq 10$, we get
$$
x \geq b \cdot 10^{-5} = 10^{-5} F / \ln(n) \geq 10^{-4} F.
$$
This means that if (and that's a big if) any outcome ever goes to a price of about $10^{-10}$ in a pool with a million units of collateral, users will still be able to buy it, but they will have to spend at least 100 units of collateral.