Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ NEXT_PUBLIC_BASE_RPC=
NEXT_PUBLIC_POLYGON_RPC=
NEXT_PUBLIC_UNICHAIN_RPC=
NEXT_PUBLIC_ARBITRUM_RPC=
NEXT_PUBLIC_ETHERLINK_RPC=
NEXT_PUBLIC_HYPEREVM_RPC=
NEXT_PUBLIC_MONAD_RPC=

Expand Down
3 changes: 3 additions & 0 deletions src/config/appkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createStorage, type Storage } from 'wagmi';
import localStorage from 'local-storage-fallback';
import { createAppKit } from '@reown/appkit/react';
import type { AppKitNetwork } from '@reown/appkit/networks';
import { etherlink } from 'viem/chains';
import { arbitrum, base, mainnet, monad, optimism, polygon, unichain } from 'wagmi/chains';
import { SupportedNetworks, getDefaultRPC, hyperEvm } from '@/utils/networks';

Expand Down Expand Up @@ -51,6 +52,7 @@ const customBase = withAppKitRpc(base, getDefaultRPC(SupportedNetworks.Base));
const customPolygon = withAppKitRpc(polygon, getDefaultRPC(SupportedNetworks.Polygon));
const customArbitrum = withAppKitRpc(arbitrum, getDefaultRPC(SupportedNetworks.Arbitrum));
const customUnichain = withAppKitRpc(unichain, getDefaultRPC(SupportedNetworks.Unichain));
const customEtherlink = withAppKitRpc(etherlink, getDefaultRPC(SupportedNetworks.Etherlink));
const customMonad = withAppKitRpc(monad, getDefaultRPC(SupportedNetworks.Monad));
const customHyperEvm = withAppKitRpc(hyperEvm, getDefaultRPC(SupportedNetworks.HyperEVM));

Expand All @@ -62,6 +64,7 @@ export const networks = [
customPolygon,
customArbitrum,
customUnichain,
customEtherlink,
customHyperEvm,
customMonad,
] as [AppKitNetwork, ...AppKitNetwork[]];
Expand Down
1 change: 1 addition & 0 deletions src/constants/public-allocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const PUBLIC_ALLOCATOR_ADDRESSES: Partial<Record<SupportedNetworks, `0x${
[SupportedNetworks.Polygon]: '0xfac15aff53ADd2ff80C2962127C434E8615Df0d3',
[SupportedNetworks.Unichain]: '0xB0c9a107fA17c779B3378210A7a593e88938C7C9',
[SupportedNetworks.Arbitrum]: '0x769583Af5e9D03589F159EbEC31Cc2c23E8C355E',
[SupportedNetworks.Etherlink]: '0x8b8B1bd41d36c06253203CD21463994aB752c1e6',
[SupportedNetworks.HyperEVM]: '0x517505be22D9068687334e69ae7a02fC77edf4Fc',
[SupportedNetworks.Monad]: '0xfd70575B732F9482F4197FE1075492e114E97302',
};
1 change: 1 addition & 0 deletions src/features/admin-v2/components/chain-volume-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const CHAIN_COLOR_INDEX: Record<number, number> = {
137: 3, // Polygon
130: 4, // Unichain
42161: 6, // Arbitrum
42793: 7, // Etherlink
999: 5, // HyperEVM
143: 2, // Monad
};
Expand Down
30 changes: 16 additions & 14 deletions src/features/autovault/components/vault-identity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ExternalLinkIcon } from '@radix-ui/react-icons';
import { TokenIcon } from '@/components/shared/token-icon';
import { TooltipContent } from '@/components/shared/tooltip-content';
import type { VaultCurator } from '@/constants/vaults/known_vaults';
import { getVaultURL } from '@/utils/external';
import { getVaultURL, supportsMorphoAppLinks } from '@/utils/external';
import { VaultIcon } from './vault-icon';

type VaultIdentityVariant = 'chip' | 'inline' | 'icon';
Expand Down Expand Up @@ -44,6 +44,7 @@ export function VaultIdentity({
showAddressInTooltip = true,
}: VaultIdentityProps) {
const vaultHref = useMemo(() => getVaultURL(address, chainId), [address, chainId]);
const canLinkToMorpho = useMemo(() => supportsMorphoAppLinks(chainId), [chainId]);
const formattedAddress = `${address.slice(0, 6)}...${address.slice(-4)}`;
const displayName = vaultName ?? formattedAddress;
const curatorLabel = curator === 'unknown' ? 'Curator unknown' : `Curated by ${curator}`;
Expand Down Expand Up @@ -92,19 +93,20 @@ export function VaultIdentity({
);
})();

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


if (!showTooltip) {
return interactiveContent;
Expand Down
16 changes: 9 additions & 7 deletions src/features/market-detail/components/market-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { convertApyToApr } from '@/utils/rateMath';
import { formatReadable } from '@/utils/balance';
import { getIRMTitle } from '@/utils/morpho';
import { getNetworkImg, getNetworkName, type SupportedNetworks } from '@/utils/networks';
import { getMarketURL } from '@/utils/external';
import { getMarketURL, supportsMorphoAppLinks } from '@/utils/external';
import type { Market, MarketPosition, WarningWithDetail } from '@/utils/types';
import { WarningCategory } from '@/utils/types';
import { getRiskLevel, countWarningsByLevel, type RiskLevel } from '@/utils/warnings';
Expand Down Expand Up @@ -630,12 +630,14 @@ export function MarketHeader({
Accrue Interest
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => window.open(getMarketURL(resolvedMarketId, network), '_blank')}
startContent={<FiExternalLink className="h-4 w-4" />}
>
View on Morpho
</DropdownMenuItem>
{supportsMorphoAppLinks(network) && (
<DropdownMenuItem
onClick={() => window.open(getMarketURL(resolvedMarketId, network), '_blank')}
startContent={<FiExternalLink className="h-4 w-4" />}
Comment on lines +633 to +636
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.

>
View on Morpho
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
Expand Down
103 changes: 80 additions & 23 deletions src/features/positions/components/rebalance/rebalance-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { FiTrash2 } from 'react-icons/fi';
import { AllocationCell } from '../allocation-cell';
import { FromMarketsTable } from '../from-markets-table';
import { MarketIdentity, MarketIdentityFocus, MarketIdentityMode } from '@/features/markets/components/market-identity';
import { RiskIndicator } from '@/features/markets/components/risk-indicator';
import { RebalanceActionInput } from './rebalance-action-input';
import { RebalanceCart } from './rebalance-cart';

Expand Down Expand Up @@ -143,6 +144,44 @@ function PreviewSection({ title, rows }: { title: string; rows: PreviewRow[] })
);
}

function formatAmountForSmartConstraintLog(value: bigint, decimals: number): { raw: string; formatted: string } {
return {
raw: value.toString(),
formatted: formatUnits(value, decimals),
};
}

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


const reasons = new Set(violations.map((violation) => violation.reason));

if (reasons.size === 1 && reasons.has('locked-liquidity')) {
return {
title: 'Some max-allocation limits could not be met with current withdrawable liquidity.',
detail:
'One or more positions cannot be reduced far enough right now. Raise the cap on the flagged market or wait for more liquidity before retrying.',
};
}

if (reasons.size === 1 && reasons.has('selected-capacity')) {
return {
title: 'Your selected max-allocation limits leave too little room for the full balance.',
detail:
plan?.diagnostics.unallocatedAmount && plan.diagnostics.unallocatedAmount > 0n
? 'Raise one or more caps or add more destination markets. Any excess amount beyond the selected room will remain in the wallet.'
: 'Raise one or more caps or add more destination markets before retrying.',
};
}

return {
title: 'Some max-allocation limits could not be fully satisfied.',
detail:
'One or more positions could not be reduced far enough because of current withdrawable liquidity or selected capacity. Check the console for per-market details.',
};
}

export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, isRefetching }: RebalanceModalProps) {
const [mode, setMode] = useState<RebalanceMode>('smart');

Expand Down Expand Up @@ -292,6 +331,34 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch,
})
.then((plan) => {
if (id !== calcIdRef.current) return;

if (plan && plan.diagnostics.constraintViolations.length > 0) {
console.warn('[smart-rebalance] unmet max-allocation constraints', {
calcId: id,
chainId: groupedPosition.chainId,
loanAssetSymbol: groupedPosition.loanAssetSymbol,
totalPool: formatAmountForSmartConstraintLog(plan.totalPool, groupedPosition.loanAssetDecimals),
totalMoved: formatAmountForSmartConstraintLog(plan.totalMoved, groupedPosition.loanAssetDecimals),
selectedCapacityShortfall: formatAmountForSmartConstraintLog(
plan.diagnostics.selectedCapacityShortfall,
groupedPosition.loanAssetDecimals,
),
unallocatedAmount: formatAmountForSmartConstraintLog(plan.diagnostics.unallocatedAmount, groupedPosition.loanAssetDecimals),
violations: plan.diagnostics.constraintViolations.map((violation) => ({
uniqueKey: violation.uniqueKey,
collateralSymbol: violation.collateralSymbol,
maxAllocationPercent: violation.maxAllocationBps / 100,
reason: violation.reason,
currentAmount: formatAmountForSmartConstraintLog(violation.currentAmount, groupedPosition.loanAssetDecimals),
targetAmount: formatAmountForSmartConstraintLog(violation.targetAmount, groupedPosition.loanAssetDecimals),
maxAllowedAmount: formatAmountForSmartConstraintLog(violation.maxAllowedAmount, groupedPosition.loanAssetDecimals),
excessAmount: formatAmountForSmartConstraintLog(violation.excessAmount, groupedPosition.loanAssetDecimals),
maxWithdrawable: formatAmountForSmartConstraintLog(violation.maxWithdrawable, groupedPosition.loanAssetDecimals),
lockedAmount: formatAmountForSmartConstraintLog(violation.lockedAmount, groupedPosition.loanAssetDecimals),
})),
});
}

setSmartPlan(plan);
})
.catch((error: unknown) => {
Expand Down Expand Up @@ -503,26 +570,7 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch,
});
}, [currentSupplyByMarket, groupedPosition.loanAssetDecimals, marketByKey, smartPlan, smartSelectedMarketKeys]);

const constraintViolations = useMemo(() => {
if (!smartPlan) return [];

const deltaByMarket = new Map(smartPlan.deltas.map((delta) => [delta.market.uniqueKey, delta]));
const violations: { uniqueKey: string; maxAllocationBps: number }[] = [];

for (const [uniqueKey, maxAllocationBps] of Object.entries(debouncedSmartMaxAllocationBps)) {
if (maxAllocationBps >= 10_000) continue;

const delta = deltaByMarket.get(uniqueKey);
const targetAmount = delta?.targetAmount ?? currentSupplyByMarket.get(uniqueKey) ?? 0n;
const maxAllowedAmount = (smartPlan.totalPool * BigInt(maxAllocationBps)) / 10_000n;

if (targetAmount > maxAllowedAmount) {
violations.push({ uniqueKey, maxAllocationBps });
}
}

return violations;
}, [currentSupplyByMarket, debouncedSmartMaxAllocationBps, smartPlan]);
const smartConstraintWarning = useMemo(() => getSmartConstraintWarning(smartPlan), [smartPlan]);

const isSmartWithdrawOnly = useMemo(() => {
if (!smartPlan || smartTotalMoved === 0n) return false;
Expand Down Expand Up @@ -1134,9 +1182,18 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch,
{!isSmartCalculating && smartPlan && smartTotalMoved > 0n && !isSmartFeeReady && (
<div className="text-sm text-red-500">Waiting for loan asset USD price to enforce the smart rebalance fee cap.</div>
)}
{!isSmartCalculating && constraintViolations.length > 0 && (
<div className="rounded border border-yellow-500/30 bg-yellow-500/10 px-2 py-1.5 text-xs text-yellow-300">
Some max-allocation limits could not be fully satisfied due to current market liquidity/capacity.
{!isSmartCalculating && smartConstraintWarning != null && (
<div className="flex items-start gap-3 rounded border border-border bg-surface px-3 py-2.5 shadow-sm">
<div className="pt-0.5">
<RiskIndicator
level="yellow"
description={smartConstraintWarning.title}
/>
</div>
<div className="min-w-0">
<div className="text-sm font-medium text-yellow-700 dark:text-yellow-300">{smartConstraintWarning.title}</div>
<div className="mt-1 text-xs leading-relaxed text-secondary">{smartConstraintWarning.detail}</div>
</div>
</div>
)}

Expand Down
Loading