Skip to content

AUM dust accumulator#235

Draft
nullpointer0x00 wants to merge 13 commits into
mainfrom
nullpointer0x00/224-dust-accumulation
Draft

AUM dust accumulator#235
nullpointer0x00 wants to merge 13 commits into
mainfrom
nullpointer0x00/224-dust-accumulation

Conversation

@nullpointer0x00

@nullpointer0x00 nullpointer0x00 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

closes: #224

Summary by CodeRabbit

  • New Features

    • Added support for tracking and carrying forward AUM fee remainder amounts between reconciliations.
    • Vault accounts now store an explicit fee remainder value, including for coarse payment denominations.
  • Bug Fixes

    • Prevents small, repeated fee calculations from losing revenue due to truncation.
    • Fee collection and valuation now use the same accrued remainder, improving consistency.
    • Legacy vault records are normalized to a safe zero remainder during upgrade.
  • Tests

    • Expanded coverage for remainder handling, validation, migration, and long-running fee accrual behavior.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@nullpointer0x00, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f46b9d8c-0b5e-4241-9dcd-49413611f011

📥 Commits

Reviewing files that changed from the base of the PR and between 2b6483d and 110da2e.

📒 Files selected for processing (4)
  • keeper/reconcile.go
  • keeper/reconcile_test.go
  • types/vault.go
  • types/vault_test.go
📝 Walkthrough

Walkthrough

This PR adds a persistent FeeRemainder decimal field to VaultAccount to carry uncollected fractional AUM fee value across reconciliation periods. It introduces CalculateAUMFeeDec, refactors fee accrual/collection in reconcile.go, adds state validation, a v1→v2 migration to default the field, proto/generated bindings, and documentation updates.

Changes

AUM Fee Dust Accumulation

Layer / File(s) Summary
Decimal fee calculation
interest/interest.go, interest/interest_test.go
CalculateAUMFeeDec computes an un-truncated decimal AUM fee; CalculateAUMFee wraps it with truncation; tests cover decimal results and cumulative truncation loss.
FeeRemainder proto and bindings
proto/provlabs/vault/v1/vault.proto, api/provlabs/vault/v1/vault.pulsar.go
Adds fee_remainder field (30) to VaultAccount proto and regenerates Pulsar reflection, accessors, marshal/unmarshal, and descriptor bytes.
FeeRemainder validation
types/vault.go, types/vault_test.go
Validate() parses and rejects malformed or negative FeeRemainder; tests cover valid, empty, malformed, negative, and >1 unit cases.
Fee remainder carry-forward
keeper/reconcile.go, keeper/reconcile_test.go
AccrueAUMFeePayment replaces CalculateAccruedAUMFeePayment, folding carried remainder into accrual, truncating to whole units, and persisting leftover fractions; CalculateAccruedAUMFee mirrors this for valuation; tests cover dust persistence, remainder carryover, and coarse payment-denom flooring.
Legacy FeeRemainder migration
keeper/migrator.go, keeper/migrations.go, keeper/migrations_test.go, module.go
Migrate1to2 runs migrateVaultFeeRemainderDefaults to normalize empty FeeRemainder to zero on legacy accounts; tests cover vault, non-vault, and idempotency cases.
State and blocker docs
.changelog/unreleased/improvements/224-aum-fee-dust.md, spec/02_state.md, spec/06_blocker.md
Changelog and specs describe the carried fee remainder and updated collection behavior across periods.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Reconcile as PerformVaultFeeTransfer
  participant Accrual as AccrueAUMFeePayment
  participant Vault as VaultAccount
  participant Bank

  Caller->>Reconcile: trigger fee reconciliation
  Reconcile->>Accrual: AccrueAUMFeePayment(ctx, vault, totalAssets)
  Accrual->>Vault: parse FeeRemainder, compute decimal fee
  Accrual->>Accrual: add remainder, truncate to whole underlying unit
  Accrual->>Vault: write leftover fraction back to FeeRemainder
  Accrual->>Accrual: convert whole units to PaymentDenom coin
  Accrual-->>Reconcile: return collectible sdk.Coin
  Reconcile->>Bank: transfer collected fee coin
  Reconcile->>Vault: persist updated VaultAccount state
Loading

Possibly related issues

Possibly related PRs

  • ProvLabs/vault#178: Modifies the same PerformVaultFeeTransfer/accrual helpers in keeper/reconcile.go that this PR extends with FeeRemainder carryover.
  • ProvLabs/vault#182: Also modifies AUM fee calculation/reconciliation in interest.CalculateAUMFee and keeper/reconcile.go.
  • ProvLabs/vault#216: Extends the same v1→v2 migration wiring in keeper/migrator.go/module.go with an additional migration step.

Suggested labels: bug

Suggested reviewers: iramiller, Taztingo

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to the AUM dust carryover work, but it's too generic to identify the main change. Use a more specific title such as "Carry AUM fee remainders across reconciliations".
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR adds FeeRemainder state, migration, reconciliation logic, and tests that preserve and accumulate AUM fee dust as requested.
Out of Scope Changes check ✅ Passed The changes are documentation, schema, logic, migration, and tests directly supporting AUM dust accumulation, with no unrelated edits evident.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nullpointer0x00/224-dust-accumulation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an AUM fee dust accumulator for vault fee reconciliation. The main changes are:

  • A new fee_remainder field on vault accounts.
  • Decimal AUM fee accrual that carries uncollected dust forward.
  • Shared fee logic for collection and valuation paths.
  • Migration and tests for legacy vault remainder defaults.

Confidence Score: 4/5

This is close, but the coarse-denom remainder validation should be fixed before merging.

  • Imported coarse-denom vaults can still carry an arbitrarily large fee_remainder.
  • Reconciliation treats that stored value as accrued fees and can collect more than the vault actually earned.
  • The rest of the accumulator and valuation changes look aligned with the intended fee flow.

types/vault.go

Important Files Changed

Filename Overview
keeper/reconcile.go Adds decimal fee accrual, carry-forward logic, and shared collectible-fee calculation for collection and valuation.
types/vault.go Adds fee_remainder validation, but coarse-denom vaults can still import an unbounded carry.
keeper/migrations.go Normalizes empty legacy remainders to explicit zero while preserving existing values.
proto/provlabs/vault/v1/vault.proto Adds the fee_remainder field and documents carry-forward behavior.

Reviews (7): Last reviewed commit: "fix AUM fee dust valuation and bound imp..." | Re-trigger Greptile

Comment thread types/vault.go
Comment thread keeper/reconcile.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
keeper/migrations_test.go (1)

80-133: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test is not table-driven.

This test exercises multiple scenarios (empty remainder normalization, existing remainder preserved, non-vault account untouched, idempotency on re-run) as a single linear sequence rather than a table-driven test with named cases. As per coding guidelines, **/*_test.go: "Unit and integration tests must use table-driven test patterns to ensure exhaustive coverage of edge cases" and each case should have "a clear, descriptive name that explains the scenario being tested."

That said, the idempotency check (running Migrate1to2 twice against shared state) is inherently sequential and doesn't map cleanly onto independent table cases, so this may be an acceptable exception if the shared-state assertions can't be cleanly decomposed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@keeper/migrations_test.go` around lines 80 - 133,
`TestKeeper_MigrateVaultFeeRemainderDefaults` currently covers several scenarios
in one linear flow; refactor the independent assertions into a table-driven test
with named cases for empty remainder normalization, existing remainder
preservation, and non-vault accounts left untouched. Use
`TestKeeper_MigrateVaultFeeRemainderDefaults`, the local
`createVault`/`getVault` helpers, and `keeper.NewMigrator(...).Migrate1to2` to
structure per-case setup and expectations. Keep the second `Migrate1to2`
idempotency check as a separate sequential subtest or clearly isolated exception
since it depends on shared state.

Source: Coding guidelines

types/vault.go (1)

298-307: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider enforcing the sub-unit invariant (< 1), not just non-negativity.

FeeRemainder is documented and tested (see AccrueAUMFeePayment's dust-accumulation test) as always a fraction in [0, 1), but Validate() only rejects negative values. A value >= 1 (e.g., from a bad genesis import or future migration bug) would pass validation and get silently folded into the next fee calculation.

♻️ Proposed fix to add an upper-bound check
 	if v.FeeRemainder != "" {
 		remainder, err := sdkmath.LegacyNewDecFromStr(v.FeeRemainder)
 		if err != nil {
 			return fmt.Errorf("invalid fee remainder: %s", v.FeeRemainder)
 		}
 		if remainder.IsNegative() {
 			return fmt.Errorf("fee remainder cannot be negative: %s", v.FeeRemainder)
 		}
+		if remainder.GTE(sdkmath.LegacyOneDec()) {
+			return fmt.Errorf("fee remainder must be less than 1: %s", v.FeeRemainder)
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@types/vault.go` around lines 298 - 307, In Vault.Validate, the FeeRemainder
check only rejects negatives, so update the validation in that block to also
enforce the documented sub-unit invariant for v.FeeRemainder; after parsing with
sdkmath.LegacyNewDecFromStr, add an upper-bound check that rejects any remainder
greater than or equal to 1.0 with a clear validation error, alongside the
existing parse and non-negative checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@keeper/migrations_test.go`:
- Around line 80-133: `TestKeeper_MigrateVaultFeeRemainderDefaults` currently
covers several scenarios in one linear flow; refactor the independent assertions
into a table-driven test with named cases for empty remainder normalization,
existing remainder preservation, and non-vault accounts left untouched. Use
`TestKeeper_MigrateVaultFeeRemainderDefaults`, the local
`createVault`/`getVault` helpers, and `keeper.NewMigrator(...).Migrate1to2` to
structure per-case setup and expectations. Keep the second `Migrate1to2`
idempotency check as a separate sequential subtest or clearly isolated exception
since it depends on shared state.

In `@types/vault.go`:
- Around line 298-307: In Vault.Validate, the FeeRemainder check only rejects
negatives, so update the validation in that block to also enforce the documented
sub-unit invariant for v.FeeRemainder; after parsing with
sdkmath.LegacyNewDecFromStr, add an upper-bound check that rejects any remainder
greater than or equal to 1.0 with a clear validation error, alongside the
existing parse and non-negative checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 01a73be4-4c20-4064-ae62-9376f76f310d

📥 Commits

Reviewing files that changed from the base of the PR and between 95fd746 and 6c748cf.

⛔ Files ignored due to path filters (1)
  • types/vault.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (15)
  • .changelog/unreleased/improvements/224-aum-fee-dust.md
  • api/provlabs/vault/v1/vault.pulsar.go
  • interest/interest.go
  • interest/interest_test.go
  • keeper/migrations.go
  • keeper/migrations_test.go
  • keeper/migrator.go
  • keeper/reconcile.go
  • keeper/reconcile_test.go
  • module.go
  • proto/provlabs/vault/v1/vault.proto
  • spec/02_state.md
  • spec/06_blocker.md
  • types/vault.go
  • types/vault_test.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
keeper/reconcile_test.go (1)

2285-2324: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Solid regression test for the dust-carry fix.

Correctly proves naive per-window truncation collects nothing while the remainder-carrying path recovers the full expected fee, matching the issue's intent.

One nit: 10,000 iterations each performing a keeper AccrueAUMFeePayment call (with state writes) is comparatively heavy for a unit test; a smaller iteration count (e.g., 500–1000) would likely still demonstrate the invariant with less runtime cost.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@keeper/reconcile_test.go` around lines 2285 - 2324, The dust-carry regression
test in AccrueAUMFeePayment is correct, but the loop count is unnecessarily high
for a unit test and makes it slow. Reduce the iterations in the
"AccrueAUMFeePayment dust accumulation preserves revenue across short windows"
subtest to a smaller value that still proves naive truncation stays zero while
the accumulated path reaches the expected whole fee, and keep the assertions
tied to interest.CalculateAUMFee, interest.CalculateAUMFeeDec, and
s.k.AccrueAUMFeePayment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@keeper/reconcile_test.go`:
- Around line 2285-2324: The dust-carry regression test in AccrueAUMFeePayment
is correct, but the loop count is unnecessarily high for a unit test and makes
it slow. Reduce the iterations in the "AccrueAUMFeePayment dust accumulation
preserves revenue across short windows" subtest to a smaller value that still
proves naive truncation stays zero while the accumulated path reaches the
expected whole fee, and keep the assertions tied to interest.CalculateAUMFee,
interest.CalculateAUMFeeDec, and s.k.AccrueAUMFeePayment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 78e0220d-9513-4c33-96d0-3478f10a17f1

📥 Commits

Reviewing files that changed from the base of the PR and between 6c748cf and 461b230.

📒 Files selected for processing (7)
  • .changelog/unreleased/improvements/224-aum-fee-dust.md
  • docs/superpowers/plans/2026-06-28-force-pause-flag.md
  • keeper/migrations_test.go
  • keeper/reconcile.go
  • keeper/reconcile_test.go
  • types/vault.go
  • types/vault_test.go
✅ Files skipped from review due to trivial changes (2)
  • .changelog/unreleased/improvements/224-aum-fee-dust.md
  • docs/superpowers/plans/2026-06-28-force-pause-flag.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • types/vault.go
  • keeper/migrations_test.go
  • keeper/reconcile.go

Comment thread keeper/reconcile.go Outdated
Comment thread keeper/reconcile.go
Comment thread keeper/reconcile.go Outdated
Comment thread types/vault.go
Comment on lines +298 to +306
if v.FeeRemainder != "" {
remainder, err := sdkmath.LegacyNewDecFromStr(v.FeeRemainder)
if err != nil {
return fmt.Errorf("invalid fee remainder: %s", v.FeeRemainder)
}
if remainder.IsNegative() {
return fmt.Errorf("fee remainder cannot be negative: %s", v.FeeRemainder)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Remainder is unbounded This accepts any non-negative imported fee_remainder, but reconciliation treats the parsed value as already accrued fee. A vault imported with a large value such as 1000000 passes validation, then the next fee reconciliation folds that amount into combined, converts it to a payment-denom fee, and adds it to the collectible or outstanding AUM fee even though the accumulator did not produce it. The validation needs to enforce the runtime carry invariant for the vault's payment denom, not just non-negativity.

Comment thread types/vault.go
Comment on lines +305 to +307
}
if v.PaymentDenom == v.UnderlyingAsset && !remainder.TruncateInt().IsZero() {
return fmt.Errorf("fee remainder must carry less than one whole unit when payment denom equals underlying asset: %s", v.FeeRemainder)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Bound Coarse Remainders This only bounds the carry when payment_denom equals underlying_asset. For a coarse payment denom, the carry still has a maximum valid value: it must be less than the underlying value needed to produce one more whole payment unit. With this validation, an imported vault using a coarse payment_denom can set fee_remainder to a large value such as 9999; the next fee reconciliation adds that stored value into combined, converts most of it into OutstandingAumFee, and collects fees the vault did not accrue. The coarse-denom branch needs its own bound based on the vault conversion rate, or it needs to reject imported values that cannot be safely bounded here.

@nullpointer0x00 nullpointer0x00 marked this pull request as draft July 2, 2026 18:23
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.

Implement dust accumulation to prevent AUM fee precision loss

1 participant