Borrow Assets on Aave V4
Borrowing assets from Aave V4 allows you to:
- Access liquidity without selling your assets
- Leverage your positions with borrowed capital
- Repay borrowed assets at any time to close your position
Borrowing lowers your health factor. Keep enough collateral and watch the health factor, especially in volatile markets, to avoid liquidation.
Borrow Assets
Borrowing on Aave V4 takes place at the spoke level, and users can only borrow against collateral supplied to that same spoke.
Borrowing can be broken down into the following steps:
- Identify the Reserve to borrow from
- Preview the impact of the borrow operation
- Borrow the assets
Identify the Reserve
The first step is to filter the borrow reserves on the same spoke as the collateral to those marked with can.
const reserves: Reserve[] = [
{
id: "SGVsbG8h",
onChainId: "42",
canBorrow: true,
settings: {
borrowCap: {
amount: { value: BigDecimal(1000000000.0) }, // 1B USDC
// …
},
// …
},
status: {
frozen: false,
paused: false,
},
spoke: {
address: "0x123…",
// …
},
// …
},
{
id: "V29ybGQh",
onChainId: "43",
canBorrow: false, // cannot borrow from this reserve
settings: {
borrowCap: {
amount: { value: BigDecimal(500000000.0) }, // 500M DAI
// …
},
// …
},
status: {
frozen: true, // reserve is frozen
paused: false,
},
spoke: {
address: "0x123…",
// …
},
// …
},
// …
];
can means the reserve is active: not frozen, not paused, and below its
borrow cap.
From the remaining borrowable reserves, select the one that:
- Best matches the desired token with a sufficient borrowable amount. This already accounts for the combined collateral factors (LTV ratios) of the position’s collateral
- Offers the lowest borrow APY: the effective borrow APY combines the reserve’s borrow APY with the Risk Premium tied to the position’s collateral
const reserve: Reserve = {
id: "SGVsbG8h",
onChainId: "42",
canBorrow: true,
chain: {
chainId: 1,
name: "Ethereum",
},
spoke: {
address: "0x123…",
// …
},
asset: {
underlying: {
address: "0xa0b86a33e6e2ad05ad6c9ac3b6e5e5f6e7b6c1b2", // USDC
},
// …
},
userState: {
borrowApy: {
normalized: BigDecimal(3.73), // 3.73% APY
// …
},
borrowable: {
amount: {
value: BigDecimal(10000.0), // 10,000 USDC
},
// …
},
// …
},
};
Include a user address when fetching reserve data, or user will be
null.
Preview Borrow with AaveKit
Preview the impact of a borrow operation before committing to it.
Use the use hook (or the imperative use variant) in React, the preview action in TypeScript, or the preview query in GraphQL to preview the impact of the borrow operation on the user’s position.
import { type BorrowRequest, usePreview } from "@aave/react";
function BorrowPreview({ request }: { request: BorrowRequest }) {
const { data, error, loading } = usePreview({
action: {
borrow: request,
},
});
if (loading) return <div>Loading…</div>;
if (error) return <div>Error: {error.message}</div>;
// data: PreviewUserPosition
return (
<div>
<h3>Health Factor:</h3>
<p>From: {data.healthFactor.current?.toFixed(2) ?? "N/A"}</p>
<p>To: {data.healthFactor.after?.toFixed(2) ?? "N/A"}</p>
<h3>User Risk Premium:</h3>
<p>From: {data.riskPremium.current.normalized.toFixed(2)}%</p>
<p>To: {data.riskPremium.after.normalized.toFixed(2)}%</p>
<h3>Net APY:</h3>
<p>From: {data.netApy.current.normalized.toFixed(2)}%</p>
<p>To: {data.netApy.after.normalized.toFixed(2)}%</p>
<h3>Net Collateral:</h3>
<p>From: {data.netCollateral.current.value.toDisplayString(2)}</p>
<p>To: {data.netCollateral.after.value.toDisplayString(2)}</p>
</div>
);
}
import { type BorrowRequest, usePreview } from "@aave/react";
function BorrowPreview({ request }: { request: BorrowRequest }) {
const { data, error, loading } = usePreview({
action: {
borrow: request,
},
suspense: true,
});
// data: PreviewUserPosition
return (
<div>
<h3>Health Factor:</h3>
<p>From: {data.healthFactor.current?.toFixed(2) ?? "N/A"}</p>
<p>To: {data.healthFactor.after?.toFixed(2) ?? "N/A"}</p>
<h3>User Risk Premium:</h3>
<p>From: {data.riskPremium.current.normalized.toFixed(2)}%</p>
<p>To: {data.riskPremium.after.normalized.toFixed(2)}%</p>
<h3>Net APY:</h3>
<p>From: {data.netApy.current.normalized.toFixed(2)}%</p>
<p>To: {data.netApy.after.normalized.toFixed(2)}%</p>
<h3>Net Collateral:</h3>
<p>From: {data.netCollateral.current.value.toDisplayString(2)}</p>
<p>To: {data.netCollateral.after.value.toDisplayString(2)}</p>
</div>
);
}
import { type BorrowRequest, usePreviewAction } from "@aave/react";
const [preview, { called, data, error, loading }] = usePreviewAction();
const handler = async (request: BorrowRequest) => {
const result = await preview({
action: {
borrow: request,
},
});
if (result.isOk()) {
console.log(result.value); // PreviewUserPosition
} else {
console.error(result.error);
}
};
import { preview } from "@aave/client/actions";
import { bigDecimal, evmAddress } from "@aave/client";
const result = await preview(client, {
action: {
borrow: {
sender: evmAddress("0x789…"), // User's address
reserve: reserve.id,
amount: {
erc20: {
value: bigDecimal(1000), // 1000 USDC
},
},
},
},
});
if (result.isOk()) {
console.log(result.value); // PreviewUserPosition
} else {
console.error(result.error);
}
query ($request: PreviewRequest!, $currency: Currency = USD) {
preview(request: $request) {
__typename
id
healthFactor {
...HealthFactorResult
}
netApy {
...PercentNumberVariation
}
riskPremium {
...PercentNumberVariation
}
netCollateral(currency: $currency) {
...ExchangeAmountVariation
}
netBalance(currency: $currency) {
...ExchangeAmountVariation
}
projectedEarnings {
...ExchangeAmountVariation
}
maxBorrowingPower {
...ExchangeAmountVariation
}
remainingBorrowingPower {
...ExchangeAmountVariation
}
reserveRates {
supplyApy {
...PercentNumberVariation
}
borrowApy {
...PercentNumberVariation
}
}
otherConditions {
...UserPositionConditionVariation
}
}
}
fragment HealthFactorResult on HealthFactorResult {
__typename
... on HealthFactorVariation {
...HealthFactorVariation
}
... on HealthFactorError {
...HealthFactorError
}
}
fragment HealthFactorVariation on HealthFactorVariation {
__typename
current
after
}
fragment HealthFactorError on HealthFactorError {
__typename
reason
current
after
}
fragment UserPositionConditionVariation on UserPositionConditionVariation {
__typename
... on CollateralFactorVariation {
reserveId
token {
...Erc20Token
}
current {
...PercentNumber
}
after {
...PercentNumber
}
}
... on LiquidationFeeVariation {
reserveId
token {
...Erc20Token
}
current {
...PercentNumber
}
after {
...PercentNumber
}
}
... on MaxLiquidationBonusVariation {
reserveId
token {
...Erc20Token
}
current {
...PercentNumber
}
after {
...PercentNumber
}
}
}
fragment PercentNumberVariation on PercentNumberVariation {
__typename
current {
...PercentNumber
}
after {
...PercentNumber
}
}
fragment ExchangeAmountVariation on ExchangeAmountVariation {
__typename
current {
...ExchangeAmount
}
after {
...ExchangeAmount
}
}
- React: The
Borrowcan be as follows.Request - GraphQL: The
Previewcan be as follows.Request
const request: BorrowRequest = {
sender: evmAddress("0x789…"), // User's address
reserve: reserve.id,
amount: {
erc20: {
value: bigDecimal(1000), // 1000 USDC
},
},
};
{
"request": {
"action": {
"borrow": {
"sender": "0x789…",
"reserve": "SGVsbG8h",
"amount": {
"erc20": {
"value": "1000"
}
}
}
}
}
}
The Preview shows the impact of the borrow operation by comparing current and after states, with the table below outlining key fields and how to interpret them.
| Field | Impact |
|---|---|
health | Higher is better ( null if not applicable) |
risk | Lower is better |
net | Higher is better |
net | Higher is better |
net | Updated balance |
max | Maximum borrowing power |
remaining | Remaining borrowing power |
reserve | Supply APY impact on the reserve |
reserve | Borrow APY impact on the reserve |
other | Dynamic config changes |
Borrowing assets updates the Dynamic Config and User Risk Premium of the user position.
The other field is an array of objects describing the resulting dynamic config changes.
Collateral– Collateral factor changeFactor Variation Liquidation– Liquidation fee changeFee Variation Max– Maximum liquidation bonus changeLiquidation Bonus Variation
You can also specify a different currency to return fiat amounts in.
import { Currency } from "@aave/react";
const { data, error, loading } = usePreview({
action: {
borrow: request,
},
currency: Currency.Eur,
});
import { Currency } from "@aave/react";
const [preview, { called, data, error, loading }] = usePreviewAction({
currency: Currency.Eur,
});
import { Currency } from "@aave/client";
const result = await preview(
client,
{
action: {
borrow: {
/* … */
},
},
},
{
currency: Currency.Eur,
},
);
{
"request": {
"action": {
"borrow": {
// …
}
}
},
"currency": "EUR"
}
Preview Borrow with Solidity
Start with the spoke address and on-chain reserve ID identified:
address spokeAddress = 0x123…;
uint256 reserveId = 42;
And an amount to borrow:
uint256 amountToBorrow = 42e6; // 42 USDC
Follow the steps below to preview the impact of a borrow operation on the user’s position.
1. Get Current Account Data
First, retrieve the user’s current account data from the Spoke contract.
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
ISpoke spoke = ISpoke(spokeAddress);
ISpoke.UserAccountData memory currentData = spoke.getUserAccountData(user);
2. Calculate New Debt Value
Then, retrieve the reserve data and asset price from the oracle to calculate the USD value of the new borrow, and add it to the current total debt.
address oracleAddress = spoke.ORACLE();
ISpoke.Reserve memory reserve = spoke.getReserve(reserveId);
uint256 assetPrice = IAaveOracle(oracleAddress).getReservePrice(reserveId);
uint256 borrowValue = amountToBorrow * assetPrice * 10 ** (18 - reserve.decimals);
uint256 newTotalDebtValue = currentData.totalDebtValueRay / 1e27 + borrowValue;
3. Compute New Health Factor
Finally, calculate the new health factor by incorporating the new debt value.
uint256 newHealthFactor = (currentData.totalCollateralValue * currentData.avgCollateralFactor) / newTotalDebtValue; // WAD, 1e18 = 1.00
Complete Preview Example
Here’s a complete Solidity example that calculates the health factor before a borrow operation:
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
import { IAaveOracle } from "aave-v4/src/spoke/interfaces/IAaveOracle.sol";
function previewBorrowHealthFactor(
address spokeAddress,
uint256 reserveId,
uint256 amountToBorrow,
address user
) external view returns (uint256) {
ISpoke spoke = ISpoke(spokeAddress);
IAaveOracle oracle = IAaveOracle(spoke.ORACLE());
// Step 1: Get Current Account Data
ISpoke.UserAccountData memory currentData = spoke.getUserAccountData(user);
// Step 2: Calculate New Debt Value
ISpoke.Reserve memory reserve = spoke.getReserve(reserveId);
uint256 assetPrice = oracle.getReservePrice(reserveId);
uint256 borrowValue = amountToBorrow * assetPrice * 10 ** (18 - reserve.decimals);
uint256 newTotalDebtValue = currentData.totalDebtValueRay / 1e27 + borrowValue;
// Step 3: Compute New Health Factor
return (currentData.totalCollateralValue * currentData.avgCollateralFactor) / newTotalDebtValue; // WAD, 1e18 = 1.00
}
Step-by-Step with AaveKit
Borrow assets from the selected reserve.
Borrowing increases risk and updates the Dynamic Config, which updates the position’s User Risk Premium. See User Position Conditions.
To borrow assets from an Aave reserve with AaveKit, follow these steps.
1. Configure Wallet Integration
In React, first instantiate the use hook for the wallet library of your choice.
import { useWalletClient } from "wagmi";
import { useSendTransaction } from "@aave/react/viem";
// …
const { data: wallet } = useWalletClient();
const [sendTransaction] = useSendTransaction(wallet);
2. Define the Borrow Flow
Then, in React, use the use hook to prepare the borrow operation.
import { useBorrow } from "@aave/react";
const [borrow, { loading, error }] = useBorrow((plan) => {
switch (plan.__typename) {
case "TransactionRequest":
return sendTransaction(plan);
case "PreContractActionRequired":
return sendTransaction(plan.transaction);
}
});
import { useBorrow } from "@aave/react";
const [status, setStatus] = useState("");
const [borrow, { loading, error }] = useBorrow((plan) => {
switch (plan.__typename) {
case "TransactionRequest":
setStatus("Please sign the Borrow transaction in your wallet");
return sendTransaction(plan).andTee(() => setStatus("Borrowing…"));
case "PreContractActionRequired":
setStatus("Please sign the Pre-Contract Action in your wallet");
return sendTransaction(plan.transaction).andTee(() =>
setStatus("Processing…"),
);
}
});
import { useBorrow } from "@aave/react";
const [borrow, { loading, error }] = useBorrow((plan, { cancel }) => {
if (window.confirm("Are you sure you want to continue?") === false) {
return cancel("User cancelled the operation");
}
switch (plan.__typename) {
case "TransactionRequest":
return sendTransaction(plan);
case "PreContractActionRequired":
return sendTransaction(plan.transaction);
}
});
3. Execute the Borrow Operation
Then, execute the desired borrow operation.
- TypeScript: Use the
borrowaction to prepare the borrow execution plan. - GraphQL: Use the
borrowquery to prepare the borrow execution plan.
import { bigDecimal, evmAddress } from "@aave/react";
const execute = async () => {
const result = await borrow({
sender: evmAddress(wallet.account.address), // User's address
reserve: reserve.id,
amount: {
erc20: {
value: bigDecimal(100), // 100 USDC
},
},
});
// …
};
import { borrow } from "@aave/client/actions";
import { bigDecimal, evmAddress } from "@aave/client";
import { client } from "./client";
const result = await borrow(client, {
sender: evmAddress(wallet.account.address), // User's address
reserve: reserve.id,
amount: {
erc20: {
value: bigDecimal(100), // 100 USDC
},
},
});
// …
import { AaveClient } from "@aave/client";
export const client = AaveClient.create();
query ($request: BorrowRequest!) {
borrow(request: $request) {
__typename
... on TransactionRequest {
...TransactionRequest
}
... on PreContractActionRequired {
reason
transaction {
...TransactionRequest
}
originalTransaction {
...TransactionRequest
}
}
}
}
fragment TransactionRequest on TransactionRequest {
to
from
data
value
chainId
operations
}
In GraphQL, below is an example of a Borrow object.
{
"request": {
"sender": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"reserve": "SGVsbG8h",
"amount": {
"erc20": {
"value": "100"
}
}
}
}
4. Process the Execution Plan
In TypeScript, use send for the wallet library of your choice to send the transactions in the execution plan, and await indexing with client..
import { borrow } from "@aave/client/actions";
import { sendWith } from "@aave/client/viem";
import { client } from "./client";
import { wallet } from "./wallet";
const result = await borrow(client, {
// …
})
.andThen(sendWith(wallet))
.andThen(client.waitForTransaction);
import { createWalletClient, http, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
export const wallet = createWalletClient({
account: privateKeyToAccount(process.env.PRIVATE_KEY as Hex),
chain: mainnet,
transport: http(),
});
In GraphQL, if the response is a Transaction, you can send it directly using the wallet associated with the from address.
{
"data": {
"borrow": {
"__typename": "TransactionRequest",
"to": "0x94e7a5dcbe816e498b89ab752661904e2f56c485",
"from": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"data": "0xd6bda0c0…",
"value": "0",
"chainId": 1,
"operations": ["SPOKE_BORROW"]
}
}
}
If the response is a Pre, you must first send the transaction request. Once it succeeds, you can then send the original.
{
"data": {
"borrow": {
"__typename": "PreContractActionRequired",
"reason": "Authorize the native wrapping gateway to operate on behalf of the user",
"transaction": {
"to": "0x94e7a5dcbe816e498b89ab752661904e2f56c485",
"from": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"data": "0x095ea7b3000000000000000000000000878…",
"value": "0",
"chainId": 1,
"operations": null
},
"originalTransaction": {
"to": "0x94e7a5dcbe816e498b89ab752661904e2f56c485",
"from": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"data": "0xd6bda0c0…",
"value": "0",
"chainId": 1,
"operations": ["SPOKE_BORROW"]
}
}
}
}
5. Handle the Result
Finally, in React and TypeScript, handle the result.
const execute = async () => {
const result = await borrow(/* … */);
if (result.isErr()) {
switch (result.error.name) {
case "CancelError":
// The user cancelled the operation
return;
case "SigningError":
console.error(
`Failed to sign the transaction: ${result.error.message}`,
);
break;
case "TimeoutError":
console.error(`Transaction timed out: ${result.error.message}`);
break;
case "TransactionError":
console.error(`Transaction failed: ${result.error.message}`);
break;
case "ValidationError":
console.error(
"Insufficient balance:",
`required: ${result.error.cause.required.value.toDisplayString(2)}`,
`available: ${result.error.cause.available.value.toDisplayString(2)}`,
);
break;
case "UnexpectedError":
console.error(result.error.message);
break;
}
return;
}
console.log("Borrow successful with hash:", result.value.txHash);
};
if (result.isErr()) {
switch (result.error.name) {
case "CancelError":
// The user cancelled the operation
return;
case "SigningError":
console.error(`Failed to sign the transaction: ${result.error.message}`);
break;
case "TimeoutError":
console.error(`Transaction timed out: ${result.error.message}`);
break;
case "TransactionError":
console.error(`Transaction failed: ${result.error.message}`);
break;
case "ValidationError":
console.error(
"Insufficient balance:",
`required: ${result.error.cause.required.value.toDisplayString(2)}`,
`available: ${result.error.cause.available.value.toDisplayString(2)}`,
);
break;
case "UnexpectedError":
console.error(result.error.message);
break;
}
return;
}
console.log("Borrow successful with hash:", result.value.txHash);
Step-by-Step with Solidity
Use the same identifiers and amount to borrow used to preview the borrow operation:
address spokeAddress = 0x123…;
uint256 reserveId = 42;
uint256 amountToBorrow = 42e6; // 42 USDC
Complete the borrow process with the following steps:
1. Import the Spoke Interface
First, create an instance of the ISpoke interface.
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
ISpoke spoke = ISpoke(spokeAddress);
2. Borrow Assets
Then, call the borrow function on the Spoke contract, specifying the reserve ID, amount to borrow, and the on address (the position owner). The borrowed tokens are sent to the caller (msg.).
spoke.borrow(
reserveId,
amountToBorrow,
msg.sender
);
Complete Borrow Example
Here’s a complete Solidity example that executes the borrow operation:
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
function borrowFromReserve(
address spokeAddress,
uint256 reserveId,
uint256 amountToBorrow,
address onBehalfOf
) external {
// Borrow assets from reserve
ISpoke(spokeAddress).borrow(reserveId, amountToBorrow, onBehalfOf);
}
Advanced Usage
Network Fee
This experimental React hook only works with Viem or Wagmi integrations.
Estimate the network cost of any action using the same Preview you pass to the use hook.
For example:
import { type PreviewAction } from "@aave/react";
const action: PreviewAction = {
borrow: {
sender: evmAddress("0x789…"), // User's address
reserve: reserve.id,
amount: {
erc20: {
value: bigDecimal(1000), // 1000 USDC
},
},
},
};
Use the use hook to estimate both the network fee for the provided action and its fiat equivalent.
import { type PreviewAction, Currency } from "@aave/react";
import { useNetworkFee } from "@aave/react/viem";
function NetworkFee({ action }: { action: PreviewAction }) {
const {
data: fee,
loading,
error,
} = useNetworkFee({
query: { estimate: action },
currency: Currency.Eur,
});
if (loading) return <p>Loading fee…</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<p>
Network Fee: {fee.amount.value.toDisplayString(2)} {fee.token.info.symbol}
<span>
≈{fee.exchange.symbol}
{fee.exchange.value.toDisplayString(2)}
</span>
</p>
);
}
Native Tokens with AaveKit
When the Reserve’s underlying token is the wrapped version of the chain’s native token (e.g., WETH on Ethereum), you can borrow the asset as the chain’s native token using the Native Token Gateway.
Use the asset. flag to determine if the underlying token is a wrapped native token. The Native Token Gateway address is available from the chain details.
const reserve: Reserve = {
id: "SGVsbG8h",
onChainId: "42",
canBorrow: true,
canSupply: true,
canUseAsCollateral: true,
asset: {
underlying: {
address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
info: {
name: "Wrapped Ether",
symbol: "WETH",
decimals: 18,
// …
},
isWrappedNativeToken: true,
// …
},
// …
},
settings: {
borrowCap: {
amount: { value: BigDecimal(1000000000.0) }, // 1B WETH
// …
},
supplyCap: {
amount: { value: BigDecimal(2000000000.0) }, // 2B WETH
// …
},
// …
},
spoke: {
address: "0x123…",
// …
},
chain: {
chainId: 1,
name: "Ethereum",
nativeGateway: "0xabc…",
},
// …
};
Specify the amount in the amount field as a native value.
const execute = async () => {
const result = await borrow({
sender: evmAddress(wallet.account.address), // User's address
reserve: reserve.id,
amount: {
native: bigDecimal(1), // 1 ETH
},
});
// …
};
const result = await borrow(client, {
sender: evmAddress(wallet.account.address), // User's address
reserve: reserve.id,
amount: {
native: bigDecimal(1), // 1 ETH
},
});
// …
{
"request": {
"sender": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"reserve": "SGVsbG8h",
"amount": {
"native": "1" // 1 ETH
}
}
}
Native Tokens with Solidity
Start with the spoke address, reserve ID, and native gateway address identified:
address spokeAddress = 0x123…;
uint256 reserveId = 42;
address nativeGateway = 0xabc…;
Follow the steps below to borrow native tokens (e.g., ETH) from a wrapped native token reserve (e.g., WETH).
1. Approve the Gateway
First, approve the Native Token Gateway as your position manager. This is a one-time setup step.
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
ISpoke spoke = ISpoke(spokeAddress);
spoke.setUserPositionManager(nativeGateway, true);
2. Borrow Native Tokens
Then, call borrow on the gateway to borrow and unwrap the tokens.
import { INativeTokenGateway } from "aave-v4/src/position-manager/interfaces/INativeTokenGateway.sol";
INativeTokenGateway gateway = INativeTokenGateway(nativeGateway);
gateway.borrowNative(spokeAddress, reserveId, amountToBorrow);
The gateway calls spoke. on your behalf, unwraps the token (WETH → ETH), and sends the native token to the caller (msg.).
Complete Native Token Example
Here’s a complete Solidity example for borrowing native tokens. This helper acts on the contract’s own position. The gateway sends the native tokens to the contract, so it needs a receive() function. To borrow for a user’s wallet, the user calls set and the gateway directly.
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
import { INativeTokenGateway } from "aave-v4/src/position-manager/interfaces/INativeTokenGateway.sol";
function borrowNativeToken(
address spokeAddress,
address nativeGateway,
uint256 reserveId,
uint256 amountToBorrow
) external {
ISpoke spoke = ISpoke(spokeAddress);
// Step 1: Approve gateway as position manager (one-time setup)
if (!spoke.isPositionManager(address(this), nativeGateway)) {
spoke.setUserPositionManager(nativeGateway, true);
}
// Step 2: Borrow native tokens via gateway
INativeTokenGateway(nativeGateway).borrowNative(spokeAddress, reserveId, amountToBorrow);
}
receive() external payable {}