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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ When touching transaction and position flows, validation MUST include all releva
12. **Aggregator API contract integrity**: quote-only request params must never be forwarded to transaction-build endpoints (e.g. Velora `version` on `/prices` but not `/transactions/:network`); enforce endpoint-specific payload/query builders, normalize fetch/network failures into typed API errors, and verify returned route token addresses match requested canonical token addresses before using previews/tx payloads.
13. **User-rejection error normalization**: transaction hooks must map wallet rejection payloads (EIP-1193 `4001`, `ACTION_REJECTED`, viem request-argument dumps) to a short canonical UI message (`User rejected transaction.`) and never render raw payload text in inline UI/error boxes.
14. **Input/state integrity in tx-critical UIs**: never strip unsupported numeric syntax into a different value (e.g. `1e-6` must be rejected, not rewritten), and after any balance refetch re-derive selected token objects from refreshed data before allowing `Max`/submit.
15. **Swap leverage/deleverage flashloan callback integrity**: Bundler3 swap leverage/deleverage must use adapter flashloan callbacks (not pre-swap borrow gating), with `callbackHash`/`reenter` wiring and adapter token flows matching on-chain contracts; before submit, verify Velora quote/tx parity (route src amount + Paraswap calldata exact/min offsets) so previewed borrow/repay/collateral amounts cannot drift from executed inputs.
16. **Multi-leg quote completeness for swap previews**: when a preview depends on multiple aggregator legs (e.g. SELL repay quote + BUY max-collateral bound), surface failures from every required leg and use conservative fallbacks (`0`, disable submit) instead of optimistic defaults so partial quote failures cannot overstate safe unwind amounts.
17. **Adapter-executed aggregator build integrity**: for Bundler3 adapter swaps, never hard-require wallet-level allowance checks from aggregator build endpoints; attempt normal build first, retry with endpoint `ignoreChecks` only for allowance-specific failures, and fail closed unless the built transaction target matches trusted addresses from the quoted route.
18. **Route-selection and entrypoint consistency**: in leverage/deleverage UIs, selected route mode must never execute a different route while capability probes are in-flight; unsupported-entrypoint CTAs must be gated by executable route availability (not transient probe states) to avoid dead-end modal paths and false unsupported flashes.

### REQUIRED: Regression Rule Capture

Expand Down
3 changes: 3 additions & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
// Object shorthand - disable for gentle migration
"useConsistentObjectDefinitions": "off",

// Catch mutable declarations that can be const
"useConst": "error",

// ENABLED - Safe, auto-fixable style improvements
"useImportType": {
"level": "warn",
Expand Down
108 changes: 108 additions & 0 deletions src/abis/bundlerV3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import type { Abi } from 'viem';

/**
* Minimal Bundler3 ABI for multicall + callback reentry.
*/
export const bundlerV3Abi = [
{
type: 'function',
stateMutability: 'view',
name: 'callbackHash',
inputs: [{ internalType: 'bytes', name: 'data', type: 'bytes' }],
outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
},
{
type: 'function',
stateMutability: 'view',
name: 'initiator',
inputs: [],
outputs: [{ internalType: 'address', name: '', type: 'address' }],
},
{
type: 'function',
stateMutability: 'view',
name: 'reenterHash',
inputs: [],
outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
},
{
type: 'function',
stateMutability: 'payable',
name: 'multicall',
inputs: [
{
internalType: 'struct Call[]',
name: 'bundle',
type: 'tuple[]',
components: [
{
internalType: 'address',
name: 'to',
type: 'address',
},
{
internalType: 'bytes',
name: 'data',
type: 'bytes',
},
{
internalType: 'uint256',
name: 'value',
type: 'uint256',
},
{
internalType: 'bool',
name: 'skipRevert',
type: 'bool',
},
{
internalType: 'bytes32',
name: 'callbackHash',
type: 'bytes32',
},
],
},
],
outputs: [],
},
{
type: 'function',
stateMutability: 'nonpayable',
name: 'reenter',
inputs: [
{
internalType: 'struct Call[]',
name: 'bundle',
type: 'tuple[]',
components: [
{
internalType: 'address',
name: 'to',
type: 'address',
},
{
internalType: 'bytes',
name: 'data',
type: 'bytes',
},
{
internalType: 'uint256',
name: 'value',
type: 'uint256',
},
{
internalType: 'bool',
name: 'skipRevert',
type: 'bool',
},
{
internalType: 'bytes32',
name: 'callbackHash',
type: 'bytes32',
},
],
},
],
outputs: [],
},
] as const satisfies Abi;
103 changes: 103 additions & 0 deletions src/abis/morphoGeneralAdapterV1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { Abi } from 'viem';

const marketParamsTuple = {
internalType: 'struct MarketParams',
name: 'marketParams',
type: 'tuple',
components: [
{ internalType: 'address', name: 'loanToken', type: 'address' },
{ internalType: 'address', name: 'collateralToken', type: 'address' },
{ internalType: 'address', name: 'oracle', type: 'address' },
{ internalType: 'address', name: 'irm', type: 'address' },
{ internalType: 'uint256', name: 'lltv', type: 'uint256' },
],
} as const;

/**
* Minimal GeneralAdapter1 ABI needed for swap-backed leverage.
*/
export const morphoGeneralAdapterV1Abi = [
{
type: 'function',
stateMutability: 'nonpayable',
name: 'erc20Transfer',
inputs: [
{ internalType: 'address', name: 'token', type: 'address' },
{ internalType: 'address', name: 'receiver', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
outputs: [],
},
{
type: 'function',
stateMutability: 'nonpayable',
name: 'erc20TransferFrom',
inputs: [
{ internalType: 'address', name: 'token', type: 'address' },
{ internalType: 'address', name: 'receiver', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
outputs: [],
},
{
type: 'function',
stateMutability: 'nonpayable',
name: 'morphoSupplyCollateral',
inputs: [
marketParamsTuple,
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
{ internalType: 'address', name: 'onBehalf', type: 'address' },
{ internalType: 'bytes', name: 'data', type: 'bytes' },
],
outputs: [],
},
{
type: 'function',
stateMutability: 'nonpayable',
name: 'morphoBorrow',
inputs: [
marketParamsTuple,
{ internalType: 'uint256', name: 'assets', type: 'uint256' },
{ internalType: 'uint256', name: 'shares', type: 'uint256' },
{ internalType: 'uint256', name: 'minSharePriceE27', type: 'uint256' },
{ internalType: 'address', name: 'receiver', type: 'address' },
],
outputs: [],
},
{
type: 'function',
stateMutability: 'nonpayable',
name: 'morphoRepay',
inputs: [
marketParamsTuple,
{ internalType: 'uint256', name: 'assets', type: 'uint256' },
{ internalType: 'uint256', name: 'shares', type: 'uint256' },
{ internalType: 'uint256', name: 'maxSharePriceE27', type: 'uint256' },
{ internalType: 'address', name: 'onBehalf', type: 'address' },
{ internalType: 'bytes', name: 'data', type: 'bytes' },
],
outputs: [],
},
{
type: 'function',
stateMutability: 'nonpayable',
name: 'morphoWithdrawCollateral',
inputs: [
marketParamsTuple,
{ internalType: 'uint256', name: 'assets', type: 'uint256' },
{ internalType: 'address', name: 'receiver', type: 'address' },
],
outputs: [],
},
{
type: 'function',
stateMutability: 'nonpayable',
name: 'morphoFlashLoan',
inputs: [
{ internalType: 'address', name: 'token', type: 'address' },
{ internalType: 'uint256', name: 'assets', type: 'uint256' },
{ internalType: 'bytes', name: 'data', type: 'bytes' },
],
outputs: [],
},
] as const satisfies Abi;
31 changes: 31 additions & 0 deletions src/abis/paraswapAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Abi } from 'viem';

/**
* Minimal ParaswapAdapter ABI for Bundler3 swap legs.
*/
export const paraswapAdapterAbi = [
{
type: 'function',
stateMutability: 'nonpayable',
name: 'sell',
inputs: [
{ internalType: 'address', name: 'augustus', type: 'address' },
{ internalType: 'bytes', name: 'callData', type: 'bytes' },
{ internalType: 'address', name: 'srcToken', type: 'address' },
{ internalType: 'address', name: 'destToken', type: 'address' },
{ internalType: 'bool', name: 'sellEntireBalance', type: 'bool' },
{
components: [
{ internalType: 'uint256', name: 'exactAmount', type: 'uint256' },
{ internalType: 'uint256', name: 'limitAmount', type: 'uint256' },
{ internalType: 'uint256', name: 'quotedAmount', type: 'uint256' },
],
internalType: 'struct Offsets',
name: 'offsets',
type: 'tuple',
},
{ internalType: 'address', name: 'receiver', type: 'address' },
],
outputs: [],
},
] as const satisfies Abi;
4 changes: 2 additions & 2 deletions src/features/markets/components/column-visibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export const DEFAULT_COLUMN_VISIBILITY: ColumnVisibility = {
totalBorrow: true,
liquidity: false,
supplyAPY: true,
borrowAPY: false,
rateAtTarget: false,
borrowAPY: true,
rateAtTarget: true,
trustedBy: false,
utilizationRate: false,
dailySupplyAPY: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ export function MarketsTableWithSameLoanAsset({
[SortColumn.COLLATSYMBOL]: 'collateralAsset.symbol',
[SortColumn.Supply]: 'state.supplyAssetsUsd',
[SortColumn.APY]: 'state.supplyApy',
[SortColumn.Liquidity]: 'state.liquidityAssets',
[SortColumn.Liquidity]: 'state.liquidityAssetsUsd',
[SortColumn.Borrow]: 'state.borrowAssetsUsd',
[SortColumn.BorrowAPY]: 'state.borrowApy',
[SortColumn.RateAtTarget]: 'state.apyAtTarget',
Expand Down
4 changes: 2 additions & 2 deletions src/features/markets/markets-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ export default function Markets() {
<MarketsTable
currentPage={currentPage}
setCurrentPage={setCurrentPage}
className={effectiveTableViewMode === 'compact' ? 'w-full' : undefined}
tableClassName={effectiveTableViewMode === 'compact' ? 'w-full min-w-full' : undefined}
className={effectiveTableViewMode === 'compact' ? 'w-full' : 'w-fit'}
tableClassName={effectiveTableViewMode === 'compact' ? 'w-full min-w-full' : 'w-fit'}
onRefresh={handleRefresh}
isMobile={isMobile}
/>
Expand Down
14 changes: 2 additions & 12 deletions src/features/swap/components/SwapModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -331,23 +331,13 @@ export function SwapModal({ isOpen, onClose, defaultTargetToken }: SwapModalProp
if (!quote || !sourceToken || !targetToken || error || !chainsMatch) return null;

if (isRateInverted) {
const inverseRate = computeUnitRatePreviewAmount(
quote.buyAmount,
targetToken.decimals,
quote.sellAmount,
sourceToken.decimals,
);
const inverseRate = computeUnitRatePreviewAmount(quote.buyAmount, targetToken.decimals, quote.sellAmount, sourceToken.decimals);
if (!inverseRate) return null;
const inverseRatePreview = formatTokenAmountPreview(inverseRate, RATE_PREVIEW_DECIMALS).compact;
return `1 ${targetToken.symbol} ≈ ${inverseRatePreview} ${sourceToken.symbol}`;
}

const forwardRate = computeUnitRatePreviewAmount(
quote.sellAmount,
sourceToken.decimals,
quote.buyAmount,
targetToken.decimals,
);
const forwardRate = computeUnitRatePreviewAmount(quote.sellAmount, sourceToken.decimals, quote.buyAmount, targetToken.decimals);
if (!forwardRate) return null;
const forwardRatePreview = formatTokenAmountPreview(forwardRate, RATE_PREVIEW_DECIMALS).compact;
return `1 ${sourceToken.symbol} ≈ ${forwardRatePreview} ${targetToken.symbol}`;
Expand Down
55 changes: 55 additions & 0 deletions src/hooks/leverage/bundler3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { type Address, encodeAbiParameters } from 'viem';

const PARASWAP_SWAP_EXACT_AMOUNT_IN_SELECTOR = '0xe3ead59e';
const PARASWAP_SELL_EXACT_AMOUNT_OFFSET = 100n;
const PARASWAP_SELL_MIN_DEST_AMOUNT_OFFSET = 132n;
const PARASWAP_SELL_QUOTED_DEST_AMOUNT_OFFSET = 164n;

export type Bundler3Call = {
to: Address;
data: `0x${string}`;
value: bigint;
skipRevert: boolean;
callbackHash: `0x${string}`;
};

const BUNDLER3_CALLS_ABI_PARAMS = [
{
type: 'tuple[]',
components: [
{ type: 'address', name: 'to' },
{ type: 'bytes', name: 'data' },
{ type: 'uint256', name: 'value' },
{ type: 'bool', name: 'skipRevert' },
{ type: 'bytes32', name: 'callbackHash' },
],
},
] as const;

export const encodeBundler3Calls = (bundle: Bundler3Call[]): `0x${string}` => {
return encodeAbiParameters(BUNDLER3_CALLS_ABI_PARAMS, [bundle]);
};

export const getParaswapSellOffsets = (augustusCallData: `0x${string}`) => {
const selector = augustusCallData.slice(0, 10).toLowerCase();
if (selector !== PARASWAP_SWAP_EXACT_AMOUNT_IN_SELECTOR) {
throw new Error('Unsupported Velora swap method for Paraswap adapter route.');
}

return {
exactAmount: PARASWAP_SELL_EXACT_AMOUNT_OFFSET,
limitAmount: PARASWAP_SELL_MIN_DEST_AMOUNT_OFFSET,
quotedAmount: PARASWAP_SELL_QUOTED_DEST_AMOUNT_OFFSET,
} as const;
};

export const readCalldataUint256 = (callData: `0x${string}`, offset: bigint): bigint => {
const byteOffset = Number(offset);
const start = 2 + byteOffset * 2;
const end = start + 64;
if (callData.length < end) {
throw new Error('Invalid Paraswap calldata for swap-backed route.');
}

return BigInt(`0x${callData.slice(start, end)}`);
};
Loading