# Dev Pot Math: Reward Accumulator Precision and Quantity Scale Fixes
## references
* this document PR: https://github.com/permaweb/HyperBEAM/pull/900
Existing mint-curve math note:
https://hackmd.io/@BlMUCyCuTQaaEkEmZl5i0g/dev-pot-math
That note covers `minted_between/6`.
This note covers reward distribution after `ToMint` is computed:
```text
ToMint -> global accumulator -> resource accumulator -> user balance
```
No mint-curve formula change is proposed here.
## TLDR
* The mint curve is unchanged.
* Reward distribution now uses a scaled accumulator so `TotalWeightedUnits >> ToMint` does not stall rewards at zero.
* `deposit` and `withdraw` normalize source-token raw units with the resource's registered `quantity-scale`.
* `delegate` and `undelegate` move balances already inside the pot, so their `quantity` is always normalized pot units. For example, delegating `0.1` of a normalized asset (at default `POT_QUANTITY_SCALE`) means `10^17` pot units, not source-token raw units.
## constants and magic numbers
### `dev_pot_math.erl`
These are the PR's reward-distribution scale constants
* `REWARD_SCALE = 10^18`
* Fixed-point reward precision.
* Chosen to match the canonical 18-decimal pot quantity scale (`POT_QUANTITY_SCALE`).
* `PRICE_SCALE = 10^6`
* Fixed-point resource weight precision.
* Resource weights represent USD-like prices with six decimal places.
* `$2300` becomes `2300 * 10^6 = 2,300,000,000`.
* `$1` becomes `1 * 10^6 = 1,000,000`.
* `$0.0015` becomes `0.0015 * 10^6 = 1500`.
* `ACCUMULATOR_SCALE = REWARD_SCALE * PRICE_SCALE = 10^24`
* The global accumulator stores rewards in fixed-point units.
* The extra `10^6` keeps accumulator precision aligned with price-scaled weights.
* User materialization divides by `10^24` to return to raw AO units.
### `dev_pot.erl`
These are the PR's pot quantity/API constants.
* `POT_QUANTITY_SCALE = 10^18`
* Canonical internal pot quantity unit.
* Deposits from source assets are normalized into this unit.
* `1 normalized asset = 10^18` pot quantity units.
* `0.1 normalized asset = 10^17` pot quantity units.
* `quantity-scale`
* Resource config field set through `register`.
* It is not a global protocol constant; it is the source asset's raw-unit scale for that resource.
* 18-decimal assets register `quantity-scale = 10^18`.
* 8-decimal assets register `quantity-scale = 10^8`.
* If absent from resource config, `quantity` is interpreted as already pot-normalized (`POT_QUANTITY_SCALE`).
* Inline request `quantity-scale` on `deposit` or `withdraw` is rejected.
* It is not applied to `delegate` or `undelegate`.
`quantity-scale` has the same resource reward config authority level as `weight`.
The configured `weight-authority`, the pot `mint-authority`, or the configured
parent can update it. A scale-only update changes future raw-quantity conversion.
It does not rewrite existing normalized deposits and does not change
`TotalWeightedUnits`.
### `dev_pot_test_vectors.erl`
These constants are test/documentation fixtures. They are not new production
policy constants.
* `AO_TOKEN_DENOMINATION = 10^12`
* AO external balances are integer raw AO units.
* `1 AO = 1,000,000,000,000` raw units.
* Anything below 1 raw AO cannot be represented in a user balance.
* `AO_TOTAL_SUPPLY = 21,000,000 * 10^12`
* Raw AO cap used by the existing mint curve.
* The accumulator changes do not change this cap.
* `AO_MS_STEP_NUMERATOR = 5492`
* `AO_MINT_PROP_DENOMINATOR = 10^15`
* Existing millisecond mint-curve parameters.
* They are used by `minted_between/6`.
* They are not changed by the reward accumulator work.
* `AO_ONE_DAY_MS = 86,400,000`
* One day in milliseconds.
* Used only for deterministic examples/tests of the millisecond mint curve.
* `10,000,000 users`
* Used for aggregate math checks.
* The EUnit vector does not allocate 10M records; the scale benchmark script is for real state/load checks.
* `1,000,000` high-price asset units at `$2300`
* TVL contribution: `$2.3B`.
* Weight: `2300 * 10^6`.
* `5,000,000,000` stable asset A units at `$1`
* TVL contribution: `$5B`.
* Weight: `1 * 10^6`.
* `2,000,000,000` stable asset B units at `$1`
* TVL contribution: `$2B`.
* Weight: `1 * 10^6`.
* Combined example TVL: `$9.3B`
* `$2.3B + $5B + $2B = $9.3B`.
* With normalized quantities and price-scaled weights:
`TotalWeightedUnits = 9,300,000,000 * 10^18 * 10^6 = 9.3 * 10^33`.
* `150,000,000,000 DOGE` and `$0.0015`
* Demonstrates a large-supply, 8-decimal, sub-dollar asset.
* Raw DOGE quantity: `150B * 10^8`.
* Normalized pot quantity: `150B * 10^18`.
* Price-scaled weight: `1500`.
## summary
The previous PoT reward distribution used integer accumulator math:
$$
\text{GlobalAccDelta} =
\left\lfloor
\frac{\text{ToMint} + \text{OldUndistributedMint}}
{\text{TotalWeightedUnits}}
\right\rfloor
$$
If deposits are raw 18-decimal token units and weights are price-like integers, then:
$$
\text{TotalWeightedUnits} > \text{ToMint} + \text{OldUndistributedMint}
$$
When that happens:
$$
\text{GlobalAccDelta} = 0
$$
Then:
* global accumulator does not advance
* resource accumulators do not advance
* user `unclaimed_yield = 0`
* user balances do not reflect the minted AO
* minted AO remains in `undistributed-mint`
The AO mint curve still mints. The failure is that minted AO cannot be distributed through the accumulator.
---
## 1. previous integer reward path
### global drip
`drip_global` computes:
* `ToMint`
Then distributes:
$$
\text{RewardInput} =
\text{ToMint} + \text{OldUndistributedMint}
$$
Previous accumulator update:
$$
\text{GlobalAccDelta} =
\left\lfloor
\frac{\text{RewardInput}}{\text{TotalWeightedUnits}}
\right\rfloor
$$
$$
\text{NewGlobalAcc} =
\text{OldGlobalAcc} + \text{GlobalAccDelta}
$$
$$
\text{NewUndistributedMint} =
\text{RewardInput} \bmod \text{TotalWeightedUnits}
$$
---
### resource drip
$$
\text{ResourceAccDelta} =
(\text{GlobalAcc} - \text{LastGlobalAcc}) \cdot \text{ResourceWeight}
$$
$$
\text{NewResourceAcc} =
\text{OldResourceAcc} + \text{ResourceAccDelta}
$$
---
### user yield
$$
\text{UserYield} =
(\text{ResourceAcc} - \text{LastResourceAcc}) \cdot \text{UserQuantity}
$$
---
### full path
$$
\text{RewardInput}
\rightarrow
\left\lfloor \frac{\text{RewardInput}}{\text{TotalWeightedUnits}} \right\rfloor
\rightarrow \text{GlobalAccDelta}
\rightarrow \text{ResourceAccDelta}
\rightarrow \text{UserYield}
$$
If:
$$
\text{GlobalAccDelta} = 0
$$
then every downstream value is 0.
---
## 2. why the precision failure happens
Deposits are integer-only.
For current bridge/oracle deposits:
* $1\ \text{stETH} = 10^{18}$
* $1\ \text{DAI} = 10^{18}$
* $1\ \text{USDS} = 10^{18}$
All current assets use:
$$
\text{TokenScale} = 10^{18}
$$
Weighted units:
$$
\text{WeightedUnits} =
\text{Quantity} \cdot \text{Weight}
$$
Example:
$$
\text{Quantity} = 10^{16} \quad (0.01\ \text{stETH})
$$
$$
\text{Weight} = 2300
$$
$$
\text{WeightedUnits} =
10^{16} \cdot 2300
= 23{,}000{,}000{,}000{,}000{,}000{,}000
$$
AO mint cap:
$$
21{,}000{,}000 \cdot 10^{12}
= 21{,}000{,}000{,}000{,}000{,}000{,}000
$$
Therefore:
$$
\text{WeightedUnits} > \text{MintCapRaw}
$$
---
## 3. production weight alignment
The example above uses `Weight = 2300`, which is the unscaled price. Production resource weights are price-scaled:
$$
\text{PriceScale} = 10^6
$$
$$
\text{ResourceWeight} =
\text{Price} \cdot \text{PriceScale}
$$
For stETH at $2300:
$$
\text{ResourceWeight}
= 2300 \cdot 10^6
= 2{,}300{,}000{,}000
$$
For the same 0.01 stETH deposit:
$$
\text{WeightedUnits}
= 10^{16} \cdot 2{,}300{,}000{,}000
= 23{,}000{,}000{,}000{,}000{,}000{,}000{,}000{,}000
$$
That is about 1.095M times larger than the entire raw AO mint cap:
$$
\frac{
23{,}000{,}000{,}000{,}000{,}000{,}000{,}000{,}000
}{
21{,}000{,}000{,}000{,}000{,}000{,}000
}
\approx
1{,}095{,}238
$$
So the previous integer accumulator path was not only fragile under raw
18-decimal deposits. It was already misaligned with production-scaled weights.
Using an unscaled `Weight = 2300` understates the production TWU by `10^6`.
---
## 4. exact failure condition
$$
\text{GlobalAccDelta} =
\left\lfloor
\frac{\text{RewardInput}}{\text{TotalWeightedUnits}}
\right\rfloor
$$
For movement:
$$
\text{GlobalAccDelta} \ge 1
\iff
\text{RewardInput} \ge \text{TotalWeightedUnits}
$$
If:
$$
\text{RewardInput} < \text{TotalWeightedUnits}
$$
then:
$$
\text{GlobalAccDelta} = 0
$$
---
## 5. permanent failure condition
$$
\text{RemainingMintableAO} =
\text{MintCap} - \text{AlreadyMinted}
$$
If:
$$
\text{TotalWeightedUnits} >
\text{RemainingMintableAO} + \text{OldUndistributedMint}
$$
then the accumulator can never advance.
This assumes `TotalWeightedUnits` does not decrease and no additional
`undistributed-mint` is introduced externally. More precisely, the accumulator
cannot advance from future minting alone under those assumptions.
---
## 6. consequence
If:
$$
\text{GlobalAccDelta} = 0
$$
Then:
* global accumulator unchanged
* resource accumulator unchanged
* user yield = 0
With the previous integer path, minted AO becomes:
* `undistributed-mint`
---
## 7. reward accumulator precision
Define:
$$
\text{AccumulatorScale}
= \text{RewardScale} \cdot \text{PriceScale}
= 10^{18} \cdot 10^6
= 10^{24}
$$
The global accumulator update becomes:
$$
\text{Numerator} =
\text{RewardInput} \cdot \text{AccumulatorScale} + \text{OldAccumulatorRemainder}
$$
$$
\text{ScaledGlobalAccDelta} =
\left\lfloor
\frac{\text{Numerator}}
{\text{TotalWeightedUnits}}
\right\rfloor
$$
$$
\text{NewGlobalAcc} =
\text{OldGlobalAcc} + \text{ScaledGlobalAccDelta}
$$
$$
\text{NewAccumulatorRemainder} =
\text{Numerator} \bmod \text{TotalWeightedUnits}
$$
When `TotalWeightedUnits > 0`, the raw AO reward input is inserted into the scaled accumulator. The fixed-point division remainder is carried by `accumulator-remainder`.
`undistributed-mint` is still raw AO. It is only raw AO that was not inserted into the accumulator, for example when `TotalWeightedUnits = 0`.
After this change, accumulator movement requires:
$$
\text{RewardInput} \cdot \text{AccumulatorScale} + \text{OldAccumulatorRemainder}
\ge
\text{TotalWeightedUnits}
$$
If:
$$
\text{RewardInput} \cdot \text{AccumulatorScale} + \text{OldAccumulatorRemainder}
<
\text{TotalWeightedUnits}
$$
then `ScaledGlobalAccDelta` is still 0. With `AccumulatorScale = 10^{24}`, the
safe range increases by 24 decimal digits, but it remains finite.
User:
$$
\text{UserYield} =
\left\lfloor
\frac{
\text{ScaledGlobalAccDelta}
\cdot \text{ResourceWeight}
\cdot \text{UserQuantity}
}
{\text{AccumulatorScale}}
\right\rfloor
$$
---
## 8. how accumulator precision preserves policy
$$
\text{UserReward} =
\frac{
\text{RewardInput}
\cdot \text{UserQuantity}
\cdot \text{ResourceWeight}
}{
\text{TotalWeightedUnits}
}
$$
Fixed-point preserves the reward-share policy, but the implementation still floors at accumulator precision. Global fixed-point division dust is carried in `accumulator-remainder`. User-level sub-raw-AO fractions are truncated unless per-user remainders are stored.
That user-level truncation happens only when yield is materialized. A read-only
balance query can show accrued yield without resetting the user's resource
checkpoint. A materializing action, such as minting a subject or modifying that
user's deposit, updates the checkpoint and stores only integer raw AO units.
The bound is:
$$
\text{LossPerMaterialization}
<
1\ \text{raw AO unit per user per resource}
$$
Since:
$$
1\ \text{raw AO unit} = 10^{-12}\ \text{AO}
$$
the loss is negligible for ordinary users. The edge case is a tiny position that
materializes very often before its sub-raw-AO yield crosses one raw unit.
Preserving that dust exactly would require per-user, per-resource remainder
storage.
---
## 9. quantity scale normalization
Define:
$$
\text{PotQuantityScale} = 10^{18}
$$
$$
\text{NormalizedQuantity} =
\left\lfloor
\frac{\text{RawQuantity} \cdot \text{PotQuantityScale}}
{\text{QuantityScale}}
\right\rfloor
$$
$$
\text{WeightedUnits} =
\text{NormalizedQuantity} \cdot \text{ResourceWeight}
$$
The same `NormalizedQuantity` must be used in both `TotalWeightedUnits` and user yield calculation. Otherwise the denominator and numerator use different units.
Request-path behavior:
* `quantity-scale` is configured on the resource with `register`
* `deposit` and `withdraw` read `quantity-scale` from resource config
* if the resource has no `quantity-scale`, `quantity` is already pot-normalized
* inline request `quantity-scale` is rejected
* `delegate` and `undelegate` remain in already-normalized pot quantity
Delegation and undelegation do not re-normalize source-token raw units. They
operate after deposit normalization, over the existing pot balance.
Changing `quantity-scale` later does not rescale existing deposits. Existing
deposits are already normalized pot units. The new scale only affects later
`deposit` and `withdraw` request quantities for that resource.
With normalized quantity:
$$
\text{UserYield} =
\left\lfloor
\frac{
\text{ScaledGlobalAccDelta}
\cdot \text{ResourceWeight}
\cdot \text{NormalizedQuantity}
}
{\text{AccumulatorScale}}
\right\rfloor
$$
---
## 10. examples
### stETH
$$
10^{18} \cdot 10^{18} / 10^{18} = 10^{18}
$$
### BTC
$$
10^{8} \cdot 10^{18} / 10^{8} = 10^{18}
$$
### DOGE
DOGE has 8 decimals, so the same quantity normalization applies:
$$
10^{8} \cdot 10^{18} / 10^{8} = 10^{18}
$$
For a sub-$1 price with `PriceScale = 10^6`, for example $0.0015:
$$
\text{ResourceWeight}
= 0.0015 \cdot 10^6
= 1500
$$
For 150B DOGE:
$$
\text{RawQuantity}
= 150{,}000{,}000{,}000 \cdot 10^8
$$
$$
\text{NormalizedQuantity}
= 150{,}000{,}000{,}000 \cdot 10^{18}
$$
$$
\text{WeightedUnits}
= 150{,}000{,}000{,}000 \cdot 10^{18} \cdot 1500
$$
---
## final outcome
Reward accumulator precision:
* prevents zero-distribution from precision loss
Quantity scale normalization:
* normalizes token decimal differences
No change to mint curve.
Policy remains:
$$
\text{UserReward} \propto
\text{NormalizedQuantity} \cdot \text{ResourceWeight}
$$
For current 18-decimal assets, `NormalizedQuantity = RawQuantity`, so current 18-decimal behavior is preserved.
Only precision changes: fractional rewards are preserved instead of discarded.