Skip to content

fix: edit cap#488

Merged
antoncoding merged 3 commits intomasterfrom
hotfix/edit
Apr 1, 2026
Merged

fix: edit cap#488
antoncoding merged 3 commits intomasterfrom
hotfix/edit

Conversation

@antoncoding
Copy link
Copy Markdown
Owner

@antoncoding antoncoding commented Apr 1, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced error diagnostics and logging for smart plan calculations, providing better visibility into when calculation failures occur
  • Refactor

    • Improved market caps state initialization with optimized equality comparison and memoization to eliminate unnecessary state updates
    • Refined smart plan recalculation with enhanced dependency tracking and deterministic signatures for markets, constraints, and positions to ensure stable and predictable behavior during rebalancing

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4c09d28f-1354-43a5-8578-a6d9f59432c4

📥 Commits

Reviewing files that changed from the base of the PR and between 63950ba and 3f699fa.

📒 Files selected for processing (1)
  • src/features/autovault/components/vault-detail/settings/EditCaps.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/features/autovault/components/vault-detail/settings/EditCaps.tsx

📝 Walkthrough

Walkthrough

Two components refactored to optimize re-computation logic. EditCaps.tsx now prevents redundant state updates using memoization and deep equality checks for market cap maps. rebalance-modal.tsx adds deterministic signature helpers and refs to control smart plan recalculation dependencies instead of relying directly on eligibleMarkets.

Changes

Cohort / File(s) Summary
Market Caps Initialization
src/features/autovault/components/vault-detail/settings/EditCaps.tsx
Introduced areMarketCapsEqual() to compare market cap maps. Replaced direct state updates with memoized initialMarketCaps and conditional updates via useEffect when values differ. Removed availableMarkets.length === 0 guard.
Smart Plan Recalculation
src/features/positions/components/rebalance/rebalance-modal.tsx
Added three signature helper functions for eligible markets, constraints, and grouped positions. Introduced smartPlannerEligibleMarketsRef to track eligible markets outside render dependencies. Memoized signatures control effect recalculation instead of direct dependency on eligibleMarkets. Enhanced error handling with structured logging.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • fix: approval + rebalance flow #138 — Modifies rebalance-modal.tsx eligibility and recalculation tracking logic similar to the smart plan dependency changes here.
  • fix: edit caps #319 — Modifies EditCaps.tsx initialization and guarding logic for market caps to prevent unwanted resets.
  • fix: rebalance modal #272 — Changes the same rebalance-modal.tsx component (though for modal state management rather than smart plan dependencies).
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 'fix: edit cap' is too vague and doesn't clearly convey the actual changes made—it touches two separate files with complex logic around market cap initialization and smart planner signatures. Use a more specific title that captures the main fix, like 'fix: prevent redundant market cap updates with deep equality checks' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hotfix/edit

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 and usage tips.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
monarch Ready Ready Preview, Comment Apr 1, 2026 3:52pm

Request Review

@coderabbitai coderabbitai Bot added the bug Something isn't working label Apr 1, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/autovault/components/vault-detail/settings/EditCaps.tsx (1)

151-158: ⚠️ Potential issue | 🟠 Major

Replace Number(bigint) division with formatUnits().

Number() conversion loses precision for large cap values, which corrupts display and later comparisons (in hasChanges). Both the relative and absolute cap conversions are affected. Use formatUnits() from viem instead.

Fix
-import { type Address, parseUnits, maxUint128 } from 'viem';
+import { type Address, formatUnits, parseUnits, maxUint128 } from 'viem';
...
-        const relativeCap = (Number(relativeCapBigInt) / 1e16).toString();
+        const relativeCap = formatUnits(relativeCapBigInt, 16);
...
-            : (Number(absoluteCapBigInt) / 10 ** vaultAssetDecimals).toString();
+            : formatUnits(absoluteCapBigInt, vaultAssetDecimals);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/autovault/components/vault-detail/settings/EditCaps.tsx` around
lines 151 - 158, The current conversions for relativeCap and absoluteCap use
Number(bigint) / divisor which loses precision; replace them with viem's
formatUnits to preserve full precision: import formatUnits from viem, convert
relativeCapBigInt via formatUnits(relativeCapBigInt, 16) (since code used 1e16)
and convert absoluteCapBigInt via formatUnits(absoluteCapBigInt,
vaultAssetDecimals); keep the existing sentinel handling for absoluteCapBigInt
(0n or >= maxUint128 => ''), and ensure parseBigIntOrFallback,
relativeCapBigInt, absoluteCapBigInt, maxUint128, and vaultAssetDecimals are
used as before so hasChanges comparisons still work.
🧹 Nitpick comments (2)
src/features/positions/components/rebalance/rebalance-modal.tsx (2)

94-104: Include chainId in these signatures.

They only stay collision-free because this modal already filters to one chain. Adding chain scope here keeps the identity rule local to the helper and avoids quiet cross-chain collisions if either helper gets reused.

Possible tweak
 function getSmartPlannerMarketSignature(market: Market): string {
   return [
+    market.morphoBlue.chain.id,
     market.uniqueKey,
     market.loanAsset.address.toLowerCase(),
     market.collateralAsset.address.toLowerCase(),
     market.oracleAddress?.toLowerCase() ?? '',
     market.irmAddress?.toLowerCase() ?? '',
     market.lltv ?? '',
     market.state.rateAtTarget,
   ].join(':');
 }

 function getSmartPlannerGroupedPositionSignature(groupedPosition: GroupedPosition): string {
-  return groupedPosition.markets
-    .map((position) => `${position.market.uniqueKey}:${position.state.supplyAssets}:${position.state.supplyShares}`)
-    .sort()
-    .join('|');
+  return `${groupedPosition.chainId}|${groupedPosition.markets
+    .map((position) => `${position.market.uniqueKey}:${position.state.supplyAssets}:${position.state.supplyShares}`)
+    .sort()
+    .join('|')}`;
 }

As per coding guidelines: "All market/token/route identity checks must be chain-scoped using canonical identifiers (chainId + market.uniqueKey or chainId + address)".

Also applies to: 113-118

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/positions/components/rebalance/rebalance-modal.tsx` around lines
94 - 104, The market signature currently built in getSmartPlannerMarketSignature
lacks chain scoping which can cause cross-chain collisions; include the chainId
as a canonical identifier (e.g., prepend or include market.chainId as a string)
when constructing the signature, normalizing it (toString()) alongside existing
fields and keeping existing lowercasing for addresses; also apply the same
change to the analogous token/route helper referenced around lines 113-118
(e.g., getSmartPlannerTokenSignature or related helper) so all market/token
identity checks are chain-scoped using chainId + uniqueKey/address.

205-214: These signatures are still bypassed by raw deps.

smartPlannerSelectedMarketsSignature and smartPlannerConstraintSignature only change on content changes, but the effect still also depends on smartSelectedMarketKeys and debouncedSmartMaxAllocationBps. Because updateMaxAllocation always creates a fresh object, a blur that normalizes back to the same value still reruns the planner. If the goal is content-based invalidation, mirror those values behind refs too, or make the setters return prev on no-op updates.

Worth rerunning the hooks exhaustive-deps check if you move either value behind a ref.

Also applies to: 313-323

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/positions/components/rebalance/rebalance-modal.tsx` around lines
205 - 214, The memoized signatures smartPlannerSelectedMarketsSignature and
smartPlannerConstraintSignature are being bypassed by their raw dependencies
(smartSelectedMarketKeys and debouncedSmartMaxAllocationBps) because updater
functions like updateMaxAllocation create new objects on every call; fix by
either (A) mirror smartSelectedMarketKeys and debouncedSmartMaxAllocationBps
behind refs and use those refs as the actual dependencies for the useMemo hooks
(eg. derive smartPlannerSelectedMarketsSignature from a ref copy instead of the
raw Set/number), or (B) change the setter/updateMaxAllocation to perform a no-op
when the new value equals the previous value and return the previous state (so
the reference doesn't change), and then re-run the hooks exhaustive-deps check
to ensure no raw deps remain; apply the same approach to the other affected
signatures (see smartPlannerGroupedPosition and eligibleMarkets usages in the
file).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/shared/token-icon.tsx`:
- Around line 41-53: The component is still using token.symbol instead of the
explicit symbol prop; destructure symbol from TokenIcon's props and replace any
uses of token.symbol (notably in the Image alt and any tooltip/title rendering
such as the renderImage/triggerImage usage and the element at the other
occurrence) with the passed-in symbol so an override is respected; keep tokenImg
= token.img as-is but ensure all user-visible labels use the symbol prop (update
TokenIcon's props destructuring and the Image alt and tooltip/title references).

In `@src/features/autovault/components/vault-detail/settings/EditCaps.tsx`:
- Around line 143-149: The current code in EditCaps.tsx calls continue when
hasCompleteEditableMarketMetadata(market) is false, which hides a persisted
on-chain cap; instead, keep the cap in the edit model but mark it as
invalid/read-only and attach an error so it remains visible and non-editable.
Replace the continue branch in the loop that builds caps (referencing market,
cap, cap.capId and hasCompleteEditableMarketMetadata) with logic that pushes a
placeholder entry into the edits array (e.g., { ...cap, readOnly: true,
validationError: 'missing token metadata' }) or toggles a flag on the existing
edit object, ensure the UI rendering for EditCaps shows readOnly rows with the
validationError, and make the save/submit path (the form submit handler) fail
the save if any edit entries have validationError/readOnly so users cannot
accidentally drop or modify that cap.
- Around line 305-310: The code is silently falling back to 0n when
parseBigIntOrFallback fails for adapterCap
(currentRelativeCap/currentAbsoluteCap) — change this so parse failures abort
the save and surface an error: replace the silent-default pattern around
existingCaps?.adapterCap parsing with explicit validation (using the parse
result or an exception from parseBigIntOrFallback) and, if parsing fails, throw
or return an error from the save handler instead of fabricating old* values;
apply the same change for collateralCap and marketCap parsing paths so malformed
persisted cap values stop the transaction flow and show a user-facing/throwable
error rather than defaulting to 0n.

---

Outside diff comments:
In `@src/features/autovault/components/vault-detail/settings/EditCaps.tsx`:
- Around line 151-158: The current conversions for relativeCap and absoluteCap
use Number(bigint) / divisor which loses precision; replace them with viem's
formatUnits to preserve full precision: import formatUnits from viem, convert
relativeCapBigInt via formatUnits(relativeCapBigInt, 16) (since code used 1e16)
and convert absoluteCapBigInt via formatUnits(absoluteCapBigInt,
vaultAssetDecimals); keep the existing sentinel handling for absoluteCapBigInt
(0n or >= maxUint128 => ''), and ensure parseBigIntOrFallback,
relativeCapBigInt, absoluteCapBigInt, maxUint128, and vaultAssetDecimals are
used as before so hasChanges comparisons still work.

---

Nitpick comments:
In `@src/features/positions/components/rebalance/rebalance-modal.tsx`:
- Around line 94-104: The market signature currently built in
getSmartPlannerMarketSignature lacks chain scoping which can cause cross-chain
collisions; include the chainId as a canonical identifier (e.g., prepend or
include market.chainId as a string) when constructing the signature, normalizing
it (toString()) alongside existing fields and keeping existing lowercasing for
addresses; also apply the same change to the analogous token/route helper
referenced around lines 113-118 (e.g., getSmartPlannerTokenSignature or related
helper) so all market/token identity checks are chain-scoped using chainId +
uniqueKey/address.
- Around line 205-214: The memoized signatures
smartPlannerSelectedMarketsSignature and smartPlannerConstraintSignature are
being bypassed by their raw dependencies (smartSelectedMarketKeys and
debouncedSmartMaxAllocationBps) because updater functions like
updateMaxAllocation create new objects on every call; fix by either (A) mirror
smartSelectedMarketKeys and debouncedSmartMaxAllocationBps behind refs and use
those refs as the actual dependencies for the useMemo hooks (eg. derive
smartPlannerSelectedMarketsSignature from a ref copy instead of the raw
Set/number), or (B) change the setter/updateMaxAllocation to perform a no-op
when the new value equals the previous value and return the previous state (so
the reference doesn't change), and then re-run the hooks exhaustive-deps check
to ensure no raw deps remain; apply the same approach to the other affected
signatures (see smartPlannerGroupedPosition and eligibleMarkets usages in the
file).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 46b170d9-e57b-4e30-adc4-dec21bd2fb2d

📥 Commits

Reviewing files that changed from the base of the PR and between b1360b4 and 63950ba.

📒 Files selected for processing (3)
  • src/components/shared/token-icon.tsx
  • src/features/autovault/components/vault-detail/settings/EditCaps.tsx
  • src/features/positions/components/rebalance/rebalance-modal.tsx

Comment thread src/components/shared/token-icon.tsx Outdated
Comment on lines +41 to +53
const tokenImg = token.img;
const renderImage = () => (
<Image
className="rounded-full"
src={token.img}
src={tokenImg}
alt={token.symbol}
width={width}
height={height}
style={{ opacity }}
unoptimized
/>
);
const triggerImage = renderImage();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use the passed symbol here too.

This helper still reads token.symbol, so callers with an intentional symbol override will keep seeing the registry symbol in the image alt text and tooltip title.

✏️ Suggested change
   if (token?.img) {
     const tokenImg = token.img;
+    const displaySymbol = symbol ?? token.symbol;
     const renderImage = () => (
       <Image
         className="rounded-full"
         src={tokenImg}
-        alt={token.symbol}
+        alt={displaySymbol}
         width={width}
         height={height}
         style={{ opacity }}
         unoptimized
       />
     );
     const triggerImage = renderImage();

-    const title = customTooltipTitle ?? token.symbol;
+    const title = customTooltipTitle ?? displaySymbol;

You’ll also need to destructure symbol from TokenIcon’s props.

Based on learnings: The TokenIcon component accepts a symbol prop, and intentional display-symbol differences should be captured as explicit overrides instead of silent drift in token metadata.

Also applies to: 55-55

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/shared/token-icon.tsx` around lines 41 - 53, The component is
still using token.symbol instead of the explicit symbol prop; destructure symbol
from TokenIcon's props and replace any uses of token.symbol (notably in the
Image alt and any tooltip/title rendering such as the renderImage/triggerImage
usage and the element at the other occurrence) with the passed-in symbol so an
override is respected; keep tokenImg = token.img as-is but ensure all
user-visible labels use the symbol prop (update TokenIcon's props destructuring
and the Image alt and tooltip/title references).

Comment on lines +143 to +149
if (!hasCompleteEditableMarketMetadata(market)) {
console.error('[EditCaps] skipping market cap with incomplete market metadata', {
marketUniqueKey: market.uniqueKey,
capId: cap.capId,
});
continue;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't hide persisted caps here.

This continue drops a live on-chain market cap from the edit model whenever metadata is incomplete. The table then shows fewer caps than actually exist, and a later save can never remove or repair that hidden cap. Better to block editing with a visible error, or keep a read-only placeholder row for the bad market.

As per coding guidelines: Fail closed for any market whose required token metadata cannot be resolved safely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/autovault/components/vault-detail/settings/EditCaps.tsx` around
lines 143 - 149, The current code in EditCaps.tsx calls continue when
hasCompleteEditableMarketMetadata(market) is false, which hides a persisted
on-chain cap; instead, keep the cap in the edit model but mark it as
invalid/read-only and attach an error so it remains visible and non-editable.
Replace the continue branch in the loop that builds caps (referencing market,
cap, cap.capId and hasCompleteEditableMarketMetadata) with logic that pushes a
placeholder entry into the edits array (e.g., { ...cap, readOnly: true,
validationError: 'missing token metadata' }) or toggles a flag on the existing
edit object, ensure the UI rendering for EditCaps shows readOnly rows with the
validationError, and make the save/submit path (the form submit handler) fail
the save if any edit entries have validationError/readOnly so users cannot
accidentally drop or modify that cap.

Comment on lines +305 to +310
const currentRelativeCap = existingCaps?.adapterCap
? parseBigIntOrFallback(existingCaps.adapterCap.relativeCap, 0n, `adapter:${existingCaps.adapterCap.capId}:relativeCap`)
: 0n;
const currentAbsoluteCap = existingCaps?.adapterCap
? parseBigIntOrFallback(existingCaps.adapterCap.absoluteCap, 0n, `adapter:${existingCaps.adapterCap.capId}:absoluteCap`)
: 0n;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Abort save on malformed persisted cap values.

Defaulting a bad persisted adapter cap to 0n here means we still build a write payload with synthetic old* values. The same pattern repeats for collateral and market caps below. In this path, parse failure should stop the save and surface an error instead of fabricating fallback state.

As per coding guidelines: Guard null/undefined/stale API and contract fields in all tx-critical paths so malformed data degrades safely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/autovault/components/vault-detail/settings/EditCaps.tsx` around
lines 305 - 310, The code is silently falling back to 0n when
parseBigIntOrFallback fails for adapterCap
(currentRelativeCap/currentAbsoluteCap) — change this so parse failures abort
the save and surface an error: replace the silent-default pattern around
existingCaps?.adapterCap parsing with explicit validation (using the parse
result or an exception from parseBigIntOrFallback) and, if parsing fails, throw
or return an error from the save handler instead of fabricating old* values;
apply the same change for collateralCap and marketCap parsing paths so malformed
persisted cap values stop the transaction flow and show a user-facing/throwable
error rather than defaulting to 0n.

@antoncoding antoncoding merged commit f99a784 into master Apr 1, 2026
4 checks passed
@antoncoding antoncoding deleted the hotfix/edit branch April 1, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant