# 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.