Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
33271fb
core: refactor helper to include fund wallet when doing a supply
juangm Dec 18, 2025
a178888
test: userPosition math checks v1
juangm Dec 18, 2025
d372e31
Merge branch 'main' into test/math-check-user-position-summary
juangm Dec 18, 2025
b3011e6
fix: rename test scenario
juangm Dec 18, 2025
b45b497
fix: add changeset
juangm Dec 18, 2025
9069110
Merge branch 'main' into test/math-check-user-position-summary
juangm Dec 18, 2025
05c6da4
feat: add helper to call getAcccountData in the spoke contract
juangm Dec 18, 2025
48daa14
test: add healthFactor math calculations
juangm Dec 18, 2025
4b0981e
test: check netCollateral and averageCollateralFactor
juangm Dec 18, 2025
202a5f1
Merge branch 'main' into test/math-check-user-position-summary
juangm Dec 19, 2025
de40ff1
Merge branch 'main' into test/math-check-user-position-summary
juangm Dec 19, 2025
5f93a96
Merge branch 'main' into test/math-check-user-position-summary
juangm Dec 19, 2025
6930de7
Merge branch 'main' into test/math-check-user-position-summary
juangm Dec 21, 2025
febf923
refactor: remove autoFund parameter and always auto-fund in test helpers
juangm Dec 21, 2025
0d56477
test: make precision parameter required in toBeBigDecimalCloseTo matcher
juangm Dec 21, 2025
1b6d4e5
refactor: inline recreateUserPositionInOneSpoke helper into userPosit…
juangm Dec 21, 2025
ecc9895
fix: remove not needed imports
juangm Dec 21, 2025
4a95adc
Merge branch 'main' into test/math-check-user-position-summary
juangm Jan 5, 2026
44ad3f1
fix: use a publicClient instead of exporting devnet
juangm Jan 5, 2026
572e901
test: check riskPremium value
juangm Jan 5, 2026
ade5c25
Merge branch 'main' into test/math-check-user-position-summary
juangm Jan 5, 2026
d8131cf
fix: typo and rename test description
juangm Jan 5, 2026
11530da
Merge branch 'main' into test/math-check-user-position-summary
juangm Jan 6, 2026
921d93a
Merge branch 'main' into test/math-check-user-position-summary
juangm Jan 6, 2026
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
2 changes: 2 additions & 0 deletions .changeset/mighty-phones-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
10 changes: 10 additions & 0 deletions packages/client/src/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export const ETHEREUM_WSTETH_ADDRESS = evmAddress(
export const ETHEREUM_1INCH_ADDRESS = evmAddress(
'0x111111111117dC0aa78b770fA6A738034120C302',
);
export const ETHEREUM_AAVE_ADDRESS = evmAddress(
'0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9',
);

// Spoke addresses and ids
export const ETHEREUM_SPOKE_CORE_ADDRESS = evmAddress(
Expand Down Expand Up @@ -123,6 +126,13 @@ const devnetChain = await chain(client, { chainId: ETHEREUM_FORK_ID })
() => never('No devnet chain found'),
);

export async function createForkPublicClient() {
return createPublicClient({
chain: devnetChain,
transport: http(ETHEREUM_FORK_RPC_URL),
});
}

export async function createNewWallet(
privateKey?: `0x${string}`,
): Promise<WalletClient<Transport, Chain, Account>> {
Expand Down
18 changes: 7 additions & 11 deletions packages/spec/borrow/business.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,12 @@ describe('Borrowing Assets on Aave V4', () => {
beforeAll(async () => {
const amountToSupply = bigDecimal('100');

const setup = await fundErc20Address(evmAddress(user.account.address), {
address: ETHEREUM_USDC_ADDRESS,
const setup = await findReserveAndSupply(client, user, {
token: ETHEREUM_USDC_ADDRESS,
spoke: ETHEREUM_SPOKE_CORE_ID,
amount: amountToSupply,
decimals: 6,
}).andThen(() =>
findReserveAndSupply(client, user, {
token: ETHEREUM_USDC_ADDRESS,
spoke: ETHEREUM_SPOKE_CORE_ID,
amount: amountToSupply,
asCollateral: true,
}),
);
asCollateral: true,
});

assertOk(setup);
});
Expand Down Expand Up @@ -105,6 +99,7 @@ describe('Borrowing Assets on Aave V4', () => {
assertSingleElementArray(result.value);
expect(result.value[0].debt.amount.value).toBeBigDecimalCloseTo(
amountToBorrow,
2,
);
expect(result.value[0].debt.token.isWrappedNativeToken).toBe(false);
});
Expand Down Expand Up @@ -189,6 +184,7 @@ describe('Borrowing Assets on Aave V4', () => {
);
expect(balanceAfter).toBeBigDecimalCloseTo(
balanceBefore.add(amountToBorrow),
2,
);

expect(position?.debt.amount.value).toBeBigDecimalCloseTo(
Expand Down
19 changes: 7 additions & 12 deletions packages/spec/borrow/multipleBorrows.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
createNewWallet,
ETHEREUM_SPOKE_CORE_ID,
ETHEREUM_USDC_ADDRESS,
fundErc20Address,
} from '@aave/client/testing';
import { sendWith } from '@aave/client/viem';
import { beforeAll, describe, expect, it } from 'vitest';
Expand All @@ -21,18 +20,12 @@ describe('Borrowing from Multiple Reserves on Aave V4', () => {
beforeAll(async () => {
const amountToSupply = bigDecimal('100');

const setup = await fundErc20Address(evmAddress(user.account.address), {
address: ETHEREUM_USDC_ADDRESS,
const setup = await findReserveAndSupply(client, user, {
token: ETHEREUM_USDC_ADDRESS,
spoke: ETHEREUM_SPOKE_CORE_ID,
amount: amountToSupply,
decimals: 6,
}).andThen(() =>
findReserveAndSupply(client, user, {
token: ETHEREUM_USDC_ADDRESS,
spoke: ETHEREUM_SPOKE_CORE_ID,
amount: amountToSupply,
asCollateral: true,
}),
);
asCollateral: true,
});

assertOk(setup);
}, 120_000);
Expand Down Expand Up @@ -107,6 +100,7 @@ describe('Borrowing from Multiple Reserves on Aave V4', () => {
reservesToBorrow.value[0]!.userState!.borrowable.amount.value.times(
0.1,
),
2,
);

// Verify second borrow position (USDS)
Expand All @@ -127,6 +121,7 @@ describe('Borrowing from Multiple Reserves on Aave V4', () => {
reservesToBorrow.value[1]!.userState!.borrowable.amount.value.times(
0.1,
),
2,
);
});
});
Expand Down
73 changes: 63 additions & 10 deletions packages/spec/helpers/supplyBorrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,49 @@ export function findReserveAndSupply(
spoke,
asCollateral,
}: {
token: EvmAddress;
amount: BigDecimal;
token?: EvmAddress;
amount?: BigDecimal;
spoke?: SpokeId;
asCollateral?: boolean;
},
): ResultAsync<Reserve, Error> {
): ResultAsync<{ reserveInfo: Reserve; amountSupplied: BigDecimal }, Error> {
return findReservesToSupply(client, user, {
token: token,
spoke: spoke,
asCollateral: asCollateral,
}).andThen((reserves) =>
supplyToReserve(client, user, {
reserve: reserves[0].id,
amount: { erc20: { value: amount } },
sender: evmAddress(user.account.address),
}).map(() => reserves[0]),
);
}).andThen((reserves) => {
return fundErc20Address(evmAddress(user.account.address), {
address: token ?? reserves[0]!.asset.underlying.address,
amount:
amount ??
reserves[0]!.supplyCap
.minus(reserves[0]!.summary.supplied.amount.value)
.div(100000),
decimals: reserves[0]!.asset.underlying.info.decimals,
}).andThen(() =>
supplyToReserve(client, user, {
reserve: reserves[0]!.id,
amount: {
erc20: {
value:
amount ??
reserves[0]!.supplyCap
.minus(reserves[0]!.summary.supplied.amount.value)
.div(100000),
},
},
sender: evmAddress(user.account.address),
enableCollateral: asCollateral ?? true,
}).map(() => ({
reserveInfo: reserves[0]!,
amountSupplied:
amount ??
reserves[0]!.supplyCap
.minus(reserves[0]!.summary.supplied.amount.value)
.div(100000),
})),
);
});
}

export function supplyAndBorrow(
Expand Down Expand Up @@ -141,6 +167,33 @@ export function supplyAndBorrow(
);
}

export function borrowFromRandomReserve(
client: AaveClient,
user: WalletClient<Transport, Chain, Account>,
params: {
spoke?: SpokeId;
token?: EvmAddress;
ratioToBorrow?: number;
},
): ResultAsync<Reserve, Error> {
return findReservesToBorrow(client, user, {
spoke: params.spoke,
token: params.token,
}).andThen((reserves) => {
return borrowFromReserve(client, user, {
reserve: reserves[0].id,
amount: {
erc20: {
value: reserves[0].userState!.borrowable.amount.value.times(
params.ratioToBorrow ?? 0.1,
),
},
},
sender: evmAddress(user.account.address),
}).map(() => reserves[0]);
});
}

export function supplyAndBorrowNativeToken(
client: AaveClient,
user: WalletClient<Transport, Chain, Account>,
Expand Down
16 changes: 5 additions & 11 deletions packages/spec/positions/business.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,12 @@ describe('Health Factor Scenarios on Aave V4', () => {
beforeAll(async () => {
const amountToSupply = bigDecimal('100');

const setup = await fundErc20Address(evmAddress(user.account.address), {
address: ETHEREUM_USDC_ADDRESS,
const setup = await findReserveAndSupply(client, user, {
token: ETHEREUM_USDC_ADDRESS,
spoke: ETHEREUM_SPOKE_CORE_ID,
asCollateral: true,
amount: amountToSupply,
decimals: 6,
}).andThen(() =>
findReserveAndSupply(client, user, {
token: ETHEREUM_USDC_ADDRESS,
spoke: ETHEREUM_SPOKE_CORE_ID,
asCollateral: true,
amount: amountToSupply,
}),
);
});

assertOk(setup);
});
Expand Down
39 changes: 1 addition & 38 deletions packages/spec/positions/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,21 @@ import {
} from '@aave/client/actions';
import {
ETHEREUM_FORK_ID,
ETHEREUM_GHO_ADDRESS,
ETHEREUM_SPOKE_CORE_ID,
ETHEREUM_SPOKE_ETHENA_ID,
ETHEREUM_WETH_ADDRESS,
ETHEREUM_WSTETH_ADDRESS,
fundErc20Address,
} from '@aave/client/testing';
import { sendWith } from '@aave/client/viem';
import type { Account, Chain, Transport, WalletClient } from 'viem';

import {
findReservesToBorrow,
findReservesToSupply,
} from '../helpers/reserves';
import {
borrowFromReserve,
findReserveAndSupply,
supplyAndBorrowNativeToken,
supplyToReserve,
} from '../helpers/supplyBorrow';
import {
Expand Down Expand Up @@ -203,41 +201,6 @@ export const recreateUserActivities = async (
}
};

export const recreateUserSummary = async (
client: AaveClient,
user: WalletClient<Transport, Chain, Account>,
) => {
const setup = await fundErc20Address(evmAddress(user.account.address), {
address: ETHEREUM_WETH_ADDRESS,
amount: bigDecimal('0.5'),
})
.andThen(() =>
fundErc20Address(evmAddress(user.account.address), {
address: ETHEREUM_WSTETH_ADDRESS,
amount: bigDecimal('0.5'),
}),
)
.andThen(() =>
fundErc20Address(evmAddress(user.account.address), {
address: ETHEREUM_GHO_ADDRESS,
amount: bigDecimal('100'),
}),
)
.andThen(() =>
findReserveAndSupply(client, user, {
token: ETHEREUM_GHO_ADDRESS,
amount: bigDecimal('100'),
}),
)
.andThen(() =>
supplyAndBorrowNativeToken(client, user, {
spoke: ETHEREUM_SPOKE_CORE_ID,
ratioToBorrow: 0.4,
}),
);
assertOk(setup);
};

export const recreateUserBorrows = async (
client: AaveClient,
user: WalletClient<Transport, Chain, Account>,
Expand Down
80 changes: 80 additions & 0 deletions packages/spec/positions/math/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { type BigDecimal, bigDecimal } from '@aave/client';
import { createForkPublicClient } from '@aave/client/testing';
import type { Address } from 'viem';

// Constants
const WAD = 10n ** 18n; // 1e18 = 1.0 in WAD format
const userAccountDataABI = [
{
inputs: [{ name: 'user', type: 'address' }],
name: 'getUserAccountData',
outputs: [
{
components: [
{ name: 'riskPremium', type: 'uint256' },
{ name: 'avgCollateralFactor', type: 'uint256' },
{ name: 'healthFactor', type: 'uint256' },
{ name: 'totalCollateralValue', type: 'uint256' },
{ name: 'totalDebtValue', type: 'uint256' },
{ name: 'activeCollateralCount', type: 'uint256' },
{ name: 'borrowedCount', type: 'uint256' },
],
name: '',
type: 'tuple',
},
],
stateMutability: 'view',
type: 'function',
},
] as const;

export interface UserAccountData {
riskPremium: BigDecimal;
avgCollateralFactor: BigDecimal;
healthFactor: BigDecimal;
totalCollateralValue: BigDecimal;
totalDebtValue: BigDecimal;
activeCollateralCount: number;
borrowedCount: number;
}

function formatWAD(value: bigint): BigDecimal {
return bigDecimal(value).div(WAD);
}

function formatUSD(value: bigint): BigDecimal {
return bigDecimal(value).div(bigDecimal('1e26'));
}

function formatBPS(value: bigint): BigDecimal {
return bigDecimal(value).div(bigDecimal('100'));
}

/**
* Get user account data from the Spoke contract
* @param address - The user address to query
* @param spoke - The Spoke contract address
* @returns User account data
*/
export async function getAccountData(
address: Address,
spoke: Address,
): Promise<UserAccountData> {
const forkPublicClient = await createForkPublicClient();
const result = await forkPublicClient.readContract({
address: spoke,
abi: userAccountDataABI,
functionName: 'getUserAccountData',
args: [address],
});

return {
riskPremium: formatBPS(result.riskPremium),
avgCollateralFactor: formatWAD(result.avgCollateralFactor),
healthFactor: formatWAD(result.healthFactor),
totalCollateralValue: formatUSD(result.totalCollateralValue),
totalDebtValue: formatUSD(result.totalDebtValue),
activeCollateralCount: Number(result.activeCollateralCount),
borrowedCount: Number(result.borrowedCount),
};
}
Loading
Loading