From b4c04656ab19bbb6bf282a9ef50598dba9acd4e5 Mon Sep 17 00:00:00 2001 From: Nick DeLuca Date: Mon, 29 Jul 2024 09:42:17 -0700 Subject: [PATCH] docs(x/precisebank): Add spec for logic (#1969) --- x/precisebank/README.md | 646 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 642 insertions(+), 4 deletions(-) diff --git a/x/precisebank/README.md b/x/precisebank/README.md index d33fb574..4f83d9d4 100644 --- a/x/precisebank/README.md +++ b/x/precisebank/README.md @@ -4,9 +4,647 @@ This document specifies the precisebank module of Kava. -The precisebank module is responsible for extending the precision of `x/bank`, -intended to be used for the `x/evm`. It serves as a wrapper of `x/bank` to -increase the precision of KAVA from 6 to 18 decimals, while preserving the -behavior of existing `x/bank` balances. +The precisebank module is responsible for extending the precision of `x/bank`, intended to be used for the `x/evm`. It serves as a wrapper of `x/bank` to increase the precision of KAVA from 6 to 18 decimals, while preserving the behavior of existing `x/bank` balances. This module is used only by `x/evm` where 18 decimal points are expected. + +## Contents + +- [Background](#background) + - [Adding](#adding) + - [Subtracting](#subtracting) + - [Transfer](#transfer) + - [Setup](#setup) + - [Remainder does not change](#remainder-does-not-change) + - [Reserve](#reserve) + - [Burn](#burn) + - [Mint](#mint) +- [State](#state) +- [Keepers](#keepers) +- [Messages](#messages) +- [Events](#events) + - [Keeper Events](#keeper-events) + - [SendCoins](#sendcoins) + - [MintCoins](#mintcoins) + - [BurnCoins](#burncoins) +- [Client](#client) + - [gRPC](#grpc) + - [TotalFractionalBalances](#totalfractionalbalances) + - [Remainder](#remainder) + - [FractionalBalance](#fractionalbalance) + +## Background + +The standard unit of currency on the Kava Chain is `KAVA`. This is denominated by the atomic unit `ukava`, which represents $10^{-6}$ `KAVA` and there are $10^6$ `ukava` per `KAVA`. + +In order to support 18 decimals of precision while maintaining `ukava` as the cosmos-native atomic unit, we further split each `ukava` unit into $10^{12}$ `akava` units, the native currency of the Kava EVM. + +This gives a full $10^{18}$ precision on the EVM. In order to avoid confusion with atomic `ukava` units, we will refer to `akava` as "sub-atomic units". + +To review we have: + - `ukava`, the cosmos-native unit and atomic unit of the Kava chain + - `akava`, the evm-native unit and sub-atomic unit of the Kava chain + +In order to maintain consistency between the `akava` supply and the `ukava` supply, we add the constraint that each sub-atomic `akava`, may only exist as part of an atomic `ukava`. Every `akava` is fully backed by a `ukava` in the `x/bank` module. + +This is a requirement since `ukava` balances in `x/bank` are shared between the cosmos modules and the EVM. We are wrapping and extending the `x/bank` module with the `x/precisebank` module to add an extra $10^{12}$ units of precision. If $10^{12}$ `akava` is transferred in the EVM, the cosmos modules will see a 1 `ukava` transfer and vice versa. If `akava` was not fully backed by `ukava`, then balance changes would not be fully consistent across the cosmos and the EVM. + +This brings us to how account balances are extended to represent `akava` balances larger than $10^{12}$. First, we define $a(n)$, $b(n)$, and $C$ where $a(n)$ is the `akava` balance of account `n`, $b(n)$ is the `ukava` balance of account `n` stored in the `x/bank` module, and $C$ is the conversion factor equal to $10^{12}$. + +Any $a(n)$ divisible by $C$, can be represented by $C$ * $b(n)$. Any remainder not divisible by $C$, we define the "fractional balance" as $f(n)$ and store this in the `x/precisebank` store. + +Thus, + +$$a(n) = b(n) \cdot C + f(n)$$ + +where + +$$0 \le f(n) < C$$ + +$$a(n), b(n) \ge 0$$ + +This is the quotient-remainder theorem and any $a(n)$ can be represented by unique integers $b(n)$, $f(n)$ where + +$$b(n) = \lfloor a(n)/C \rfloor$$ + +$$f(n) = a(n)\bmod{C}$$ + +With this definition in mind we will refer to $b(n)$ units as integer units, and $f(n)$ as fractional units. + +Now since $f(n)$ is stored in the `x/precisebank` and not tracked by the `x/bank` keeper, these are not counted in the `ukava` supply, so if we define + +$$T_a \equiv \sum_{n \in \mathcal{A}}{a(n)}$$ + +$$T_b \equiv \sum_{n \in \mathcal{A}}{b(n)}$$ + +where $\mathcal{A}$ is the set of all accounts, $T_a$ is the total `akava` supply, and $T_b$ is the total `ukava` supply, then a reserve account $R$ is added such that + +$$a(R) = 0$$ + +$$b(R) \cdot C = \sum_{n \in \mathcal{A}}{f(n)} + r$$ + +where $R$ is the module account of the `x/precisebank`, and $r$ is the remainder or fractional amount backed by $b(R)$, but not yet in circulation such that + +$$T_a = T_b \cdot C - r$$ + +and + +$$ 0 <= r < C$$ + +We see that $0 \le T_b \cdot C - T_a < C$. If we mint, burn, or transfer `akava` such that this inequality would be invalid after updates to account balances, we adjust the $T_b$ supply by minting or burning to the reserve account which holds `ukava` equal to that of all `akava` balances less than `C` plus the remainder. + +If we didn't add these constraints, then the total supply of `ukava` reported by the bank keeper would not account for the `akava` units. We would incorrectly increase the supply of `akava` without increasing the reported total supply of KAVA. + +### Adding + +When adding we have + +$$a'(n) = a(n) + a$$ + +$$b'(n) \cdot C + f'(n) = b(n) \cdot C + f(n) + a$$ + +where $a'(n)$ is the new `akava` balance after adding `akava` amount $a$. These +must hold true for all $a$. We can determine the new $b'(n)$ and $f'(n)$ with the following formula. + +$$f'(n) = f(n) + a \mod{C}$$ + +$$b'(n) = \begin{cases} b(n) + \lfloor a/C \rfloor & f'(n) \geq f(n) \\ +b(n) + \lfloor a/C \rfloor + 1 & f'(n) < f(n) \end{cases}$$ + +We can see that $b'(n)$ is incremented by an additional 1 integer unit if +$f'(n) < f(n)$ because the new balance requires an arithmetic carry from the +fractional to the integer unit. + +### Subtracting + +When subtracting we have + +$$a'(n) = a(n) - a$$ + +$$b'(n) \cdot C + f'(n) = b(n) \cdot C + f(n) - a$$ + +and + +$$f'(n) = f(n) - a \mod{C}$$ + +$$b'(n) = \begin{cases} b(n) - \lfloor a/C \rfloor & f'(n) \leq f(n) \\ +b(n) - \lfloor a/C \rfloor - 1 & f'(n) > f(n) \end{cases}$$ + +Similar to the adding case, we subtract $b'(n)$ by an additional 1 if +$f'(n) > f(n)$ because $f(n)$ is insufficient on its own and requires an +arithmetic borrow from the integer units. + +### Transfer + +A transfer is a combination of adding and subtracting of a single amount between +two different accounts. The transfer is valid if both the subtraction for the +sender and the addition for the receiver are valid. + +#### Setup + +Let two accounts $1$ and $2$ have balances $a(1)$ and $a(2)$, and $a$ is the +amount to transfer. Assume that $a(1) \ge a$ to ensure that the transfer is +valid. We initiate a transfer by subtracting $a$ from account $1$ and adding $a$ +to account $2$, yielding + +$$a'(1) = a(1) - a$$ + +$$a'(2) = a(2) + a$$ + +The reserve account must also be updated to reflect the change in the total +supply of fractional units. + +$$b(R) \cdot C = \sum_{n \in \mathcal{A}}{f(n)} + r$$ + +$$b'(R) \cdot C = \sum_{n \in \mathcal{A}}{f'(n)} + r'$$ + +With these two formulas, we can determine the new remainder and reserve by using +the delta of the sum of fractional units and the remainder. + +$$(b'(R)-b(R)) \cdot C = \sum_{n \in \mathcal{A}}{f'(n)} - \sum_{n \in \mathcal{A}}{f(n)} + r' - r$$ + +Since only two accounts are involved in the transfer, we can use the two account +balances in place of the fractional sum delta. + +$$(b'(R)-b(R)) \cdot C = f'(1) - f(1) + f'(2) - f(2) + r' - r$$ + +#### Remainder does not change + +Take $\mod{C}$ of both sides of the equation. + +$$(b'(R)-b(R)) \cdot C \mod{C} = [f'(1) - f(1) + f'(2) - f(2) + r' - r] \mod{C}$$ + +Since $C$ is a multiple of $C$, the left side of the equation is $0$. + +$$0 = f'(1) - f(1) + f'(2) - f(2) + r' - r \mod{C}$$ + +Replace $f'(1)$ and $f'(2)$ with their definitions in terms of $f(1)$ and $f(2)$. + +$$0 = (f(1) - a)\bmod{C} - f(1) + (f(2) + a)\bmod{C} - f(2) + r' - r \mod{C}$$ + +This can be simplified to: + +$$0 = f(1) - a - f(1) + f(2) + a - f(2) + r' - r \mod{C}$$ + +Canceling out terms $a$, $f(1)$ and $f(2)$. + +$$0 = r' - r \mod{C}$$ + +By the quotient remainder theorem, we can express $r' - r$ as: + +$$q * C = r' - r$$ + +for some integer $q$. + +With our known range of $r$ and $r'$: + +$$0 \leq r' < C, 0 \leq r < C$$ + +We can see that $r' - r$ must be in the range + +$$ -C < r' - r < C$$ + +This implies that $q$ must be $0$ as there is no other integer $q$ that satisfies the inequality. + +$$ -C < q * C < C$$ + +$$q = 0$$ + +$$ r' - r = 0$$ + +Therefore, the remainder does not change during a transfer. + +#### Reserve + +The reserve account must be updated to reflect the change in the total supply of fractional units. + +The change in reserve is determined by the change in the fractional units of the two accounts. + +$$(b'(R)-b(R)) \cdot C = f'(1) - f(1) + f'(2) - f(2)$$ + +For $f'(1)$, we can represent the new fractional balance as: + +$$f'(1) = f(1) - a \mod{C}$$ + +$$f'(1)\bmod{C}= f(1)\bmod{C} - a \bmod{C} \mod{C}$$ + +$$f'(1) = f(1) - a \bmod{C} \mod{C}$$ + +This results in two cases for $f'(1)$: + +$$f'(1) = \begin{cases} f(1) - a\bmod{C} & 0 \leq f(1) - a\bmod{C} \\ +f(1) - a\bmod{C} + C & 0 > f(1) - a\bmod{C} \end{cases}$$ + +Since we can identify the following: + +$$f'(1) \leq f(1) \Longleftrightarrow f'(1) = f(1) - a\bmod{C} $$ + +$$f'(1) > f(1) \Longleftrightarrow f'(1) = f(1) - a\bmod{C} + C$$ + +We can simplify the two cases for $f'(1)$: + +$$f'(1) = \begin{cases} f(1) - a\bmod{C} & f'(1) \leq f(1) \\ +f(1) - a\bmod{C} + C & f'(1) > f(1) \end{cases}$$ + +The same for $f'(2)$: + +$$f'(2) = f(2) + a \mod{C}$$ + +$$f'(2)\bmod{C}= f(2)\bmod{C} + a \bmod{C} \mod{C}$$ + +$$f'(2) = f(2) + a \bmod{C} \mod{C}$$ + +$$f'(2) = \begin{cases} f(2) + a\bmod{C} & f'(2) \geq f(2) \\ +f(2) + a\bmod{C} - C & f'(2) < f(2) \end{cases}$$ + +Bringing the two cases for the two accounts together to determine the change in the reserve account: + +$$b'(R) - b(R) \cdot C = \begin{cases} f(1) - a\bmod{C} + C - f(1) + f(2) + a\bmod{C} - C + f(2) & f'(1) > f(1) \land f'(2) < f(2) \\ +f(1) - a\bmod{C} - f(1) + f(2) + a\bmod{C} - C + f(2) & f'(1) \leq f(1) \land f'(2) < f(2) \\ +f(1) - a\bmod{C} + C - f(1) + f(2) + a\bmod{C} + f(2) & f'(1) > f(1) \land f'(2) \geq f(2) \\ +f(1) - a\bmod{C} - f(1) + f(2) + a\bmod{C} + f(2) & f'(1) \leq f(1) \land f'(2) \geq f(2) \\ +\end{cases}$$ + +This simplifies to: + +$$b'(R) - b(R) \cdot C = \begin{cases} 0 & f'(1) > f(1) \land f'(2) < f(2) \\ +-C & f'(1) \leq f(1) \land f'(2) < f(2) \\ +C & f'(1) > f(1) \land f'(2) \geq f(2) \\ +0 & f'(1) \leq f(1) \land f'(2) \geq f(2) \\ +\end{cases}$$ + +Simplifying further by dividing by $C$: + +$$b'(R) - b(R) = \begin{cases} 0 & f'(1) > f(1) \land f'(2) < f(2) \\ +-1 & f'(1) \leq f(1) \land f'(2) < f(2) \\ +1 & f'(1) > f(1) \land f'(2) \geq f(2) \\ +0 & f'(1) \leq f(1) \land f'(2) \geq f(2) \\ +\end{cases}$$ + +Thus the reserve account is updated based on the changes in the fractional units of the two accounts. + +### Burn + +When burning, we change only 1 account. Assume we are burning an amount $a$ from account $1$. + +$$a'(1) = a(1) - a$$ + +The change in reserve is determined by the change in the fractional units of the account and the remainder. + +$$(b'(R)-b(R)) \cdot C = f'(1) - f(1) + r' - r$$ + +The new fractional balance is: + +$$f'(1) = f(1) - a \mod{C}$$ + +Apply modulo $C$ to both sides of the equation. + +$$f'(1)\bmod{C}= f(1)\bmod{C} - a \bmod{C} \mod{C}$$ + +This simplifies to: + +$$f'(1) = f(1) - a \bmod{C} \mod{C}$$ + +We can see two cases for $f'(1)$, depending on whether the new fractional balance is less than the old fractional balance. + +$$f'(1) = \begin{cases} f(1) - a\bmod{C} & f'(1) \leq f(1) \\ +f(1) - a\bmod{C} + C & f'(1) > f(1) \end{cases}$$ + +The second case occurs when we need to borrow from the integer units. + +We update the remainder by adding $a$ to $r$ as burning increases the amount no longer in circulation but still backed by the reserve. + +$$r' = r + a \mod{C}$$ + +$$r'\bmod{C}= r\bmod{C} + a \bmod{C} \mod{C}$$ + +$$r' = r + a \bmod{C} \mod{C}$$ + +We can see two cases for $r'$, depending on whether the new remainder is less than the old remainder. + +$$r' = \begin{cases} r + a\bmod{C} & r' \geq r \\ +r + a\bmod{C} - C & r' < r \end{cases}$$ + +The reserve account is updated based on the changes in the fractional units of the account and remainder. + +$$b'(R) - b(R) = \begin{cases} 0 & f'(1) > f(1) \land r' < r \\ +-1 & f'(1) \leq f(1) \land r' < r \\ +1 & f'(1) > f(1) \land r' \geq r \\ +0 & f'(1) \leq f(1) \land r' \geq r \\ +\end{cases}$$ + +### Mint + +Minting is similar to burning, but we add to the account instead of +removing it. Assume we are minting an amount $a$ to account $1$. + +$$a'(1) = a(1) + a$$ + +The change in reserve is determined by the change in the fractional units of the account and the remainder. + +$$(b'(R)-b(R)) \cdot C = f'(1) - f(1) + r' - r$$ + +The new fractional balance is: + +$$f'(1) = f(1) + a \mod{C}$$ + +Apply modulo $C$ to both sides of the equation. + +$$f'(1)\bmod{C}= f(1)\bmod{C} + a \bmod{C} \mod{C}$$ + +$$f'(1) = f(1) + a \bmod{C} \mod{C}$$ + +We can see two cases for $f'(1)$, depending on whether the new fractional balance is greater than the old fractional balance. + +$$f'(1) = \begin{cases} f(1) + a\bmod{C} & f'(1) \geq f(1) \\ +f(1) + a\bmod{C} - C & f'(1) < f(1) \end{cases}$$ + +The second case occurs when we need to carry to the integer unit. + +We update the remainder by subtracting $a$ from $r$ as minting decreases the amount no longer in circulation but still backed by the reserve. + +$$r' = r - a \mod{C}$$ + +$$r'\bmod{C}= r\bmod{C} - a \bmod{C} \mod{C}$$ + +$$r' = r - a \bmod{C} \mod{C}$$ + +$$r' = \begin{cases} r - a\bmod{C} & r' \leq r \\ +r - a\bmod{C} + C & r' > r \end{cases}$$ + +The reserve account is updated based on the changes in the fractional units of the account and the remainder. + +$$b'(R) - b(R) = \begin{cases} 0 & r' > r \land f'(1) < f(1) \\ +-1 & r' \leq r \land f'(1) < f(1) \\ +1 & r' > r \land f'(1) \geq f(1) \\ +0 & r' \leq r \land f'(1) \geq f(1) \\ +\end{cases}$$ + +## State + +The `x/precisebank` module keeps state of the following: +1. Account fractional balances. +2. Remainder amount. This amount represents the fractional amount that is backed + by the reserve account but not yet in circulation. This can be non-zero if + a fractional amount less than `1ukava` is minted. + + **Note:** Currently, mint and burns are only used to transfer fractional + amounts between accounts via `x/evm`. This means mint and burns on mainnet + state will always be equal and opposite, always resulting in a zero remainder + at the end of each transaction and block. + +The `x/precisebank` module does not keep track of the reserve as it is stored in +the `x/bank` module. + +## Keepers + +The `x/precisebank module only exposes one keeper that wraps the bank module` +keeper and implements bank keeper compatible methods to support extended coin. +This complies with the `x/evm` module interface for `BankKeeper`. + +```go +type BankKeeper interface { + authtypes.BankKeeper + SpendableCoin(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error + MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error + BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error +} +``` + +## Messages + +The `x/precisebank` module does not have any messages and is intended to be used +by other modules as a replacement of the bank module. + +## Events + +### Keeper Events + +The `x/precisebank` module emits the following events, that are meant to be +match the events emitted by the `x/bank` module. Events emitted by +`x/precisebank` will only contain `akava` amounts, as the `x/bank` module will +emit events with all other denoms. This means if an account transfers multiple +coins including `akava`, the `x/precisebank` module will emit an event with the +full `akava` amount. If `ukava` is included in a transfer, mint, or burn, the +`x/precisebank` module will emit an event with the full equivalent `akava` +amount. + +#### SendCoins + +```json +{ + "type": "transfer", + "attributes": [ + { + "key": "recipient", + "value": "{{sdk.AccAddress of the recipient}}", + "index": true + }, + { + "key": "sender", + "value": "{{sdk.AccAddress of the sender}}", + "index": true + }, + { + "key": "amount", + "value": "{{sdk.Coins being transferred}}", + "index": true + } + ] +} +``` + +```json +{ + "type": "coin_spent", + "attributes": [ + { + "key": "spender", + "value": "{{sdk.AccAddress of the address which is spending coins}}", + "index": true + }, + { + "key": "amount", + "value": "{{sdk.Coins being spent}}", + "index": true + } + ] +} +``` + +```json +{ + "type": "coin_received", + "attributes": [ + { + "key": "receiver", + "value": "{{sdk.AccAddress of the address beneficiary of the coins}}", + "index": true + }, + { + "key": "amount", + "value": "{{sdk.Coins being received}}", + "index": true + } + ] +} +``` + +#### MintCoins + +```json +{ + "type": "coinbase", + "attributes": [ + { + "key": "minter", + "value": "{{sdk.AccAddress of the module minting coins}}", + "index": true + }, + { + "key": "amount", + "value": "{{sdk.Coins being minted}}", + "index": true + } + ] +} +``` + +```json +{ + "type": "coin_received", + "attributes": [ + { + "key": "receiver", + "value": "{{sdk.AccAddress of the module minting coins}}", + "index": true + }, + { + "key": "amount", + "value": "{{sdk.Coins being received}}", + "index": true + } + ] +} +``` + +#### BurnCoins + +```json +{ + "type": "burn", + "attributes": [ + { + "key": "burner", + "value": "{{sdk.AccAddress of the module burning coins}}", + "index": true + }, + { + "key": "amount", + "value": "{{sdk.Coins being burned}}", + "index": true + } + ] +} +``` + +```json +{ + "type": "coin_spent", + "attributes": [ + { + "key": "spender", + "value": "{{sdk.AccAddress of the module burning coins}}", + "index": true + }, + { + "key": "amount", + "value": "{{sdk.Coins being burned}}", + "index": true + } + ] +} +``` + +## Client + +### gRPC + +A user can query the precisebank module using gRPC endpoints. + +#### TotalFractionalBalances + +The `TotalFractionalBalances` endpoint allows users to query the aggregate sum +of all fractional balances. This is primarily used for external verification of +the module state against the reserve balance. + +```shell +kava.precisebank.v1.Query/TotalFractionalBalances +``` + +Example: + +```shell +grpcurl -plaintext \ + localhost:9090 \ + kava.precisebank.v1.Query/TotalFractionalBalances +``` + +Example Output: + +```json +{ + "total": "2000000000000akava" +} +``` + +#### Remainder + +The `Remainder` endpoint allows users to query the current remainder amount. + +```shell +kava.precisebank.v1.Query/Remainder +``` + +Example: + +```shell +grpcurl -plaintext \ + localhost:9090 \ + kava.precisebank.v1.Query/Remainder +``` + +Example Output: + +```json +{ + "remainder": "100akava" +} +``` + +#### FractionalBalance + +The `FractionalBalance` endpoint allows users to query the fractional balance of +a specific account. + +```shell +kava.precisebank.v1.Query/FractionalBalance +``` + +Example: + +```shell +grpcurl -plaintext \ + -d '{"address": "kava1..."}' \ + localhost:9090 \ + kava.precisebank.v1.Query/FractionalBalance +``` + +Example Output: + +```json +{ + "fractional_balance": "10000akava" +} +```