-
Notifications
You must be signed in to change notification settings - Fork 3
fix: smart rebalance #489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: smart rebalance #489
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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'; | ||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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*\)" -C2Repository: 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*\)" -C2Repository: 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" -C2Repository: antoncoding/monarch Length of output: 23504 Add security flags to window.open call Line 635 opens a new tab without 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., 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| > | ||||||||||||||||||
| View on Morpho | ||||||||||||||||||
| </DropdownMenuItem> | ||||||||||||||||||
| )} | ||||||||||||||||||
| </DropdownMenuContent> | ||||||||||||||||||
| </DropdownMenu> | ||||||||||||||||||
| </div> | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
|
||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Warn on global capacity shortfalls even without per-market violations.
Also applies to: 335-359, 1185-1197 🤖 Prompt for AI Agents |
||
|
|
||
| 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'); | ||
|
|
||
|
|
@@ -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) => { | ||
|
|
@@ -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; | ||
|
|
@@ -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> | ||
| )} | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apply Morpho-link gating to tooltip action too
The new gate only affects
interactiveContent. On unsupported chains, the tooltip still rendersactionHref(Lines 151-153), so users can still open Morpho from the tooltip.Proposed fix
🤖 Prompt for AI Agents