Skip to content

feat: pack ProperQF PackedProject into 1 slot (uint128 + uint128)#400

Draft
0xferit wants to merge 2 commits intodevelopfrom
feature/pack-properqf-project-storage
Draft

feat: pack ProperQF PackedProject into 1 slot (uint128 + uint128)#400
0xferit wants to merge 2 commits intodevelopfrom
feature/pack-properqf-project-storage

Conversation

@0xferit
Copy link
Contributor

@0xferit 0xferit commented Mar 3, 2026

Reduce per-project ProperQF storage from 2 slots to 1 using uint-quantization-lib v7.0.1.

PackedProject { uint128 sumContributions, uint128 sumSquareRoots } = 256 bits = 1 slot.

sumContributions is lossy-quantized (shift=32, step=2^32 wei, effective range 2^160). Contributions operate in the 18-decimal-normalized voting-power space (see QuadraticVotingMechanism._normalizeToDecimals), so the step size corresponds to ~4.3 nanotoken regardless of the underlying asset's native decimals. sumSquareRoots is stored losslessly; its uint128 width matches the arithmetic ceiling imposed by on-chain squaring (sumSR * sumSR overflows uint256 at 2^128). Global sums stay exact via delta update cancellation.

Weight alignment (weight % MIN_VOTE_WEIGHT == 0) is enforced on-chain so contribution = weight^2 is always step-aligned, making per-project sums lossless. The 18-decimal input assumption is enforced via ProperQF(uint8 inputDecimals) constructor parameter, preventing miscalibration by future inheritors.

@github-actions
Copy link

github-actions bot commented Mar 3, 2026

Code Coverage Report for src/ files

File % Lines % Statements % Branches % Funcs
src/core/BaseStrategy.sol ✅ 100.00% (54/54) ✅ 100.00% (39/39) ✅ 100.00% (2/2) ✅ 100.00% (19/19)
src/core/MultistrategyLockedVault.sol ✅ 100.00% (117/117) ✅ 100.00% (121/121) ✅ 100.00% (22/22) ✅ 100.00% (18/18)
src/core/MultistrategyVault.sol ✅ 100.00% (593/593) ✅ 100.00% (617/617) ✅ 98.49% (196/199) ✅ 100.00% (88/88)
src/core/PaymentSplitter.sol ✅ 100.00% (55/55) ✅ 100.00% (52/52) ✅ 100.00% (18/18) ✅ 100.00% (16/16)
src/core/Privileged.sol ✅ 100.00% (13/13) ✅ 100.00% (12/12) ✅ 100.00% (0/0) ✅ 100.00% (4/4)
src/core/TokenizedStrategy.sol ✅ 99.35% (308/310) ✅ 99.64% (275/276) ✅ 95.60% (87/91) ✅ 98.81% (83/84)
src/core/libs/DebtManagementLib.sol ✅ 100.00% (76/76) ✅ 100.00% (80/80) ✅ 100.00% (20/20) ✅ 100.00% (2/2)
src/core/libs/ERC20SafeApproveLib.sol ✅ 100.00% (4/4) ✅ 100.00% (5/5) ✅ 100.00% (1/1) ✅ 100.00% (1/1)
src/factories/AaveV3StrategyFactory.sol ✅ 100.00% (13/13) ✅ 100.00% (19/19) ✅ 100.00% (2/2) ✅ 100.00% (2/2)
src/factories/AddressSetFactory.sol ✅ 100.00% (15/15) ✅ 100.00% (15/15) ✅ 100.00% (0/0) ✅ 100.00% (4/4)
src/factories/BaseERC4626StrategyFactory.sol ✅ 100.00% (15/15) ✅ 100.00% (19/19) ✅ 100.00% (0/0) ✅ 100.00% (3/3)
src/factories/BaseStrategyFactory.sol ✅ 100.00% (14/14) ✅ 100.00% (14/14) ✅ 100.00% (1/1) ✅ 100.00% (4/4)
src/factories/ERC4626StrategyFactory.sol ✅ 100.00% (2/2) ✅ 100.00% (1/1) ✅ 100.00% (0/0) ✅ 100.00% (1/1)
src/factories/LidoStrategyFactory.sol ✅ 100.00% (12/12) ✅ 100.00% (17/17) ✅ 100.00% (2/2) ✅ 100.00% (2/2)
src/factories/MorphoCompounderStrategyFactory.sol ✅ 100.00% (13/13) ✅ 100.00% (19/19) ✅ 100.00% (2/2) ✅ 100.00% (2/2)
src/factories/MultistrategyVaultFactory.sol ✅ 100.00% (74/74) ✅ 100.00% (64/64) ✅ 100.00% (31/31) ✅ 100.00% (18/18)
src/factories/PaymentSplitterFactory.sol ✅ 100.00% (58/58) ✅ 100.00% (69/69) ✅ 100.00% (22/22) ✅ 100.00% (10/10)
src/factories/RegenEarningPowerCalculatorFactory.sol ✅ 100.00% (14/14) ✅ 100.00% (14/14) ✅ 100.00% (0/0) ✅ 100.00% (4/4)
src/factories/RegenStakerFactory.sol ✅ 100.00% (32/32) ✅ 100.00% (29/29) ✅ 100.00% (2/2) ✅ 100.00% (10/10)
src/factories/SkyCompounderStrategyFactory.sol ✅ 100.00% (12/12) ✅ 100.00% (17/17) ✅ 100.00% (2/2) ✅ 100.00% (2/2)
src/factories/SparkStrategyFactory.sol ✅ 100.00% (2/2) ✅ 100.00% (1/1) ✅ 100.00% (0/0) ✅ 100.00% (1/1)
src/factories/yieldDonating/YearnV3StrategyFactory.sol ✅ 100.00% (12/12) ✅ 100.00% (16/16) ✅ 100.00% (0/0) ✅ 100.00% (2/2)
src/factories/yieldSkimming/RocketPoolStrategyFactory.sol ✅ 100.00% (12/12) ✅ 100.00% (17/17) ✅ 100.00% (2/2) ✅ 100.00% (2/2)
src/guards/KeeperBotGuard.sol ✅ 100.00% (32/32) ✅ 100.00% (30/30) ✅ 100.00% (8/8) ✅ 100.00% (6/6)
src/mechanisms/AllocationMechanismFactory.sol ✅ 100.00% (50/50) ✅ 100.00% (53/53) ✅ 100.00% (4/4) ✅ 100.00% (10/10)
src/mechanisms/BaseAllocationMechanism.sol ✅ 97.01% (65/67) ✅ 97.18% (69/71) ✅ 100.00% (7/7) ✅ 95.83% (23/24)
src/mechanisms/TokenizedAllocationMechanism.sol ✅ 99.76% (422/423) ✅ 99.57% (466/468) ✅ 95.38% (124/130) ✅ 100.00% (81/81)
src/mechanisms/mechanism/OctantQFMechanism.sol ✅ 100.00% (37/37) ✅ 100.00% (37/37) ✅ 100.00% (14/14) ✅ 100.00% (7/7)
src/mechanisms/mechanism/QuadraticVotingMechanism.sol ✅ 100.00% (71/71) ✅ 98.91% (91/92) 🔴 94.74% (18/19) ✅ 100.00% (17/17)
src/mechanisms/voting-strategy/ProperQF.sol ✅ 100.00% (90/90) ✅ 99.07% (106/107) 🔴 92.86% (13/14) ✅ 100.00% (17/17)
src/regen/RegenEarningPowerCalculator.sol ✅ 100.00% (35/35) ✅ 100.00% (31/31) ✅ 100.00% (6/6) ✅ 100.00% (8/8)
src/regen/RegenStaker.sol ✅ 100.00% (16/16) ✅ 100.00% (14/14) ✅ 100.00% (1/1) ✅ 100.00% (5/5)
src/regen/RegenStakerBase.sol ✅ 99.59% (241/242) ✅ 99.59% (242/243) ✅ 97.33% (73/75) ✅ 100.00% (34/34)
src/regen/RegenStakerWithoutDelegateSurrogateVotes.sol ✅ 100.00% (20/20) ✅ 100.00% (17/17) ✅ 100.00% (4/4) ✅ 100.00% (5/5)
src/strategies/periphery/BaseHealthCheck.sol ✅ 100.00% (32/32) ✅ 100.00% (24/24) ✅ 100.00% (14/14) ✅ 100.00% (9/9)
src/strategies/periphery/BaseYieldSkimmingHealthCheck.sol ✅ 100.00% (41/41) ✅ 100.00% (39/39) ✅ 100.00% (18/18) ✅ 100.00% (10/10)
src/strategies/periphery/UniswapV3Swapper.sol ✅ 100.00% (23/23) ✅ 100.00% (30/30) ✅ 100.00% (7/7) ✅ 100.00% (4/4)
src/strategies/yieldDonating/AaveV3Strategy.sol ✅ 100.00% (39/39) ✅ 100.00% (48/48) ✅ 100.00% (9/9) ✅ 100.00% (7/7)
src/strategies/yieldDonating/ERC4626Strategy.sol ✅ 100.00% (26/26) ✅ 100.00% (30/30) ✅ 100.00% (2/2) ✅ 100.00% (7/7)
src/strategies/yieldDonating/MorphoCompounderStrategy.sol ✅ 100.00% (26/26) ✅ 100.00% (30/30) ✅ 100.00% (2/2) ✅ 100.00% (7/7)
src/strategies/yieldDonating/PrivilegedYieldDonatingTokenizedStrategy.sol ✅ 100.00% (16/16) ✅ 100.00% (14/14) ✅ 100.00% (4/4) ✅ 100.00% (6/6)
src/strategies/yieldDonating/SkyCompounderStrategy.sol ✅ 100.00% (88/88) ✅ 100.00% (77/77) ✅ 100.00% (24/24) ✅ 100.00% (19/19)
src/strategies/yieldDonating/SparkStrategy.sol ✅ 100.00% (8/8) ✅ 100.00% (9/9) ✅ 100.00% (6/6) ✅ 100.00% (1/1)
src/strategies/yieldDonating/YearnV3Strategy.sol ✅ 100.00% (26/26) ✅ 100.00% (30/30) ✅ 100.00% (2/2) ✅ 100.00% (7/7)
src/strategies/yieldDonating/YieldDonatingTokenizedStrategy.sol ✅ 100.00% (23/23) ✅ 100.00% (26/26) ✅ 100.00% (5/5) ✅ 100.00% (2/2)
src/strategies/yieldSkimming/BaseYieldSkimmingStrategy.sol ✅ 100.00% (8/8) ✅ 100.00% (5/5) ✅ 100.00% (0/0) ✅ 100.00% (5/5)
src/strategies/yieldSkimming/LidoStrategy.sol ✅ 100.00% (4/4) ✅ 100.00% (3/3) ✅ 100.00% (0/0) ✅ 100.00% (2/2)
src/strategies/yieldSkimming/PrivilegedYieldSkimmingTokenizedStrategy.sol ✅ 100.00% (16/16) ✅ 100.00% (14/14) ✅ 100.00% (4/4) ✅ 100.00% (6/6)
src/strategies/yieldSkimming/RocketPoolStrategy.sol ✅ 100.00% (4/4) ✅ 100.00% (3/3) ✅ 100.00% (0/0) ✅ 100.00% (2/2)
src/strategies/yieldSkimming/YieldSkimmingTokenizedStrategy.sol ✅ 99.61% (254/255) ✅ 99.39% (326/328) ✅ 96.43% (81/84) ✅ 100.00% (28/28)
src/utils/AddressSet.sol ✅ 100.00% (34/34) ✅ 100.00% (34/34) ✅ 100.00% (12/12) ✅ 100.00% (7/7)
src/zodiac-core/LinearAllowanceExecutor.sol ✅ 100.00% (24/24) ✅ 100.00% (24/24) ✅ 100.00% (7/7) ✅ 100.00% (7/7)
src/zodiac-core/modules/LinearAllowanceSingletonForGnosisSafe.sol ✅ 100.00% (91/91) ✅ 100.00% (100/100) ✅ 95.24% (20/21) ✅ 100.00% (16/16)

@0xferit 0xferit force-pushed the feature/pack-properqf-project-storage branch 2 times, most recently from 8956279 to 17f7bf2 Compare March 3, 2026 23:39
@0xferit 0xferit changed the title feat: pack ProperQF Project struct (2 slots -> 1 slot) feat: pack ProperQF PackedProject into 1 slot (uint96 + uint160) Mar 3, 2026
@0xferit
Copy link
Contributor Author

0xferit commented Mar 3, 2026

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b69448ef1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

/// The unchecked downcasts are safe because encode() guarantees the result fits.
function _writeProject(uint256 projectId, uint256 sumContributions, uint256 sumSquareRoots) internal {
_getProperQFStorage().projects[projectId] = PackedProject({
sumContributions: uint96(CONTRIBUTIONS_SCHEME.encode(sumContributions)),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve per-project remainder when quantizing contributions

sumContributions is quantized on every write, and _processVoteUnchecked rebuilds newSumContributions from the already-decoded (floored) value, so sub-step remainders are dropped permanently each vote instead of accumulating. For example, two votes with contribution = STEP/2 still leave sumContributions == 0; this undercounts the linear term in getTally, and QuadraticVotingMechanism uses that linear term for both quorum and share minting, so proposals can be under-allocated or fail quorum whenever vote costs are not step-aligned (especially with smaller weights or lower-alpha settings).

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both this and the sibling finding (silent zeroing for small weights) are addressed by the same fix: a ContributionBelowMinimum guard in _processVoteUnchecked that rejects contribution < CONTRIBUTIONS_SCHEME.stepSize() (= 2^32).

Why remainders don't compound: _readProject always returns a step-aligned value (stored << 32), so the floor loss per vote is strictly contribution mod STEP, independent of accumulated history. Remainders from previous votes have already been truncated and don't interact with new contributions.

Bounded loss after the guard: With contribution >= STEP enforced, the per-vote loss ratio is remainder / contribution. For the minimum weight (65536), contribution = 2^32 = STEP exactly, so loss = 0. For realistic weights the loss ratio is negligible (e.g. weight=1e9: ~5e-8% per vote).

Global sums stay exact: totalLinearSum uses the delta pattern old_decoded - old_decoded + (old_decoded + contribution), which cancels the quantization error. Only per-project readback in getTally is lossy, bounded by N * (STEP - 1) across N votes.

Preserving the remainder would require either an extra storage slot per project (defeating the packing goal) or a fundamentally different accumulation strategy. Given the guard makes the worst case precision dust, I think the tradeoff is right.

@0xferit 0xferit force-pushed the feature/pack-properqf-project-storage branch from cda6624 to a98bbe8 Compare March 14, 2026 13:48
@0xferit 0xferit changed the title feat: pack ProperQF PackedProject into 1 slot (uint96 + uint160) feat: pack ProperQF PackedProject into 1 slot (uint128 + uint128) Mar 14, 2026
Compress per-project storage from 2 slots to 1 using symmetric uint128
field widths with lossy quantization for sumContributions via
uint-quantization-lib v7.0.1.

Layout: uint128 sumContributions (shift=32, lossy) + uint128 sumSquareRoots (lossless)

sumContributions: quantized via CONTRIBUTIONS_SCHEME (step=2^32, max~2^160).
sumSquareRoots: uint128 ceiling matches its arithmetic constraint (squared
on-chain, which overflows uint256 at 2^128).

Enforce weight alignment to MIN_VOTE_WEIGHT=2^16 so contribution=weight^2
is always step-aligned, making per-project sums lossless. Enforce
18-decimal input assumption via ProperQF constructor parameter to prevent
miscalibration by future inheritors.
…ation-lib v7.1.0

Replace custom ContributionBelowMinimum error and manual check in ProperQF
with requireMinStep() and BelowMinStep error from the library (issue #93).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant