Skip to content

fix: smart rebalance#489

Closed
antoncoding wants to merge 2 commits intomasterfrom
fix/smart-rebalance
Closed

fix: smart rebalance#489
antoncoding wants to merge 2 commits intomasterfrom
fix/smart-rebalance

Conversation

@antoncoding
Copy link
Copy Markdown
Owner

@antoncoding antoncoding commented Apr 2, 2026

Summary by CodeRabbit

Release Notes

  • New Features
    • Etherlink network is now fully supported across the platform
    • Smart rebalance feature now displays detailed constraint diagnostics with visual warnings when allocation limits are exceeded
    • Morpho external links are now intelligently available only on supported networks

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 2, 2026

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

Project Deployment Actions Updated (UTC)
monarch Ready Ready Preview, Comment Apr 2, 2026 7:33am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Adds Etherlink network support throughout the application, including network configuration, RPC integration, token lists, and contract addresses. Also enhances the smart rebalance engine with constraint violation diagnostics and adds conditional Morpho app link rendering based on network support.

Changes

Cohort / File(s) Summary
Network Configuration & Environment
.env.local.example, src/utils/networks.ts, src/config/appkit.ts, src/store/createWagmiConfig.ts, src/utils/rpc.ts
Added Etherlink as a supported network with RPC configuration, chain setup, and Wagmi integration.
Etherlink Contract Addresses & Metadata
src/utils/morpho.ts, src/constants/public-allocator.ts, src/types/token.ts, src/utils/tokens.ts
Registered Morpho contract addresses, allocator address, WETH mapping, and token addresses (USDC, WETH, WBTC, WXTZ, mBASIS, mMEV, mTBILL, xU3O8) for Etherlink.
Morpho Link Conditional Rendering
src/utils/external.ts, src/features/autovault/components/vault-identity.tsx, src/features/market-detail/components/market-header.tsx
Made Morpho app links conditional—exported getMorphoNetworkSlug, added supportsMorphoAppLinks check, updated external URLs to fall back to base Morpho app when network unsupported.
Smart Rebalance Engine Diagnostics
src/features/positions/smart-rebalance/types.ts, src/features/positions/smart-rebalance/engine.ts, src/features/positions/components/rebalance/rebalance-modal.tsx
Added constraint violation tracking and diagnostics to engine output; updated modal to display structured warnings with violation details instead of generic constraint flags.
UI & Styling
src/features/admin-v2/components/chain-volume-chart.tsx
Added color mapping for Etherlink chain ID in chart visualization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

network, integration, enhancement

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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: smart rebalance' is vague and doesn't convey what the actual fix entails. The changeset includes substantial network support (Etherlink), diagnostics infrastructure, and constraint handling improvements—none of which are reflected in the title. Consider a more specific title like 'Add Etherlink network support and smart rebalance diagnostics' or 'Improve smart rebalance constraint tracking and Morpho app link handling'.
✅ 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 fix/smart-rebalance

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.

@coderabbitai coderabbitai Bot added bug Something isn't working ui User interface labels Apr 2, 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: 5

Caution

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

⚠️ Outside diff range comments (1)
src/utils/networks.ts (1)

26-37: ⚠️ Potential issue | 🟠 Major

Don't store an empty RPC URL for Etherlink.

This new path is the only one with no Alchemy fallback, so an unset NEXT_PUBLIC_ETHERLINK_RPC now resolves to ''. That leaves Etherlink configured with a non-endpoint instead of failing fast or using a real fallback.

Also applies to: 181-181

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

In `@src/utils/networks.ts` around lines 26 - 37, getRpcUrl currently can return
an empty string when neither a specific RPC nor Alchemy URL is available, which
causes Etherlink to be configured with a non-endpoint; update the function
(getRpcUrl) to never return '' by using undefined as the final fallback (replace
the literal '' in both return branches with undefined) and ensure
specificRpcUrl/NEXT_PUBLIC_ETHERLINK_RPC are still sanitized to undefined (the
existing targetRpc logic is fine); leave caller behavior unchanged but confirm
callers handle undefined instead of an empty string.
🧹 Nitpick comments (1)
src/utils/external.ts (1)

17-19: Prefer undefined over a homepage fallback for unsupported Morpho links.

supportsMorphoAppLinks() already models the unsupported case. Returning the Morpho homepage here makes a missed guard look like a valid deep link, which is easy to ship by accident. Let these helpers return undefined instead, so callers have to handle unsupported chains explicitly.

Also applies to: 21-30

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

In `@src/utils/external.ts` around lines 17 - 19, Change helpers to return
undefined for unsupported Morpho chains instead of defaulting to the Morpho
homepage: update supportsMorphoAppLinks to return getMorphoNetworkSlug(chainId)
!== undefined (already OK) and modify the other helper(s) in this file that
currently return the Morpho homepage fallback (referenced in lines 21-30) to
return undefined when getMorphoNetworkSlug(chainId) is undefined; ensure callers
must explicitly handle the undefined case rather than receiving a homepage URL.
🤖 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/features/autovault/components/vault-identity.tsx`:
- Around line 96-109: The tooltip action still uses actionHref regardless of
chain gating; update the tooltip rendering to apply the same showLink &&
canLinkToMorpho guard used for interactiveContent: when rendering the tooltip
action that currently uses actionHref, only render the external Link (or anchor
with href=vaultHref/actionHref) if showLink && canLinkToMorpho, otherwise render
the non-clickable/baseContent (or a disabled/plain element). Locate
interactiveContent, baseContent, showLink, canLinkToMorpho and actionHref in the
VaultIdentity component and make the tooltip’s action follow the same
conditional pattern (preserve target, rel and stopPropagation behavior when
emitting the Link).

In `@src/features/market-detail/components/market-header.tsx`:
- Around line 633-636: The onClick handler in market-header's DropdownMenuItem
uses window.open(getMarketURL(resolvedMarketId, network), '_blank') without
security flags; update the window.open call in the MarketHeader component to
include the noopener,noreferrer feature string (e.g., third argument
'noopener,noreferrer') to prevent window.opener access and reverse-tabnabbing,
and scan other components mentioned (Navbar, AccountDropdown,
useTransactionWithToast) for similar window.open uses and apply the same change
or replace with an <a> element that includes rel="noopener noreferrer" where
appropriate.

In `@src/features/positions/components/rebalance/rebalance-modal.tsx`:
- Around line 154-156: getSmartConstraintWarning currently returns null when
diagnostics.constraintViolations is empty, which misses cases where
diagnostics.selectedCapacityShortfall or diagnostics.unallocatedAmount > 0;
update getSmartConstraintWarning to also detect positive
selectedCapacityShortfall or unallocatedAmount and return the existing warning
object (and emit the console.warn) even if constraintViolations is empty so the
banner is shown; also update any other gating logic that only checks
diagnostics.constraintViolations (the other instances referenced in the review)
to include checks for selectedCapacityShortfall and unallocatedAmount so
shortfalls trigger the same warning/console.warn behavior.

In `@src/features/positions/smart-rebalance/engine.ts`:
- Around line 269-277: The helper hasExplicitMaxAllocationConstraint currently
treats any cap <100% as "explicit" even when it's non-binding; update
hasExplicitMaxAllocationConstraint to only return true when a clamped
maxAllocationBps is defined, < 10_000, AND it actually binds the current
portfolio (i.e., entry.currentAllocationBps > maxAllocationBps). Locate
hasExplicitMaxAllocationConstraint and the analogous logic at the other spot
(lines ~431-433) and change the condition to check the entry's current
allocation (e.g., entry.currentAllocationBps) against the clamped
maxAllocationBps before marking the cap as explicit.

In `@src/utils/tokens.ts`:
- Around line 368-373: There are duplicate token records for symbol 'xU3O8' on
the same chain (etherlink) and identical address
(0x79052Ab3C166D4899a1e0DD033aC3b379AF0B1fD) which causes findToken() to always
match the first and getUniqueTokens() to potentially return both; remove or
merge the duplicate entry so each chain-scoped token (chainId + address) appears
only once, ensuring token identity follows the canonical chain-scoped rule used
by findToken() and getUniqueTokens().

---

Outside diff comments:
In `@src/utils/networks.ts`:
- Around line 26-37: getRpcUrl currently can return an empty string when neither
a specific RPC nor Alchemy URL is available, which causes Etherlink to be
configured with a non-endpoint; update the function (getRpcUrl) to never return
'' by using undefined as the final fallback (replace the literal '' in both
return branches with undefined) and ensure
specificRpcUrl/NEXT_PUBLIC_ETHERLINK_RPC are still sanitized to undefined (the
existing targetRpc logic is fine); leave caller behavior unchanged but confirm
callers handle undefined instead of an empty string.

---

Nitpick comments:
In `@src/utils/external.ts`:
- Around line 17-19: Change helpers to return undefined for unsupported Morpho
chains instead of defaulting to the Morpho homepage: update
supportsMorphoAppLinks to return getMorphoNetworkSlug(chainId) !== undefined
(already OK) and modify the other helper(s) in this file that currently return
the Morpho homepage fallback (referenced in lines 21-30) to return undefined
when getMorphoNetworkSlug(chainId) is undefined; ensure callers must explicitly
handle the undefined case rather than receiving a homepage URL.
🪄 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: 1730c7a2-9481-41da-8834-024569aae833

📥 Commits

Reviewing files that changed from the base of the PR and between f99a784 and 73c65b3.

⛔ Files ignored due to path filters (5)
  • src/imgs/chains/etherlink.svg is excluded by !**/*.svg
  • src/imgs/tokens/mbasis.png is excluded by !**/*.png
  • src/imgs/tokens/mmev.svg is excluded by !**/*.svg
  • src/imgs/tokens/mtbill.png is excluded by !**/*.png
  • src/imgs/tokens/xu3o8.png is excluded by !**/*.png
📒 Files selected for processing (16)
  • .env.local.example
  • src/config/appkit.ts
  • src/constants/public-allocator.ts
  • src/features/admin-v2/components/chain-volume-chart.tsx
  • src/features/autovault/components/vault-identity.tsx
  • src/features/market-detail/components/market-header.tsx
  • src/features/positions/components/rebalance/rebalance-modal.tsx
  • src/features/positions/smart-rebalance/engine.ts
  • src/features/positions/smart-rebalance/types.ts
  • src/store/createWagmiConfig.ts
  • src/types/token.ts
  • src/utils/external.ts
  • src/utils/morpho.ts
  • src/utils/networks.ts
  • src/utils/rpc.ts
  • src/utils/tokens.ts

Comment on lines +96 to +109
const interactiveContent =
showLink && canLinkToMorpho ? (
<Link
href={vaultHref}
target="_blank"
rel="noopener noreferrer"
className="no-underline"
onClick={(e) => e.stopPropagation()}
>
{baseContent}
</Link>
) : (
baseContent
);
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

Apply Morpho-link gating to tooltip action too

The new gate only affects interactiveContent. On unsupported chains, the tooltip still renders actionHref (Lines 151-153), so users can still open Morpho from the tooltip.

Proposed fix
-          actionIcon={<ExternalLinkIcon className="h-4 w-4" />}
-          actionHref={vaultHref}
+          actionIcon={canLinkToMorpho ? <ExternalLinkIcon className="h-4 w-4" /> : undefined}
+          actionHref={canLinkToMorpho ? vaultHref : undefined}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/autovault/components/vault-identity.tsx` around lines 96 - 109,
The tooltip action still uses actionHref regardless of chain gating; update the
tooltip rendering to apply the same showLink && canLinkToMorpho guard used for
interactiveContent: when rendering the tooltip action that currently uses
actionHref, only render the external Link (or anchor with
href=vaultHref/actionHref) if showLink && canLinkToMorpho, otherwise render the
non-clickable/baseContent (or a disabled/plain element). Locate
interactiveContent, baseContent, showLink, canLinkToMorpho and actionHref in the
VaultIdentity component and make the tooltip’s action follow the same
conditional pattern (preserve target, rel and stopPropagation behavior when
emitting the Link).

Comment on lines +633 to +636
{supportsMorphoAppLinks(network) && (
<DropdownMenuItem
onClick={() => window.open(getMarketURL(resolvedMarketId, network), '_blank')}
startContent={<FiExternalLink className="h-4 w-4" />}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find _blank window.open calls that appear to omit the 3rd-arg feature string
rg -nP --type=ts --type=tsx "window\.open\(\s*[^,]+,\s*['\"]_blank['\"]\s*\)" -C2

Repository: antoncoding/monarch

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Read the specific file to see lines 633-640
cat -n src/features/market-detail/components/market-header.tsx | sed -n '630,645p'

Repository: antoncoding/monarch

Length of output: 784


🏁 Script executed:

#!/bin/bash
# Fix the file type syntax and search for window.open with _blank calls
rg -nP --type=ts "window\.open\([^,]+,\s*['\"]_blank['\"]\s*\)" -C2

Repository: antoncoding/monarch

Length of output: 1727


🏁 Script executed:

#!/bin/bash
# Also search without type restriction to catch tsx files
fd -e ts -e tsx -x grep -n "window\.open" {} + | grep "_blank" -C2

Repository: antoncoding/monarch

Length of output: 23504


Add security flags to window.open call

Line 635 opens a new tab without noopener,noreferrer flags, leaving the opened page access to the window.opener property. Add the flags to block reverse-tabnabbing attacks.

Proposed fix
- onClick={() => window.open(getMarketURL(resolvedMarketId, network), '_blank')}
+ onClick={() => window.open(getMarketURL(resolvedMarketId, network), '_blank', 'noopener,noreferrer')}

Note: Several other files in the codebase also lack these flags (e.g., Navbar.tsx, AccountDropdown.tsx, useTransactionWithToast.tsx), so consider a broader audit and fix.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{supportsMorphoAppLinks(network) && (
<DropdownMenuItem
onClick={() => window.open(getMarketURL(resolvedMarketId, network), '_blank')}
startContent={<FiExternalLink className="h-4 w-4" />}
{supportsMorphoAppLinks(network) && (
<DropdownMenuItem
onClick={() => window.open(getMarketURL(resolvedMarketId, network), '_blank', 'noopener,noreferrer')}
startContent={<FiExternalLink className="h-4 w-4" />}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/market-detail/components/market-header.tsx` around lines 633 -
636, The onClick handler in market-header's DropdownMenuItem uses
window.open(getMarketURL(resolvedMarketId, network), '_blank') without security
flags; update the window.open call in the MarketHeader component to include the
noopener,noreferrer feature string (e.g., third argument 'noopener,noreferrer')
to prevent window.opener access and reverse-tabnabbing, and scan other
components mentioned (Navbar, AccountDropdown, useTransactionWithToast) for
similar window.open uses and apply the same change or replace with an <a>
element that includes rel="noopener noreferrer" where appropriate.

Comment on lines +154 to +156
function getSmartConstraintWarning(plan: SmartRebalancePlan | null): { title: string; detail: string } | null {
const violations = plan?.diagnostics.constraintViolations ?? [];
if (violations.length === 0) return null;
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

Warn on global capacity shortfalls even without per-market violations.

selectedCapacityShortfall / unallocatedAmount can be positive when every chosen market lands exactly at its cap, which leaves constraintViolations empty. With the current gates, that suppresses both the banner and the console.warn, so users get no signal that some funds will stay in the wallet.

Also applies to: 335-359, 1185-1197

🤖 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
154 - 156, getSmartConstraintWarning currently returns null when
diagnostics.constraintViolations is empty, which misses cases where
diagnostics.selectedCapacityShortfall or diagnostics.unallocatedAmount > 0;
update getSmartConstraintWarning to also detect positive
selectedCapacityShortfall or unallocatedAmount and return the existing warning
object (and emit the console.warn) even if constraintViolations is empty so the
banner is shown; also update any other gating logic that only checks
diagnostics.constraintViolations (the other instances referenced in the review)
to include checks for selectedCapacityShortfall and unallocatedAmount so
shortfalls trigger the same warning/console.warn behavior.

Comment on lines +269 to +277
function hasExplicitMaxAllocationConstraint(
entries: SmartRebalanceEngineInput['entries'],
constraints: SmartRebalanceConstraintMap | undefined,
): boolean {
return entries.some((entry) => {
const maxAllocationBps = clampBps(constraints?.[entry.uniqueKey]?.maxAllocationBps);
return maxAllocationBps !== undefined && maxAllocationBps < 10_000;
});
}
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 drop the reliability guard for non-binding caps.

Any cap below 100% makes hasExplicitConstraints true, even when the current allocation already satisfies every cap. In that case the current portfolio is still a valid constrained plan, so skipping the guard lets the greedy allocator return a lower weighted APY for no reason.

Possible fix
-  const hasExplicitConstraints = hasExplicitMaxAllocationConstraint(entries, constraints);
-
   // 1. Simulate extra liquidity and start state:
   //    - attempt best-effort withdrawal from each selected market
   //    - keep any non-withdrawable remainder locked in-place
   const cleaned = cleanStates(entries, constraints, totalPool);
+  const currentAllocationViolatesConstraints = entries.some((entry) => {
+    const maxAllocation = cleaned.maxAllocationMap.get(entry.uniqueKey);
+    return maxAllocation !== undefined && entry.currentSupply > maxAllocation;
+  });
@@
-  if (!hasExplicitConstraints && projectedObjective < currentObjective) {
+  if (!currentAllocationViolatesConstraints && projectedObjective < currentObjective) {

Also applies to: 431-433

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

In `@src/features/positions/smart-rebalance/engine.ts` around lines 269 - 277, The
helper hasExplicitMaxAllocationConstraint currently treats any cap <100% as
"explicit" even when it's non-binding; update hasExplicitMaxAllocationConstraint
to only return true when a clamped maxAllocationBps is defined, < 10_000, AND it
actually binds the current portfolio (i.e., entry.currentAllocationBps >
maxAllocationBps). Locate hasExplicitMaxAllocationConstraint and the analogous
logic at the other spot (lines ~431-433) and change the condition to check the
entry's current allocation (e.g., entry.currentAllocationBps) against the
clamped maxAllocationBps before marking the cap as explicit.

Comment thread src/utils/tokens.ts
Comment on lines +368 to +373
{
symbol: 'xU3O8',
img: undefined,
decimals: 18,
networks: [{ chain: etherlink, address: '0x79052Ab3C166D4899a1e0DD033aC3b379AF0B1fD' }],
},
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

Deduplicate the Etherlink xU3O8 entry.

These two records resolve to the same chainId + address. findToken() will always hit the first one, and getUniqueTokens() can return both, so the later Midas/image metadata is effectively dead.

♻️ One simple fix
-  {
-    symbol: 'xU3O8',
-    img: undefined,
-    decimals: 18,
-    networks: [{ chain: etherlink, address: '0x79052Ab3C166D4899a1e0DD033aC3b379AF0B1fD' }],
-  },

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: 421-429

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

In `@src/utils/tokens.ts` around lines 368 - 373, There are duplicate token
records for symbol 'xU3O8' on the same chain (etherlink) and identical address
(0x79052Ab3C166D4899a1e0DD033aC3b379AF0B1fD) which causes findToken() to always
match the first and getUniqueTokens() to potentially return both; remove or
merge the duplicate entry so each chain-scoped token (chainId + address) appears
only once, ensuring token identity follows the canonical chain-scoped rule used
by findToken() and getUniqueTokens().

@antoncoding antoncoding closed this Apr 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working ui User interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant