Skip to content
Merged
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
4 changes: 4 additions & 0 deletions src/constants/repay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const BPS_DENOMINATOR = 10_000n;

// 0.10% buffer for repay-by-shares.
export const REPAY_BY_SHARES_BUFFER_BPS = 10n;
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,6 @@ export function BorrowedMorphoBlueTable({ account, positions, onRefetch, isRefet
showOracle
showLltv
/>
{row.hasResidualCollateral && (
<span className="rounded bg-hovered px-1.5 py-0.5 text-xs font-medium text-secondary">Inactive</span>
)}
</div>
</TableCell>

Expand Down
33 changes: 15 additions & 18 deletions src/hooks/useBorrowTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { formatBalance } from '@/utils/balance';
import { getBundlerV2, MONARCH_TX_IDENTIFIER } from '@/utils/morpho';
import type { Market } from '@/utils/types';
import { useERC20Approval } from './useERC20Approval';
import { useMorphoAuthorization } from './useMorphoAuthorization';
import { useBundlerAuthorizationStep } from './useBundlerAuthorizationStep';
import { usePermit2 } from './usePermit2';
import { useAppSettings } from '@/stores/useAppSettings';
import { useStyledToast } from './useStyledToast';
Expand Down Expand Up @@ -45,11 +45,12 @@ export function useBorrowTransaction({ market, collateralAmount, borrowAmount, o
const bundlerAddress = getBundlerV2(market.morphoBlue.chain.id);

// Hook for Morpho bundler authorization (both sig and tx)
const { isBundlerAuthorized, isAuthorizingBundler, authorizeBundlerWithSignature, authorizeWithTransaction, refetchIsBundlerAuthorized } =
useMorphoAuthorization({
const { isBundlerAuthorized, isAuthorizingBundler, ensureBundlerAuthorization, refetchIsBundlerAuthorized } = useBundlerAuthorizationStep(
{
chainId: market.morphoBlue.chain.id,
authorized: bundlerAddress,
});
bundlerAddress: bundlerAddress as Address,
},
);

// Get approval for collateral token
const {
Expand Down Expand Up @@ -165,11 +166,9 @@ export function useBorrowTransaction({ market, collateralAmount, borrowAmount, o
tracking.update('execute');

// Bundler authorization may still be needed for the borrow operation
if (!isBundlerAuthorized) {
const bundlerAuthSigTx = await authorizeBundlerWithSignature();
if (bundlerAuthSigTx) {
transactions.push(bundlerAuthSigTx);
}
const { authorizationTxData } = await ensureBundlerAuthorization({ mode: 'signature' });
if (authorizationTxData) {
transactions.push(authorizationTxData);
}
} else if (usePermit2Setting) {
// --- Permit2 Flow ---
Expand All @@ -180,9 +179,9 @@ export function useBorrowTransaction({ market, collateralAmount, borrowAmount, o
}

tracking.update('authorize_bundler_sig');
const bundlerAuthSigTx = await authorizeBundlerWithSignature(); // Get signature for Bundler auth if needed
if (bundlerAuthSigTx) {
transactions.push(bundlerAuthSigTx);
const { authorizationTxData } = await ensureBundlerAuthorization({ mode: 'signature' }); // Get signature for Bundler auth if needed
if (authorizationTxData) {
transactions.push(authorizationTxData);
await new Promise((resolve) => setTimeout(resolve, 800)); // UI delay
}

Expand Down Expand Up @@ -212,8 +211,8 @@ export function useBorrowTransaction({ market, collateralAmount, borrowAmount, o
} else {
// --- Standard ERC20 Flow ---
tracking.update('authorize_bundler_tx');
const bundlerTxAuthorized = await authorizeWithTransaction(); // Authorize Bundler via TX if needed
if (!bundlerTxAuthorized) {
const { authorized } = await ensureBundlerAuthorization({ mode: 'transaction' }); // Authorize Bundler via TX if needed
if (!authorized) {
throw new Error('Failed to authorize Bundler via transaction.'); // Stop if auth tx fails/is rejected
}
// Wait for tx confirmation implicitly handled by useTransactionWithToast within authorizeWithTransaction
Expand Down Expand Up @@ -332,10 +331,8 @@ export function useBorrowTransaction({ market, collateralAmount, borrowAmount, o
usePermit2Setting,
permit2Authorized,
authorizePermit2,
authorizeBundlerWithSignature,
isBundlerAuthorized,
ensureBundlerAuthorization,
signForBundlers,
authorizeWithTransaction,
isApproved,
approve,
batchAddUserMarkets,
Expand Down
68 changes: 68 additions & 0 deletions src/hooks/useBundlerAuthorizationStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useCallback } from 'react';
import type { Address } from 'viem';
import { useMorphoAuthorization } from './useMorphoAuthorization';

type AuthorizationMode = 'signature' | 'transaction';

type EnsureBundlerAuthorizationParams = {
mode: AuthorizationMode;
};

type UseBundlerAuthorizationStepParams = {
chainId: number;
bundlerAddress: Address;
};

type EnsureBundlerAuthorizationResult = {
authorized: boolean;
authorizationTxData: `0x${string}` | null;
};

export const useBundlerAuthorizationStep = ({ chainId, bundlerAddress }: UseBundlerAuthorizationStepParams) => {
const { isBundlerAuthorized, isAuthorizingBundler, authorizeBundlerWithSignature, authorizeWithTransaction, refetchIsBundlerAuthorized } =
useMorphoAuthorization({
chainId,
authorized: bundlerAddress,
});

const ensureBundlerAuthorization = useCallback(
async ({ mode }: EnsureBundlerAuthorizationParams): Promise<EnsureBundlerAuthorizationResult> => {
if (isBundlerAuthorized === true) {
return {
authorized: true,
authorizationTxData: null,
};
}

if (mode === 'signature') {
const authorizationTxData = await authorizeBundlerWithSignature();
if (authorizationTxData) {
return {
authorized: true,
authorizationTxData: authorizationTxData as `0x${string}`,
};
}

const refreshedAuthorization = await refetchIsBundlerAuthorized();
return {
authorized: refreshedAuthorization.data === true,
authorizationTxData: null,
};
}
Comment on lines +37 to +51
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

authorized: true returned even when signature wasn't obtained.

authorizeBundlerWithSignature() can return null without actually authorizing (e.g., missing account or nonce). In that case you'd return { authorized: true, authorizationTxData: null }, which tells callers everything is fine when it isn't.

Proposed fix
      if (mode === 'signature') {
        const authorizationTxData = await authorizeBundlerWithSignature();
        return {
-          authorized: true,
+          authorized: authorizationTxData !== null,
          authorizationTxData: authorizationTxData as `0x${string}` | null,
        };
      }
📝 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
if (mode === 'signature') {
const authorizationTxData = await authorizeBundlerWithSignature();
return {
authorized: true,
authorizationTxData: authorizationTxData as `0x${string}` | null,
};
}
if (mode === 'signature') {
const authorizationTxData = await authorizeBundlerWithSignature();
return {
authorized: authorizationTxData !== null,
authorizationTxData: authorizationTxData as `0x${string}` | null,
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useBundlerAuthorizationStep.ts` around lines 37 - 43, The code in
useBundlerAuthorizationStep sets authorized: true unconditionally when mode ===
'signature' even though authorizeBundlerWithSignature() can return null; change
the logic to await authorizeBundlerWithSignature(), then if authorizationTxData
is truthy (non-null) return { authorized: true, authorizationTxData:
authorizationTxData as `0x${string}` }, otherwise return { authorized: false,
authorizationTxData: null } (optionally include any error handling or logging);
update references to authorizationTxData and the mode === 'signature' branch in
useBundlerAuthorizationStep accordingly.


const authorized = await authorizeWithTransaction();
return {
authorized,
authorizationTxData: null,
};
},
[isBundlerAuthorized, authorizeBundlerWithSignature, authorizeWithTransaction, refetchIsBundlerAuthorized],
);

return {
isBundlerAuthorized,
isAuthorizingBundler,
ensureBundlerAuthorization,
refetchIsBundlerAuthorized,
};
};
31 changes: 18 additions & 13 deletions src/hooks/useERC20Approval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,29 @@ export function useERC20Approval({
successDescription: `Successfully approved ${tokenSymbol} for spender ${spender.slice(2, 8)}`,
});

const approve = useCallback(async () => {
if (!account || !amount) return;
const approve = useCallback(
async (amountOverride?: bigint) => {
const approvalAmount = amountOverride ?? amount;
if (!account || !approvalAmount) return;

await sendTransactionAsync({
account,
to: token,
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [spender, amount],
}),
});
await sendTransactionAsync({
account,
to: token,
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [spender, approvalAmount],
}),
});

await refetchAllowance();
}, [account, amount, sendTransactionAsync, token, spender, refetchAllowance]);
await refetchAllowance();
},
[account, amount, sendTransactionAsync, token, spender, refetchAllowance],
);

return {
isApproved,
allowance: allowance ?? 0n,
approve,
isApproving,
};
Expand Down
85 changes: 45 additions & 40 deletions src/hooks/usePermit2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,54 +50,59 @@ export function usePermit2({ user, chainId = 1, token, spender, refetchInterval

const permit2Authorized = useMemo(() => !!allowanceToPermit2 && allowanceToPermit2 > amount, [allowanceToPermit2, amount]);

const signForBundlers = useCallback(async () => {
if (!user || !spender || !token) throw new Error('User, spender, or token not provided');
const signForBundlers = useCallback(
async (amountOverride?: bigint) => {
if (!user || !spender || !token) throw new Error('User, spender, or token not provided');

const deadline = moment.now() + 600;
const deadline = moment.now() + 600;
const permitAmount = amountOverride ?? amount;

const nonce = packedAllowance ? ((packedAllowance as number[])[2] as number) : 0;
const nonce = packedAllowance ? ((packedAllowance as number[])[2] as number) : 0;

const permitSingle = {
details: {
token: token,
amount: amount,
expiration: deadline,
nonce,
},
spender: spender,
sigDeadline: BigInt(deadline),
};
const permitSingle = {
details: {
token,
amount: permitAmount,
expiration: deadline,
nonce,
},
spender,
sigDeadline: BigInt(deadline),
};

// sign erc712 signature for permit2
const sigs = await signTypedDataAsync({
domain: {
name: 'Permit2',
chainId,
verifyingContract: PERMIT2_ADDRESS,
},
types: {
PermitDetails: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint160' },
{ name: 'expiration', type: 'uint48' },
{ name: 'nonce', type: 'uint48' },
],
// (PermitDetails details,address spender,uint256 sigDeadline)
PermitSingle: [
{ name: 'details', type: 'PermitDetails' },
{ name: 'spender', type: 'address' },
{ name: 'sigDeadline', type: 'uint256' },
],
},
primaryType: 'PermitSingle',
message: permitSingle,
});
// sign erc712 signature for permit2
const sigs = await signTypedDataAsync({
domain: {
name: 'Permit2',
chainId,
verifyingContract: PERMIT2_ADDRESS,
},
types: {
PermitDetails: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint160' },
{ name: 'expiration', type: 'uint48' },
{ name: 'nonce', type: 'uint48' },
],
// (PermitDetails details,address spender,uint256 sigDeadline)
PermitSingle: [
{ name: 'details', type: 'PermitDetails' },
{ name: 'spender', type: 'address' },
{ name: 'sigDeadline', type: 'uint256' },
],
},
primaryType: 'PermitSingle',
message: permitSingle,
});

return { sigs, permitSingle };
}, [user, spender, token, chainId, packedAllowance, amount, signTypedDataAsync]);
return { sigs, permitSingle };
},
[user, spender, token, chainId, packedAllowance, amount, signTypedDataAsync],
);

return {
permit2Authorized,
permit2Allowance: allowanceToPermit2,
authorizePermit2,
signForBundlers,
isLoading: isLoadingAllowance,
Expand Down
24 changes: 12 additions & 12 deletions src/hooks/useRebalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getBundlerV2, MONARCH_TX_IDENTIFIER } from '@/utils/morpho';
import type { GroupedPosition, RebalanceAction } from '@/utils/types';
import { GAS_COSTS, GAS_MULTIPLIER_NUMERATOR, GAS_MULTIPLIER_DENOMINATOR } from '@/features/markets/components/constants';
import { useERC20Approval } from './useERC20Approval';
import { useMorphoAuthorization } from './useMorphoAuthorization';
import { useBundlerAuthorizationStep } from './useBundlerAuthorizationStep';
import { usePermit2 } from './usePermit2';
import { useAppSettings } from '@/stores/useAppSettings';
import { useStyledToast } from './useStyledToast';
Expand Down Expand Up @@ -37,11 +37,12 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: ()
const totalAmount = rebalanceActions.reduce((acc, action) => acc + BigInt(action.amount), BigInt(0));

// Hook for Morpho bundler authorization (both sig and tx)
const { isBundlerAuthorized, isAuthorizingBundler, authorizeBundlerWithSignature, authorizeWithTransaction, refetchIsBundlerAuthorized } =
useMorphoAuthorization({
const { isBundlerAuthorized, isAuthorizingBundler, ensureBundlerAuthorization, refetchIsBundlerAuthorized } = useBundlerAuthorizationStep(
{
chainId: groupedPosition.chainId,
authorized: bundlerAddress,
});
bundlerAddress: bundlerAddress as Address,
},
);

// Hook for Permit2 handling
const {
Expand Down Expand Up @@ -266,9 +267,9 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: ()
}

tracking.update('authorize_bundler_sig');
const bundlerAuthSigTx = await authorizeBundlerWithSignature(); // Get signature for Bundler auth if needed
if (bundlerAuthSigTx) {
transactions.push(bundlerAuthSigTx);
const { authorizationTxData } = await ensureBundlerAuthorization({ mode: 'signature' }); // Get signature for Bundler auth if needed
if (authorizationTxData) {
transactions.push(authorizationTxData);
await new Promise((resolve) => setTimeout(resolve, 800)); // UI delay
}

Expand All @@ -292,8 +293,8 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: ()
} else {
// --- Standard ERC20 Flow ---
tracking.update('authorize_bundler_tx');
const bundlerTxAuthorized = await authorizeWithTransaction(); // Authorize Bundler via TX if needed
if (!bundlerTxAuthorized) {
const { authorized } = await ensureBundlerAuthorization({ mode: 'transaction' }); // Authorize Bundler via TX if needed
if (!authorized) {
throw new Error('Failed to authorize Bundler via transaction.'); // Stop if auth tx fails/is rejected
}
// Wait for tx confirmation implicitly handled by useTransactionWithToast within authorizeWithTransaction
Expand Down Expand Up @@ -385,9 +386,8 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: ()
usePermit2Setting,
permit2Authorized,
authorizePermit2,
authorizeBundlerWithSignature,
ensureBundlerAuthorization,
signForBundlers,
authorizeWithTransaction,
isTokenApproved,
approveToken,
generateRebalanceTxData,
Expand Down
Loading