From 33271fb377e91f220ae6494ab74ec19de32ff564 Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Thu, 18 Dec 2025 12:06:56 +0100 Subject: [PATCH 01/14] core: refactor helper to include fund wallet when doing a supply --- packages/client/src/testing.ts | 4 + packages/spec/borrow/business.spec.ts | 17 +-- packages/spec/borrow/multipleBorrows.spec.ts | 18 +-- packages/spec/helpers/supplyBorrow.ts | 53 +++++++- packages/spec/positions/business.spec.ts | 17 +-- packages/spec/positions/helper.ts | 124 +++++++++++++------ packages/spec/tools/balances.spec.ts | 23 ++-- 7 files changed, 164 insertions(+), 92 deletions(-) diff --git a/packages/client/src/testing.ts b/packages/client/src/testing.ts index a5f7cb03..91ee07f8 100644 --- a/packages/client/src/testing.ts +++ b/packages/client/src/testing.ts @@ -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( @@ -128,6 +131,7 @@ export async function createNewWallet( ): Promise> { if (!privateKey) { const privateKey = generatePrivateKey(); + console.log('Generated private key:', privateKey); const wallet = createWalletClient({ account: privateKeyToAccount(privateKey), chain: devnetChain, diff --git a/packages/spec/borrow/business.spec.ts b/packages/spec/borrow/business.spec.ts index c51bb0fa..4abbfcdd 100644 --- a/packages/spec/borrow/business.spec.ts +++ b/packages/spec/borrow/business.spec.ts @@ -23,18 +23,13 @@ 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, + autoFund: true, + }); assertOk(setup); }); diff --git a/packages/spec/borrow/multipleBorrows.spec.ts b/packages/spec/borrow/multipleBorrows.spec.ts index 5012e7ef..caa4c3ae 100644 --- a/packages/spec/borrow/multipleBorrows.spec.ts +++ b/packages/spec/borrow/multipleBorrows.spec.ts @@ -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'; @@ -21,18 +20,13 @@ 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, + autoFund: true, + }); assertOk(setup); }, 120_000); diff --git a/packages/spec/helpers/supplyBorrow.ts b/packages/spec/helpers/supplyBorrow.ts index 63b0fb87..c4b90ce9 100644 --- a/packages/spec/helpers/supplyBorrow.ts +++ b/packages/spec/helpers/supplyBorrow.ts @@ -72,11 +72,13 @@ export function findReserveAndSupply( amount, spoke, asCollateral, + autoFund, }: { token: EvmAddress; amount: BigDecimal; spoke?: SpokeId; asCollateral?: boolean; + autoFund?: boolean; }, ): ResultAsync { return findReservesToSupply(client, user, { @@ -84,11 +86,25 @@ export function findReserveAndSupply( 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]), + autoFund + ? fundErc20Address(evmAddress(user.account.address), { + address: token, + amount: amount, + decimals: reserves[0]!.asset.underlying.info.decimals, + }).andThen(() => + supplyToReserve(client, user, { + reserve: reserves[0]!.id, + amount: { erc20: { value: amount } }, + sender: evmAddress(user.account.address), + enableCollateral: asCollateral ?? true, + }).map(() => reserves[0]), + ) + : supplyToReserve(client, user, { + reserve: reserves[0].id, + amount: { erc20: { value: amount } }, + sender: evmAddress(user.account.address), + enableCollateral: asCollateral ?? true, + }).map(() => reserves[0]), ); } @@ -141,6 +157,33 @@ export function supplyAndBorrow( ); } +export function borrowFromRandomReserve( + client: AaveClient, + user: WalletClient, + params: { + spoke?: SpokeId; + token?: EvmAddress; + ratioToBorrow?: number; + }, +): ResultAsync { + 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, diff --git a/packages/spec/positions/business.spec.ts b/packages/spec/positions/business.spec.ts index 7d12a169..d1503d75 100644 --- a/packages/spec/positions/business.spec.ts +++ b/packages/spec/positions/business.spec.ts @@ -40,18 +40,13 @@ 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, - }), - ); + autoFund: true, + }); assertOk(setup); }); diff --git a/packages/spec/positions/helper.ts b/packages/spec/positions/helper.ts index 36aa343e..d30ac539 100644 --- a/packages/spec/positions/helper.ts +++ b/packages/spec/positions/helper.ts @@ -16,24 +16,26 @@ import { userSupplies, } from '@aave/client/actions'; import { + ETHEREUM_AAVE_ADDRESS, ETHEREUM_FORK_ID, ETHEREUM_GHO_ADDRESS, ETHEREUM_SPOKE_CORE_ID, ETHEREUM_SPOKE_ETHENA_ID, + ETHEREUM_USDC_ADDRESS, 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 { + borrowFromRandomReserve, borrowFromReserve, findReserveAndSupply, - supplyAndBorrowNativeToken, supplyToReserve, } from '../helpers/supplyBorrow'; import { @@ -203,41 +205,6 @@ export const recreateUserActivities = async ( } }; -export const recreateUserSummary = async ( - client: AaveClient, - user: WalletClient, -) => { - 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, @@ -423,3 +390,86 @@ export const recreateUserPositions = async ( assertOk(resultEmodeSpoke); } }; + +export const recreateUserPositionInOneSpoke = async ( + client: AaveClient, + user: WalletClient, +) => { + // Check the user has at least one + const supplies = await userSupplies(client, { + query: { + userSpoke: { + spoke: ETHEREUM_SPOKE_CORE_ID, + user: evmAddress(user.account.address), + }, + }, + }); + assertOk(supplies); + const userInfo = await userPositions(client, { + user: evmAddress(user.account.address), + filter: { + chainIds: [ETHEREUM_FORK_ID], + }, + }); + assertOk(userInfo); + + // Create a position in the spoke + if ( + supplies.value.length < 3 || + // Add supply if health factor is less than 4 + userInfo.value[0]!.healthFactor.current!.lt(4) + ) { + const supplyGHOCollateral = await findReserveAndSupply(client, user, { + spoke: ETHEREUM_SPOKE_CORE_ID, + token: ETHEREUM_GHO_ADDRESS, + asCollateral: true, + amount: bigDecimal('100'), + autoFund: true, + }); + assertOk(supplyGHOCollateral); + + const supplyUSDCDNoCollateral = await findReserveAndSupply(client, user, { + spoke: ETHEREUM_SPOKE_CORE_ID, + token: ETHEREUM_USDC_ADDRESS, + asCollateral: false, + amount: bigDecimal('100'), + autoFund: true, + }); + assertOk(supplyUSDCDNoCollateral); + + const supplyWETHCollateral = await findReserveAndSupply(client, user, { + spoke: ETHEREUM_SPOKE_CORE_ID, + token: ETHEREUM_AAVE_ADDRESS, + asCollateral: true, + amount: bigDecimal('0.5'), + autoFund: true, + }); + assertOk(supplyWETHCollateral); + } + + const borrows = await userBorrows(client, { + query: { + userSpoke: { + spoke: ETHEREUM_SPOKE_CORE_ID, + user: evmAddress(user.account.address), + }, + }, + }); + assertOk(borrows); + + if (borrows.value.length < 2) { + const borrowAAVE = await borrowFromRandomReserve(client, user, { + spoke: ETHEREUM_SPOKE_CORE_ID, + token: ETHEREUM_AAVE_ADDRESS, + ratioToBorrow: 0.1, + }); + assertOk(borrowAAVE); + + const borrowWETH = await borrowFromRandomReserve(client, user, { + spoke: ETHEREUM_SPOKE_CORE_ID, + token: ETHEREUM_WETH_ADDRESS, + ratioToBorrow: 0.1, + }); + assertOk(borrowWETH); + } +}; diff --git a/packages/spec/tools/balances.spec.ts b/packages/spec/tools/balances.spec.ts index 3018bd04..8a577acb 100644 --- a/packages/spec/tools/balances.spec.ts +++ b/packages/spec/tools/balances.spec.ts @@ -8,7 +8,6 @@ import { ETHEREUM_HUB_CORE_ADDRESS, ETHEREUM_SPOKE_CORE_ADDRESS, ETHEREUM_USDC_ADDRESS, - fundErc20Address, } from '@aave/client/testing'; import { beforeAll, describe, expect, it } from 'vitest'; import { findReserveAndSupply } from '../helpers/supplyBorrow'; @@ -33,21 +32,13 @@ describe('Querying User Balances on Aave V4', () => { assertOk(balances); if (balances.value.length < 3) { for (const token of [ETHEREUM_USDC_ADDRESS, ETHEREUM_1INCH_ADDRESS]) { - const result = await fundErc20Address( - evmAddress(user.account.address), - { - address: token, - amount: bigDecimal('100'), - decimals: token === ETHEREUM_1INCH_ADDRESS ? 18 : 6, - }, - ).andThen(() => - findReserveAndSupply(client, user, { - token: token, - amount: bigDecimal('50'), - asCollateral: true, - }), - ); - assertOk(result); + const setup = await findReserveAndSupply(client, user, { + token: token, + amount: bigDecimal('100'), + asCollateral: true, + autoFund: true, + }); + assertOk(setup); } } }, 60_000); From a178888c485a51ead17116c7aefa4f162028a8a8 Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Thu, 18 Dec 2025 12:07:25 +0100 Subject: [PATCH 02/14] test: userPosition math checks v1 --- .../spec/positions/math/userPositions.spec.ts | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 packages/spec/positions/math/userPositions.spec.ts diff --git a/packages/spec/positions/math/userPositions.spec.ts b/packages/spec/positions/math/userPositions.spec.ts new file mode 100644 index 00000000..4e5f771d --- /dev/null +++ b/packages/spec/positions/math/userPositions.spec.ts @@ -0,0 +1,160 @@ +import { + assertOk, + bigDecimal, + evmAddress, + type UserBorrowItem, + type UserPosition, + type UserSupplyItem, +} from '@aave/client'; +import { userBorrows, userPositions, userSupplies } from '@aave/client/actions'; +import { + client, + createNewWallet, + ETHEREUM_FORK_ID, +} from '@aave/client/testing'; +import { beforeAll, describe, expect, it } from 'vitest'; + +import { + assertNonEmptyArray, + assertSingleElementArray, +} from '../../test-utils'; +import { recreateUserPositionInOneSpoke } from '../helper'; + +const user = await createNewWallet( + '0xbae6035617e696766fc0a0739508200144f6e785600cc155496ddfc1d78a6a14', +); + +describe('Check User Positions Math on Aave V4', () => { + describe('Given a user with multiple deposits and at least one borrow in one spoke', () => { + beforeAll(async () => { + // NOTE: Recreate user with at least one position with multiple deposits and at least two borrow in one spoke + await recreateUserPositionInOneSpoke(client, user); + }, 180_000); + + describe('When fetching the user positions for the user', () => { + let positions: UserPosition; + let suppliesPositions: UserSupplyItem[]; + let borrowPositions: UserBorrowItem[]; + + beforeAll(async () => { + const [positionsResult, suppliesResult, borrowResult] = + await Promise.all([ + userPositions(client, { + user: evmAddress(user.account.address), + filter: { + chainIds: [ETHEREUM_FORK_ID], + }, + }), + userSupplies(client, { + query: { + userChains: { + chainIds: [ETHEREUM_FORK_ID], + user: evmAddress(user.account.address), + }, + }, + }), + userBorrows(client, { + query: { + userChains: { + chainIds: [ETHEREUM_FORK_ID], + user: evmAddress(user.account.address), + }, + }, + }), + ]); + + assertOk(positionsResult); + assertNonEmptyArray(positionsResult.value); + // We only operate on one spoke, so we expect a single element array + assertSingleElementArray(positionsResult.value); + positions = positionsResult.value[0]; + + assertOk(suppliesResult); + assertNonEmptyArray(suppliesResult.value); + suppliesPositions = suppliesResult.value; + + assertOk(borrowResult); + assertNonEmptyArray(borrowResult.value); + borrowPositions = borrowResult.value; + }, 180_000); + + it('Then it should return the correct totalSupplied value', async () => { + // total supplied is the sum of the principal and interest for all positions in the spoke + const totalSupplied = suppliesPositions.reduce( + (acc, supply) => + acc.plus( + supply.principal.exchange.value.plus( + supply.interest.exchange.value, + ), + ), + bigDecimal('0'), + ); + expect(totalSupplied).toBeBigDecimalCloseTo( + positions.totalSupplied.current.value, + 1, + ); + }); + + it('Then it should return the correct totalCollateral value', async () => { + // total collateral is the sum of the principal and interest for all positions marked as collateral in the spoke + const totalCollateral = suppliesPositions + .filter((supply) => supply.isCollateral) + .reduce( + (acc, supply) => + acc.plus( + supply.principal.exchange.value.plus( + supply.interest.exchange.value, + ), + ), + bigDecimal('0'), + ); + + expect(totalCollateral).toBeBigDecimalCloseTo( + positions.totalCollateral.current.value, + 1, + ); + }); + + it('Then it should return the correct totalDebt value', async () => { + // total debt is the sum of the principal and interest for all positions in the spoke + const totalDebt = borrowPositions.reduce( + (acc, borrow) => + acc.plus( + borrow.debt.exchange.value.plus(borrow.interest.exchange.value), + ), + bigDecimal('0'), + ); + expect(totalDebt).toBeBigDecimalCloseTo( + positions.totalDebt.current.value, + 1, + ); + }); + + it('Then it should return the correct netBalance value', async () => { + // net balance is the sum of the total supplied minus the borrows (debt) + const totalSupplied = suppliesPositions.reduce( + (acc, supply) => + acc.plus( + supply.principal.exchange.value.plus( + supply.interest.exchange.value, + ), + ), + bigDecimal('0'), + ); + + const totalDebt = borrowPositions.reduce( + (acc, borrow) => + acc.plus( + borrow.debt.exchange.value.plus(borrow.interest.exchange.value), + ), + bigDecimal('0'), + ); + + expect(totalSupplied.minus(totalDebt)).toBeBigDecimalCloseTo( + positions.netBalance.current.value, + 1, + ); + }); + }); + }); +}); From b3011e6f439e56211e81e993bb5f1ff7f566ed56 Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Thu, 18 Dec 2025 12:13:59 +0100 Subject: [PATCH 03/14] fix: rename test scenario --- packages/client/src/testing.ts | 1 - packages/spec/positions/math/userPositions.spec.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/client/src/testing.ts b/packages/client/src/testing.ts index 91ee07f8..be850fc8 100644 --- a/packages/client/src/testing.ts +++ b/packages/client/src/testing.ts @@ -131,7 +131,6 @@ export async function createNewWallet( ): Promise> { if (!privateKey) { const privateKey = generatePrivateKey(); - console.log('Generated private key:', privateKey); const wallet = createWalletClient({ account: privateKeyToAccount(privateKey), chain: devnetChain, diff --git a/packages/spec/positions/math/userPositions.spec.ts b/packages/spec/positions/math/userPositions.spec.ts index 4e5f771d..2cc0c697 100644 --- a/packages/spec/positions/math/userPositions.spec.ts +++ b/packages/spec/positions/math/userPositions.spec.ts @@ -25,9 +25,8 @@ const user = await createNewWallet( ); describe('Check User Positions Math on Aave V4', () => { - describe('Given a user with multiple deposits and at least one borrow in one spoke', () => { + describe('Given a user with multiple deposits and at least two borrows in one spoke', () => { beforeAll(async () => { - // NOTE: Recreate user with at least one position with multiple deposits and at least two borrow in one spoke await recreateUserPositionInOneSpoke(client, user); }, 180_000); From b45b497bd8161481993e8be6a5411b3bd508ca78 Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Thu, 18 Dec 2025 13:07:19 +0100 Subject: [PATCH 04/14] fix: add changeset --- .changeset/mighty-phones-raise.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/mighty-phones-raise.md diff --git a/.changeset/mighty-phones-raise.md b/.changeset/mighty-phones-raise.md new file mode 100644 index 00000000..a845151c --- /dev/null +++ b/.changeset/mighty-phones-raise.md @@ -0,0 +1,2 @@ +--- +--- From 05c6da4c77303ba342649a151ea3de655a91e321 Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Thu, 18 Dec 2025 15:00:06 +0100 Subject: [PATCH 05/14] feat: add helper to call getAcccountData in the spoke contract --- packages/client/src/testing.ts | 2 +- packages/spec/positions/math/helper.ts | 93 ++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 packages/spec/positions/math/helper.ts diff --git a/packages/client/src/testing.ts b/packages/client/src/testing.ts index 65666f06..4da8f0a2 100644 --- a/packages/client/src/testing.ts +++ b/packages/client/src/testing.ts @@ -118,7 +118,7 @@ export const client = AaveClient.create({ environment, }); -const devnetChain = await chain(client, { chainId: ETHEREUM_FORK_ID }) +export const devnetChain = await chain(client, { chainId: ETHEREUM_FORK_ID }) .map(nonNullable) .map(toViemChain) .match( diff --git a/packages/spec/positions/math/helper.ts b/packages/spec/positions/math/helper.ts new file mode 100644 index 00000000..303c0e75 --- /dev/null +++ b/packages/spec/positions/math/helper.ts @@ -0,0 +1,93 @@ +import { type BigDecimal, bigDecimal } from '@aave/client'; +import { devnetChain, ETHEREUM_FORK_RPC_URL } from '@aave/client/testing'; +import { type Address, createPublicClient, http } from 'viem'; + +// Constants +const WAD = 10n ** 18n; // 1e18 = 1.0 in WAD format +const MAX_UINT256 = 2n ** 256n - 1n; +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 formatHealthFactor(healthFactor: bigint): BigDecimal { + if (healthFactor === MAX_UINT256) { + return bigDecimal(0); + } + const hf = bigDecimal(healthFactor).div(WAD); + return hf; +} + +function formatUSD(value: bigint): BigDecimal { + return bigDecimal(value).div(bigDecimal('1e26')); +} + +function formatPercentage(value: bigint): BigDecimal { + return bigDecimal(value).div(WAD).mul(bigDecimal('100')); +} + +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 { + const publicClient = createPublicClient({ + chain: devnetChain, + transport: http(ETHEREUM_FORK_RPC_URL), + }); + + const result = await publicClient.readContract({ + address: spoke, + abi: userAccountDataABI, + functionName: 'getUserAccountData', + args: [address], + }); + + return { + riskPremium: formatBPS(result.riskPremium), + avgCollateralFactor: formatPercentage(result.avgCollateralFactor), + healthFactor: formatHealthFactor(result.healthFactor), + totalCollateralValue: formatUSD(result.totalCollateralValue), + totalDebtValue: formatUSD(result.totalDebtValue), + activeCollateralCount: Number(result.activeCollateralCount), + borrowedCount: Number(result.borrowedCount), + }; +} From 48daa14d6db716f6bbe063275291376a9e523724 Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Thu, 18 Dec 2025 15:00:22 +0100 Subject: [PATCH 06/14] test: add healthFactor math calculations --- .../spec/positions/math/userPositions.spec.ts | 126 +++++++++++++++--- 1 file changed, 104 insertions(+), 22 deletions(-) diff --git a/packages/spec/positions/math/userPositions.spec.ts b/packages/spec/positions/math/userPositions.spec.ts index 2cc0c697..f85fd56c 100644 --- a/packages/spec/positions/math/userPositions.spec.ts +++ b/packages/spec/positions/math/userPositions.spec.ts @@ -11,6 +11,7 @@ import { client, createNewWallet, ETHEREUM_FORK_ID, + ETHEREUM_SPOKE_CORE_ADDRESS, } from '@aave/client/testing'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -19,6 +20,7 @@ import { assertSingleElementArray, } from '../../test-utils'; import { recreateUserPositionInOneSpoke } from '../helper'; +import { getAccountData, type UserAccountData } from './helper'; const user = await createNewWallet( '0xbae6035617e696766fc0a0739508200144f6e785600cc155496ddfc1d78a6a14', @@ -34,33 +36,42 @@ describe('Check User Positions Math on Aave V4', () => { let positions: UserPosition; let suppliesPositions: UserSupplyItem[]; let borrowPositions: UserBorrowItem[]; + let accountDataOnChain: UserAccountData; beforeAll(async () => { - const [positionsResult, suppliesResult, borrowResult] = - await Promise.all([ - userPositions(client, { - user: evmAddress(user.account.address), - filter: { + const [ + positionsResult, + suppliesResult, + borrowResult, + accountDataResult, + ] = await Promise.all([ + userPositions(client, { + user: evmAddress(user.account.address), + filter: { + chainIds: [ETHEREUM_FORK_ID], + }, + }), + userSupplies(client, { + query: { + userChains: { chainIds: [ETHEREUM_FORK_ID], + user: evmAddress(user.account.address), }, - }), - userSupplies(client, { - query: { - userChains: { - chainIds: [ETHEREUM_FORK_ID], - user: evmAddress(user.account.address), - }, - }, - }), - userBorrows(client, { - query: { - userChains: { - chainIds: [ETHEREUM_FORK_ID], - user: evmAddress(user.account.address), - }, + }, + }), + userBorrows(client, { + query: { + userChains: { + chainIds: [ETHEREUM_FORK_ID], + user: evmAddress(user.account.address), }, - }), - ]); + }, + }), + getAccountData( + evmAddress(user.account.address), + ETHEREUM_SPOKE_CORE_ADDRESS, + ), + ]); assertOk(positionsResult); assertNonEmptyArray(positionsResult.value); @@ -75,6 +86,8 @@ describe('Check User Positions Math on Aave V4', () => { assertOk(borrowResult); assertNonEmptyArray(borrowResult.value); borrowPositions = borrowResult.value; + + accountDataOnChain = accountDataResult; }, 180_000); it('Then it should return the correct totalSupplied value', async () => { @@ -108,6 +121,12 @@ describe('Check User Positions Math on Aave V4', () => { bigDecimal('0'), ); + // Cross check with the account data on chain + expect(accountDataOnChain.totalCollateralValue).toBeBigDecimalCloseTo( + totalCollateral, + 1, + ); + // Cross check with the user positions expect(totalCollateral).toBeBigDecimalCloseTo( positions.totalCollateral.current.value, 1, @@ -123,6 +142,13 @@ describe('Check User Positions Math on Aave V4', () => { ), bigDecimal('0'), ); + + // Cross check with the account data on chain + expect(accountDataOnChain.totalDebtValue).toBeBigDecimalCloseTo( + totalDebt, + 1, + ); + // Cross check with the user positions expect(totalDebt).toBeBigDecimalCloseTo( positions.totalDebt.current.value, 1, @@ -154,6 +180,62 @@ describe('Check User Positions Math on Aave V4', () => { 1, ); }); + + it('Then it should return the correct health factor', async () => { + // Calculate health factor according to the contract logic in Spoke.sol: + // The contract uses BPS (basis points) internally and converts to WAD (18 decimals) + + // Step 1: Calculate weighted sum of collateral factors + // For each collateral asset: + // - Calculate collateral value: (principal + interest) in USD + // - Accumulate: avgCollateralFactorWeightedSum += collateralFactor × collateralValue + let avgCollateralFactorWeightedSum = bigDecimal('0'); + let totalCollateralValue = bigDecimal('0'); + + for (const supply of suppliesPositions) { + if (supply.isCollateral) { + const collateralValue = supply.principal.exchange.value.plus( + supply.interest.exchange.value, + ); + const collateralFactor = + supply.reserve.settings.collateralFactor.value; + + avgCollateralFactorWeightedSum = + avgCollateralFactorWeightedSum.plus( + collateralFactor.times(collateralValue), + ); + totalCollateralValue = totalCollateralValue.plus(collateralValue); + } + } + + // Step 2: Calculate total debt value + // For each debt asset: debt = drawnDebt + premiumDebt = debt + interest + const totalDebtValue = borrowPositions.reduce( + (acc, borrow) => + acc.plus( + borrow.debt.exchange.value.plus(borrow.interest.exchange.value), + ), + bigDecimal('0'), + ); + + // Step 3: Compute health factor + // - Formula: healthFactor = avgCollateralFactorWeightedSum / totalDebtValue + + // If totalDebtValue is greater than 0, calculate the health factor + if (totalDebtValue.gt(0)) { + const calculatedHealthFactor = + avgCollateralFactorWeightedSum.div(totalDebtValue); + + // Cross check with the account data on chain + expect(calculatedHealthFactor).toBeBigDecimalCloseTo( + accountDataOnChain.healthFactor, + ); + // Cross check with the user positions + expect(calculatedHealthFactor).toBeBigDecimalCloseTo( + positions.healthFactor.current, + ); + } + }); }); }); }); From 4b0981e8ab19d8e10efcf377294c3a2e45de156e Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Thu, 18 Dec 2025 15:55:03 +0100 Subject: [PATCH 07/14] test: check netCollateral and averageCollateralFactor --- packages/spec/positions/math/helper.ts | 17 +--- .../spec/positions/math/userPositions.spec.ts | 93 ++++++++++++++++--- 2 files changed, 84 insertions(+), 26 deletions(-) diff --git a/packages/spec/positions/math/helper.ts b/packages/spec/positions/math/helper.ts index 303c0e75..eb1fa260 100644 --- a/packages/spec/positions/math/helper.ts +++ b/packages/spec/positions/math/helper.ts @@ -4,7 +4,6 @@ import { type Address, createPublicClient, http } from 'viem'; // Constants const WAD = 10n ** 18n; // 1e18 = 1.0 in WAD format -const MAX_UINT256 = 2n ** 256n - 1n; const userAccountDataABI = [ { inputs: [{ name: 'user', type: 'address' }], @@ -39,22 +38,14 @@ export interface UserAccountData { borrowedCount: number; } -function formatHealthFactor(healthFactor: bigint): BigDecimal { - if (healthFactor === MAX_UINT256) { - return bigDecimal(0); - } - const hf = bigDecimal(healthFactor).div(WAD); - return hf; +function formatWAD(value: bigint): BigDecimal { + return bigDecimal(value).div(WAD); } function formatUSD(value: bigint): BigDecimal { return bigDecimal(value).div(bigDecimal('1e26')); } -function formatPercentage(value: bigint): BigDecimal { - return bigDecimal(value).div(WAD).mul(bigDecimal('100')); -} - function formatBPS(value: bigint): BigDecimal { return bigDecimal(value).div(bigDecimal('100')); } @@ -83,8 +74,8 @@ export async function getAccountData( return { riskPremium: formatBPS(result.riskPremium), - avgCollateralFactor: formatPercentage(result.avgCollateralFactor), - healthFactor: formatHealthFactor(result.healthFactor), + avgCollateralFactor: formatWAD(result.avgCollateralFactor), + healthFactor: formatWAD(result.healthFactor), totalCollateralValue: formatUSD(result.totalCollateralValue), totalDebtValue: formatUSD(result.totalDebtValue), activeCollateralCount: Number(result.activeCollateralCount), diff --git a/packages/spec/positions/math/userPositions.spec.ts b/packages/spec/positions/math/userPositions.spec.ts index f85fd56c..aa502b44 100644 --- a/packages/spec/positions/math/userPositions.spec.ts +++ b/packages/spec/positions/math/userPositions.spec.ts @@ -133,6 +133,34 @@ describe('Check User Positions Math on Aave V4', () => { ); }); + it('Then it should return the correct netCollateral value', async () => { + // net collateral is the sum of the total collateral minus the total debt + const totalCollateral = suppliesPositions + .filter((supply) => supply.isCollateral) + .reduce( + (acc, supply) => + acc.plus( + supply.principal.exchange.value.plus( + supply.interest.exchange.value, + ), + ), + bigDecimal('0'), + ); + const totalDebt = borrowPositions.reduce( + (acc, borrow) => + acc.plus( + borrow.debt.exchange.value.plus(borrow.interest.exchange.value), + ), + bigDecimal('0'), + ); + + // Cross check with the user positions + expect(totalCollateral.minus(totalDebt)).toBeBigDecimalCloseTo( + positions.netCollateral.current.value, + 1, + ); + }); + it('Then it should return the correct totalDebt value', async () => { // total debt is the sum of the principal and interest for all positions in the spoke const totalDebt = borrowPositions.reduce( @@ -189,24 +217,16 @@ describe('Check User Positions Math on Aave V4', () => { // For each collateral asset: // - Calculate collateral value: (principal + interest) in USD // - Accumulate: avgCollateralFactorWeightedSum += collateralFactor × collateralValue - let avgCollateralFactorWeightedSum = bigDecimal('0'); - let totalCollateralValue = bigDecimal('0'); - - for (const supply of suppliesPositions) { - if (supply.isCollateral) { + const avgCollateralFactorWeightedSum = suppliesPositions + .filter((supply) => supply.isCollateral) + .reduce((acc, supply) => { const collateralValue = supply.principal.exchange.value.plus( supply.interest.exchange.value, ); const collateralFactor = supply.reserve.settings.collateralFactor.value; - - avgCollateralFactorWeightedSum = - avgCollateralFactorWeightedSum.plus( - collateralFactor.times(collateralValue), - ); - totalCollateralValue = totalCollateralValue.plus(collateralValue); - } - } + return acc.plus(collateralFactor.times(collateralValue)); + }, bigDecimal('0')); // Step 2: Calculate total debt value // For each debt asset: debt = drawnDebt + premiumDebt = debt + interest @@ -236,6 +256,53 @@ describe('Check User Positions Math on Aave V4', () => { ); } }); + + it('Then it should return the correct averageCollateralFactor value', async () => { + const collateralPositions = suppliesPositions.filter( + (supply) => supply.isCollateral, + ); + + const { weightedSum, totalValue } = collateralPositions.reduce( + (acc, supply) => { + const collateralValue = supply.principal.exchange.value.plus( + supply.interest.exchange.value, + ); + const collateralFactor = + supply.reserve.settings.collateralFactor.value; + return { + weightedSum: acc.weightedSum.plus( + collateralFactor.times(collateralValue), + ), + totalValue: acc.totalValue.plus(collateralValue), + }; + }, + { + weightedSum: bigDecimal('0'), + totalValue: bigDecimal('0'), + }, + ); + + // Normalize: avgCollateralFactor = weightedSum / totalValue + const averageCollateralFactor = weightedSum.div(totalValue); + + // Cross check with the account data on chain + expect(averageCollateralFactor).toBeBigDecimalCloseTo( + accountDataOnChain.avgCollateralFactor, + 5, + ); + // Cross check with the user positions + expect(averageCollateralFactor).toBeBigDecimalCloseTo( + positions.averageCollateralFactor.value, + 5, + ); + }); + + it.todo('Then it should return the correct netApy value'); + it.todo('Then it should return the correct netSupplyApy value'); + it.todo('Then it should return the correct netBorrowApy value'); + it.todo('Then it should return the correct riskPremium value'); + it.todo('Then it should return the correct liquidationPrice value'); + it.todo('Then it should return the correct borrowingPower value'); }); }); }); From febf9237e76e893d8027db0a536e4e5c45458ed3 Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Sun, 21 Dec 2025 17:39:15 +0100 Subject: [PATCH 08/14] refactor: remove autoFund parameter and always auto-fund in test helpers --- packages/spec/borrow/business.spec.ts | 1 - packages/spec/borrow/multipleBorrows.spec.ts | 1 - packages/spec/helpers/supplyBorrow.ts | 62 ++++++++++++-------- packages/spec/positions/business.spec.ts | 1 - packages/spec/positions/helper.ts | 3 - packages/spec/repay/business.spec.ts | 27 ++------- packages/spec/tools/balances.spec.ts | 1 - packages/spec/withdraw/business.spec.ts | 30 ++-------- 8 files changed, 44 insertions(+), 82 deletions(-) diff --git a/packages/spec/borrow/business.spec.ts b/packages/spec/borrow/business.spec.ts index 4abbfcdd..5ae52a56 100644 --- a/packages/spec/borrow/business.spec.ts +++ b/packages/spec/borrow/business.spec.ts @@ -28,7 +28,6 @@ describe('Borrowing Assets on Aave V4', () => { spoke: ETHEREUM_SPOKE_CORE_ID, amount: amountToSupply, asCollateral: true, - autoFund: true, }); assertOk(setup); diff --git a/packages/spec/borrow/multipleBorrows.spec.ts b/packages/spec/borrow/multipleBorrows.spec.ts index caa4c3ae..7f4f22c2 100644 --- a/packages/spec/borrow/multipleBorrows.spec.ts +++ b/packages/spec/borrow/multipleBorrows.spec.ts @@ -25,7 +25,6 @@ describe('Borrowing from Multiple Reserves on Aave V4', () => { spoke: ETHEREUM_SPOKE_CORE_ID, amount: amountToSupply, asCollateral: true, - autoFund: true, }); assertOk(setup); diff --git a/packages/spec/helpers/supplyBorrow.ts b/packages/spec/helpers/supplyBorrow.ts index c4b90ce9..806edac0 100644 --- a/packages/spec/helpers/supplyBorrow.ts +++ b/packages/spec/helpers/supplyBorrow.ts @@ -72,40 +72,50 @@ export function findReserveAndSupply( amount, spoke, asCollateral, - autoFund, }: { - token: EvmAddress; - amount: BigDecimal; + token?: EvmAddress; + amount?: BigDecimal; spoke?: SpokeId; asCollateral?: boolean; - autoFund?: boolean; }, -): ResultAsync { +): ResultAsync<{ reserveInfo: Reserve; amountSupplied: BigDecimal }, Error> { return findReservesToSupply(client, user, { token: token, spoke: spoke, asCollateral: asCollateral, - }).andThen((reserves) => - autoFund - ? fundErc20Address(evmAddress(user.account.address), { - address: token, - amount: amount, - decimals: reserves[0]!.asset.underlying.info.decimals, - }).andThen(() => - supplyToReserve(client, user, { - reserve: reserves[0]!.id, - amount: { erc20: { value: amount } }, - sender: evmAddress(user.account.address), - enableCollateral: asCollateral ?? true, - }).map(() => reserves[0]), - ) - : supplyToReserve(client, user, { - reserve: reserves[0].id, - amount: { erc20: { value: amount } }, - sender: evmAddress(user.account.address), - enableCollateral: asCollateral ?? true, - }).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( diff --git a/packages/spec/positions/business.spec.ts b/packages/spec/positions/business.spec.ts index d1503d75..0e0e10cb 100644 --- a/packages/spec/positions/business.spec.ts +++ b/packages/spec/positions/business.spec.ts @@ -45,7 +45,6 @@ describe('Health Factor Scenarios on Aave V4', () => { spoke: ETHEREUM_SPOKE_CORE_ID, asCollateral: true, amount: amountToSupply, - autoFund: true, }); assertOk(setup); diff --git a/packages/spec/positions/helper.ts b/packages/spec/positions/helper.ts index d30ac539..572724e6 100644 --- a/packages/spec/positions/helper.ts +++ b/packages/spec/positions/helper.ts @@ -424,7 +424,6 @@ export const recreateUserPositionInOneSpoke = async ( token: ETHEREUM_GHO_ADDRESS, asCollateral: true, amount: bigDecimal('100'), - autoFund: true, }); assertOk(supplyGHOCollateral); @@ -433,7 +432,6 @@ export const recreateUserPositionInOneSpoke = async ( token: ETHEREUM_USDC_ADDRESS, asCollateral: false, amount: bigDecimal('100'), - autoFund: true, }); assertOk(supplyUSDCDNoCollateral); @@ -442,7 +440,6 @@ export const recreateUserPositionInOneSpoke = async ( token: ETHEREUM_AAVE_ADDRESS, asCollateral: true, amount: bigDecimal('0.5'), - autoFund: true, }); assertOk(supplyWETHCollateral); } diff --git a/packages/spec/repay/business.spec.ts b/packages/spec/repay/business.spec.ts index e05a3480..fe328c18 100644 --- a/packages/spec/repay/business.spec.ts +++ b/packages/spec/repay/business.spec.ts @@ -17,14 +17,11 @@ import { sendWith, signERC20PermitWith } from '@aave/client/viem'; import type { Reserve } from '@aave/graphql'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { - findReservesToBorrow, - findReservesToSupply, -} from '../helpers/reserves'; +import { findReservesToBorrow } from '../helpers/reserves'; import { borrowFromReserve, + findReserveAndSupply, supplyAndBorrowNativeToken, - supplyToReserve, } from '../helpers/supplyBorrow'; const user = await createNewWallet(); @@ -34,29 +31,13 @@ describe('Repaying Loans on Aave V4', () => { let reserve: Reserve; beforeEach(async () => { - const supplySetup = await findReservesToSupply(client, user, { + const supplySetup = await findReserveAndSupply(client, user, { token: ETHEREUM_USDC_ADDRESS, spoke: ETHEREUM_SPOKE_CORE_ID, asCollateral: true, - }).andThen((supplyReserves) => { - const amountToSupply = supplyReserves[0].supplyCap - .minus(supplyReserves[0].summary.supplied.amount.value) - .div(10000); - - return fundErc20Address(evmAddress(user.account.address), { - address: supplyReserves[0].asset.underlying.address, - amount: amountToSupply, - decimals: supplyReserves[0].asset.underlying.info.decimals, - }).andThen(() => - supplyToReserve(client, user, { - reserve: supplyReserves[0].id, - amount: { erc20: { value: amountToSupply } }, - sender: evmAddress(user.account.address), - enableCollateral: true, - }), - ); }); assertOk(supplySetup); + const borrowSetup = await findReservesToBorrow(client, user, { spoke: ETHEREUM_SPOKE_CORE_ID, }).andThen((borrowReserves) => { diff --git a/packages/spec/tools/balances.spec.ts b/packages/spec/tools/balances.spec.ts index 8a577acb..d5243dba 100644 --- a/packages/spec/tools/balances.spec.ts +++ b/packages/spec/tools/balances.spec.ts @@ -36,7 +36,6 @@ describe('Querying User Balances on Aave V4', () => { token: token, amount: bigDecimal('100'), asCollateral: true, - autoFund: true, }); assertOk(setup); } diff --git a/packages/spec/withdraw/business.spec.ts b/packages/spec/withdraw/business.spec.ts index e44556cb..43b4ffbd 100644 --- a/packages/spec/withdraw/business.spec.ts +++ b/packages/spec/withdraw/business.spec.ts @@ -9,18 +9,15 @@ import { client, createNewWallet, ETHEREUM_SPOKE_CORE_ID, - fundErc20Address, getBalance, getNativeBalance, } from '@aave/client/testing'; import { sendWith } from '@aave/client/viem'; import type { Reserve } from '@aave/graphql'; import { beforeEach, describe, expect, it } from 'vitest'; - -import { findReservesToSupply } from '../helpers/reserves'; import { + findReserveAndSupply, supplyNativeTokenToReserve, - supplyToReserve, } from '../helpers/supplyBorrow'; import { assertSingleElementArray } from '../test-utils'; @@ -32,31 +29,12 @@ describe('Withdrawing Assets on Aave V4', () => { let amountToSupply: BigDecimal; beforeEach(async () => { - const setup = await findReservesToSupply(client, user, { + const setup = await findReserveAndSupply(client, user, { spoke: ETHEREUM_SPOKE_CORE_ID, - }).andThen((listReserves) => { - amountToSupply = listReserves[0].supplyCap - .minus(listReserves[0].summary.supplied.amount.value) - .div(10000); - - return fundErc20Address(evmAddress(user.account!.address), { - address: listReserves[0].asset.underlying.address, - amount: amountToSupply, - decimals: listReserves[0].asset.underlying.info.decimals, - }).andThen(() => - supplyToReserve(client, user, { - reserve: listReserves[0].id, - amount: { - erc20: { - value: amountToSupply, - }, - }, - sender: evmAddress(user.account.address), - }).map(() => listReserves[0]), - ); }); assertOk(setup); - reserve = setup.value; + reserve = setup.value.reserveInfo; + amountToSupply = setup.value.amountSupplied; }, 40_000); describe('When the user withdraws part of their supplied tokens', () => { From 0d56477b5e1990185fb20234145887c4275e09e7 Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Sun, 21 Dec 2025 17:45:47 +0100 Subject: [PATCH 09/14] test: make precision parameter required in toBeBigDecimalCloseTo matcher --- packages/spec/borrow/business.spec.ts | 2 + packages/spec/borrow/multipleBorrows.spec.ts | 2 + .../spec/positions/math/userPositions.spec.ts | 88 ++----------------- packages/spec/repay/business.spec.ts | 2 + packages/spec/supply/business.spec.ts | 2 + packages/spec/supply/setCollateral.spec.ts | 5 +- packages/spec/withdraw/business.spec.ts | 2 +- vitest.d.ts | 2 +- vitest.setup.ts | 2 +- 9 files changed, 23 insertions(+), 84 deletions(-) diff --git a/packages/spec/borrow/business.spec.ts b/packages/spec/borrow/business.spec.ts index 5ae52a56..2c11dd06 100644 --- a/packages/spec/borrow/business.spec.ts +++ b/packages/spec/borrow/business.spec.ts @@ -99,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); }); @@ -183,6 +184,7 @@ describe('Borrowing Assets on Aave V4', () => { ); expect(balanceAfter).toBeBigDecimalCloseTo( balanceBefore.add(amountToBorrow), + 2, ); expect(position?.debt.amount.value).toBeBigDecimalCloseTo( diff --git a/packages/spec/borrow/multipleBorrows.spec.ts b/packages/spec/borrow/multipleBorrows.spec.ts index 7f4f22c2..75e4f42d 100644 --- a/packages/spec/borrow/multipleBorrows.spec.ts +++ b/packages/spec/borrow/multipleBorrows.spec.ts @@ -100,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) @@ -120,6 +121,7 @@ describe('Borrowing from Multiple Reserves on Aave V4', () => { reservesToBorrow.value[1]!.userState!.borrowable.amount.value.times( 0.1, ), + 2, ); }); }); diff --git a/packages/spec/positions/math/userPositions.spec.ts b/packages/spec/positions/math/userPositions.spec.ts index aa502b44..be3a8ece 100644 --- a/packages/spec/positions/math/userPositions.spec.ts +++ b/packages/spec/positions/math/userPositions.spec.ts @@ -108,27 +108,9 @@ describe('Check User Positions Math on Aave V4', () => { }); it('Then it should return the correct totalCollateral value', async () => { - // total collateral is the sum of the principal and interest for all positions marked as collateral in the spoke - const totalCollateral = suppliesPositions - .filter((supply) => supply.isCollateral) - .reduce( - (acc, supply) => - acc.plus( - supply.principal.exchange.value.plus( - supply.interest.exchange.value, - ), - ), - bigDecimal('0'), - ); - // Cross check with the account data on chain - expect(accountDataOnChain.totalCollateralValue).toBeBigDecimalCloseTo( - totalCollateral, - 1, - ); - // Cross check with the user positions - expect(totalCollateral).toBeBigDecimalCloseTo( - positions.totalCollateral.current.value, + expect(positions.totalCollateral.current.value).toBeBigDecimalCloseTo( + accountDataOnChain.totalCollateralValue, 1, ); }); @@ -163,22 +145,8 @@ describe('Check User Positions Math on Aave V4', () => { it('Then it should return the correct totalDebt value', async () => { // total debt is the sum of the principal and interest for all positions in the spoke - const totalDebt = borrowPositions.reduce( - (acc, borrow) => - acc.plus( - borrow.debt.exchange.value.plus(borrow.interest.exchange.value), - ), - bigDecimal('0'), - ); - - // Cross check with the account data on chain - expect(accountDataOnChain.totalDebtValue).toBeBigDecimalCloseTo( - totalDebt, - 1, - ); - // Cross check with the user positions - expect(totalDebt).toBeBigDecimalCloseTo( - positions.totalDebt.current.value, + expect(positions.totalDebt.current.value).toBeBigDecimalCloseTo( + accountDataOnChain.totalDebtValue, 1, ); }); @@ -210,51 +178,11 @@ describe('Check User Positions Math on Aave V4', () => { }); it('Then it should return the correct health factor', async () => { - // Calculate health factor according to the contract logic in Spoke.sol: - // The contract uses BPS (basis points) internally and converts to WAD (18 decimals) - - // Step 1: Calculate weighted sum of collateral factors - // For each collateral asset: - // - Calculate collateral value: (principal + interest) in USD - // - Accumulate: avgCollateralFactorWeightedSum += collateralFactor × collateralValue - const avgCollateralFactorWeightedSum = suppliesPositions - .filter((supply) => supply.isCollateral) - .reduce((acc, supply) => { - const collateralValue = supply.principal.exchange.value.plus( - supply.interest.exchange.value, - ); - const collateralFactor = - supply.reserve.settings.collateralFactor.value; - return acc.plus(collateralFactor.times(collateralValue)); - }, bigDecimal('0')); - - // Step 2: Calculate total debt value - // For each debt asset: debt = drawnDebt + premiumDebt = debt + interest - const totalDebtValue = borrowPositions.reduce( - (acc, borrow) => - acc.plus( - borrow.debt.exchange.value.plus(borrow.interest.exchange.value), - ), - bigDecimal('0'), + // Cross check with the user positions + expect(positions.healthFactor.current).toBeBigDecimalCloseTo( + accountDataOnChain.healthFactor, + 2, ); - - // Step 3: Compute health factor - // - Formula: healthFactor = avgCollateralFactorWeightedSum / totalDebtValue - - // If totalDebtValue is greater than 0, calculate the health factor - if (totalDebtValue.gt(0)) { - const calculatedHealthFactor = - avgCollateralFactorWeightedSum.div(totalDebtValue); - - // Cross check with the account data on chain - expect(calculatedHealthFactor).toBeBigDecimalCloseTo( - accountDataOnChain.healthFactor, - ); - // Cross check with the user positions - expect(calculatedHealthFactor).toBeBigDecimalCloseTo( - positions.healthFactor.current, - ); - } }); it('Then it should return the correct averageCollateralFactor value', async () => { diff --git a/packages/spec/repay/business.spec.ts b/packages/spec/repay/business.spec.ts index fe328c18..7e3d780b 100644 --- a/packages/spec/repay/business.spec.ts +++ b/packages/spec/repay/business.spec.ts @@ -276,6 +276,7 @@ describe('Repaying Loans on Aave V4', () => { invariant(positionAfter, 'No position found'); expect(positionAfter.debt.amount.value).toBeBigDecimalCloseTo( positionBefore.debt.amount.value.minus(amountToRepay), + 2, ); }); }); @@ -349,6 +350,7 @@ describe('Repaying Loans on Aave V4', () => { invariant(positionAfter, 'No position found'); expect(positionAfter.debt.amount.value).toBeBigDecimalCloseTo( amountToRepay, + 2, ); const balanceAfter = await getNativeBalance( diff --git a/packages/spec/supply/business.spec.ts b/packages/spec/supply/business.spec.ts index 8cc9c5f0..36619137 100644 --- a/packages/spec/supply/business.spec.ts +++ b/packages/spec/supply/business.spec.ts @@ -82,6 +82,7 @@ describe('Supplying Assets on Aave V4', () => { expect(supplyPosition.isCollateral).toEqual(true); expect(supplyPosition.principal.amount.value).toBeBigDecimalCloseTo( amountToSupply, + 2, ); // Check the other reserves were not affected @@ -295,6 +296,7 @@ describe('Supplying Assets on Aave V4', () => { supplyPositionBefore.value?.withdrawable.amount.value.plus( amountToSupply, ), + 2, ); }); }); diff --git a/packages/spec/supply/setCollateral.spec.ts b/packages/spec/supply/setCollateral.spec.ts index 544bcb67..73648af6 100644 --- a/packages/spec/supply/setCollateral.spec.ts +++ b/packages/spec/supply/setCollateral.spec.ts @@ -123,7 +123,10 @@ describe('Setting Supply as Collateral on Aave V4', () => { // netBalance should be the same expect( previewResult.value.netBalance.after.value, - ).toBeBigDecimalCloseTo(previewResult.value.netBalance.current.value); + ).toBeBigDecimalCloseTo( + previewResult.value.netBalance.current.value, + 2, + ); if (!positions.value[0].isCollateral) { expect( previewResult.value.netCollateral.after.value, diff --git a/packages/spec/withdraw/business.spec.ts b/packages/spec/withdraw/business.spec.ts index 43b4ffbd..85f5a12e 100644 --- a/packages/spec/withdraw/business.spec.ts +++ b/packages/spec/withdraw/business.spec.ts @@ -68,7 +68,7 @@ describe('Withdrawing Assets on Aave V4', () => { assertSingleElementArray(withdrawResult.value); expect( withdrawResult.value[0].withdrawable.amount.value, - ).toBeBigDecimalCloseTo(amountToSupply.minus(amountToWithdraw)); + ).toBeBigDecimalCloseTo(amountToSupply.minus(amountToWithdraw), 2); const balanceAfter = await getBalance( evmAddress(user.account.address), diff --git a/vitest.d.ts b/vitest.d.ts index 53ce0245..810d5d86 100644 --- a/vitest.d.ts +++ b/vitest.d.ts @@ -2,7 +2,7 @@ import 'vitest'; declare module 'vitest' { interface AsymmetricMatchersContaining extends JestExtendedMatchers { - toBeBigDecimalCloseTo: (expected: number | string, precision?: number) => R; + toBeBigDecimalCloseTo: (expected: number | string, precision: number) => R; toBeBigDecimalGreaterThan: (expected: number | string) => R; toBeBigDecimalLessThan: (expected: number | string) => R; toBeBetweenDates: (start: Date, end: Date) => R; diff --git a/vitest.setup.ts b/vitest.setup.ts index a2006fcf..4b777a1a 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -8,7 +8,7 @@ expect.extend({ toBeBigDecimalCloseTo( received: BigDecimal, expected: BigDecimal, - precision = 2, + precision: number, ) { const pass = received.round(precision).eq(expected.round(precision)); From 1b6d4e5dcec65502491ed7ff1e4c3378eb1a2140 Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Sun, 21 Dec 2025 17:54:29 +0100 Subject: [PATCH 10/14] refactor: inline recreateUserPositionInOneSpoke helper into userPositions spec --- packages/spec/positions/helper.ts | 80 ---- .../spec/positions/math/userPositions.spec.ts | 376 +++++++++--------- 2 files changed, 197 insertions(+), 259 deletions(-) diff --git a/packages/spec/positions/helper.ts b/packages/spec/positions/helper.ts index 572724e6..c97092e2 100644 --- a/packages/spec/positions/helper.ts +++ b/packages/spec/positions/helper.ts @@ -390,83 +390,3 @@ export const recreateUserPositions = async ( assertOk(resultEmodeSpoke); } }; - -export const recreateUserPositionInOneSpoke = async ( - client: AaveClient, - user: WalletClient, -) => { - // Check the user has at least one - const supplies = await userSupplies(client, { - query: { - userSpoke: { - spoke: ETHEREUM_SPOKE_CORE_ID, - user: evmAddress(user.account.address), - }, - }, - }); - assertOk(supplies); - const userInfo = await userPositions(client, { - user: evmAddress(user.account.address), - filter: { - chainIds: [ETHEREUM_FORK_ID], - }, - }); - assertOk(userInfo); - - // Create a position in the spoke - if ( - supplies.value.length < 3 || - // Add supply if health factor is less than 4 - userInfo.value[0]!.healthFactor.current!.lt(4) - ) { - const supplyGHOCollateral = await findReserveAndSupply(client, user, { - spoke: ETHEREUM_SPOKE_CORE_ID, - token: ETHEREUM_GHO_ADDRESS, - asCollateral: true, - amount: bigDecimal('100'), - }); - assertOk(supplyGHOCollateral); - - const supplyUSDCDNoCollateral = await findReserveAndSupply(client, user, { - spoke: ETHEREUM_SPOKE_CORE_ID, - token: ETHEREUM_USDC_ADDRESS, - asCollateral: false, - amount: bigDecimal('100'), - }); - assertOk(supplyUSDCDNoCollateral); - - const supplyWETHCollateral = await findReserveAndSupply(client, user, { - spoke: ETHEREUM_SPOKE_CORE_ID, - token: ETHEREUM_AAVE_ADDRESS, - asCollateral: true, - amount: bigDecimal('0.5'), - }); - assertOk(supplyWETHCollateral); - } - - const borrows = await userBorrows(client, { - query: { - userSpoke: { - spoke: ETHEREUM_SPOKE_CORE_ID, - user: evmAddress(user.account.address), - }, - }, - }); - assertOk(borrows); - - if (borrows.value.length < 2) { - const borrowAAVE = await borrowFromRandomReserve(client, user, { - spoke: ETHEREUM_SPOKE_CORE_ID, - token: ETHEREUM_AAVE_ADDRESS, - ratioToBorrow: 0.1, - }); - assertOk(borrowAAVE); - - const borrowWETH = await borrowFromRandomReserve(client, user, { - spoke: ETHEREUM_SPOKE_CORE_ID, - token: ETHEREUM_WETH_ADDRESS, - ratioToBorrow: 0.1, - }); - assertOk(borrowWETH); - } -}; diff --git a/packages/spec/positions/math/userPositions.spec.ts b/packages/spec/positions/math/userPositions.spec.ts index be3a8ece..fd8bebca 100644 --- a/packages/spec/positions/math/userPositions.spec.ts +++ b/packages/spec/positions/math/userPositions.spec.ts @@ -10,116 +10,138 @@ import { userBorrows, userPositions, userSupplies } from '@aave/client/actions'; import { client, createNewWallet, + ETHEREUM_AAVE_ADDRESS, ETHEREUM_FORK_ID, + ETHEREUM_GHO_ADDRESS, ETHEREUM_SPOKE_CORE_ADDRESS, + ETHEREUM_SPOKE_CORE_ID, + ETHEREUM_USDC_ADDRESS, + ETHEREUM_WETH_ADDRESS, } from '@aave/client/testing'; import { beforeAll, describe, expect, it } from 'vitest'; - +import { + borrowFromRandomReserve, + findReserveAndSupply, +} from '../../helpers/supplyBorrow'; import { assertNonEmptyArray, assertSingleElementArray, } from '../../test-utils'; -import { recreateUserPositionInOneSpoke } from '../helper'; import { getAccountData, type UserAccountData } from './helper'; const user = await createNewWallet( '0xbae6035617e696766fc0a0739508200144f6e785600cc155496ddfc1d78a6a14', ); -describe('Check User Positions Math on Aave V4', () => { - describe('Given a user with multiple deposits and at least two borrows in one spoke', () => { - beforeAll(async () => { - await recreateUserPositionInOneSpoke(client, user); - }, 180_000); +describe('Given a user with a User Position on a Spoke', () => { + describe('With 3 supply positions, 2 of which set as collateral', () => { + let suppliesPositions: UserSupplyItem[]; - describe('When fetching the user positions for the user', () => { - let positions: UserPosition; - let suppliesPositions: UserSupplyItem[]; + beforeAll(async () => { + const resultSupplies = await userSupplies(client, { + query: { + userSpoke: { + spoke: ETHEREUM_SPOKE_CORE_ID, + user: evmAddress(user.account.address), + }, + }, + }); + assertOk(resultSupplies); + suppliesPositions = resultSupplies.value; + + if (suppliesPositions.length < 3) { + const supplyGHOCollateral = await findReserveAndSupply(client, user, { + spoke: ETHEREUM_SPOKE_CORE_ID, + token: ETHEREUM_GHO_ADDRESS, + asCollateral: true, + amount: bigDecimal('100'), + }); + assertOk(supplyGHOCollateral); + + const supplyUSDCDNoCollateral = await findReserveAndSupply( + client, + user, + { + spoke: ETHEREUM_SPOKE_CORE_ID, + token: ETHEREUM_USDC_ADDRESS, + asCollateral: true, + amount: bigDecimal('100'), + }, + ); + assertOk(supplyUSDCDNoCollateral); + + const supplyWETHCollateral = await findReserveAndSupply(client, user, { + spoke: ETHEREUM_SPOKE_CORE_ID, + token: ETHEREUM_AAVE_ADDRESS, + asCollateral: false, + amount: bigDecimal('0.5'), + }); + assertOk(supplyWETHCollateral); + } + }, 100_000); + + describe('And 2 borrow positions', () => { let borrowPositions: UserBorrowItem[]; - let accountDataOnChain: UserAccountData; beforeAll(async () => { - const [ - positionsResult, - suppliesResult, - borrowResult, - accountDataResult, - ] = await Promise.all([ - userPositions(client, { - user: evmAddress(user.account.address), - filter: { - chainIds: [ETHEREUM_FORK_ID], - }, - }), - userSupplies(client, { - query: { - userChains: { - chainIds: [ETHEREUM_FORK_ID], - user: evmAddress(user.account.address), - }, - }, - }), - userBorrows(client, { - query: { - userChains: { - chainIds: [ETHEREUM_FORK_ID], - user: evmAddress(user.account.address), - }, + const borrows = await userBorrows(client, { + query: { + userSpoke: { + spoke: ETHEREUM_SPOKE_CORE_ID, + user: evmAddress(user.account.address), }, - }), - getAccountData( - evmAddress(user.account.address), - ETHEREUM_SPOKE_CORE_ADDRESS, - ), - ]); - - assertOk(positionsResult); - assertNonEmptyArray(positionsResult.value); - // We only operate on one spoke, so we expect a single element array - assertSingleElementArray(positionsResult.value); - positions = positionsResult.value[0]; - - assertOk(suppliesResult); - assertNonEmptyArray(suppliesResult.value); - suppliesPositions = suppliesResult.value; - - assertOk(borrowResult); - assertNonEmptyArray(borrowResult.value); - borrowPositions = borrowResult.value; - - accountDataOnChain = accountDataResult; + }, + }); + assertOk(borrows); + borrowPositions = borrows.value; + + if (borrows.value.length < 2) { + const borrowAAVE = await borrowFromRandomReserve(client, user, { + spoke: ETHEREUM_SPOKE_CORE_ID, + token: ETHEREUM_AAVE_ADDRESS, + ratioToBorrow: 0.1, + }); + assertOk(borrowAAVE); + + const borrowWETH = await borrowFromRandomReserve(client, user, { + spoke: ETHEREUM_SPOKE_CORE_ID, + token: ETHEREUM_WETH_ADDRESS, + ratioToBorrow: 0.1, + }); + assertOk(borrowWETH); + } }, 180_000); - it('Then it should return the correct totalSupplied value', async () => { - // total supplied is the sum of the principal and interest for all positions in the spoke - const totalSupplied = suppliesPositions.reduce( - (acc, supply) => - acc.plus( - supply.principal.exchange.value.plus( - supply.interest.exchange.value, - ), + describe('When fetching the user positions for the user', () => { + let position: UserPosition; + let accountDataOnChain: UserAccountData; + + beforeAll(async () => { + const [positionsResult, accountDataResult] = await Promise.all([ + userPositions(client, { + user: evmAddress(user.account.address), + filter: { + chainIds: [ETHEREUM_FORK_ID], + }, + }), + getAccountData( + evmAddress(user.account.address), + ETHEREUM_SPOKE_CORE_ADDRESS, ), - bigDecimal('0'), - ); - expect(totalSupplied).toBeBigDecimalCloseTo( - positions.totalSupplied.current.value, - 1, - ); - }); + ]); - it('Then it should return the correct totalCollateral value', async () => { - // Cross check with the account data on chain - expect(positions.totalCollateral.current.value).toBeBigDecimalCloseTo( - accountDataOnChain.totalCollateralValue, - 1, - ); - }); + assertOk(positionsResult); + assertNonEmptyArray(positionsResult.value); + // We only operate on one spoke, so we expect a single element array + assertSingleElementArray(positionsResult.value); + position = positionsResult.value[0]; + + accountDataOnChain = accountDataResult; + }, 180_000); - it('Then it should return the correct netCollateral value', async () => { - // net collateral is the sum of the total collateral minus the total debt - const totalCollateral = suppliesPositions - .filter((supply) => supply.isCollateral) - .reduce( + it('Then it should return the correct totalSupplied value', async () => { + // total supplied is the sum of the principal and interest for all positions in the spoke + const totalSupplied = suppliesPositions.reduce( (acc, supply) => acc.plus( supply.principal.exchange.value.plus( @@ -128,109 +150,105 @@ describe('Check User Positions Math on Aave V4', () => { ), bigDecimal('0'), ); - const totalDebt = borrowPositions.reduce( - (acc, borrow) => - acc.plus( - borrow.debt.exchange.value.plus(borrow.interest.exchange.value), - ), - bigDecimal('0'), - ); - - // Cross check with the user positions - expect(totalCollateral.minus(totalDebt)).toBeBigDecimalCloseTo( - positions.netCollateral.current.value, - 1, - ); - }); - - it('Then it should return the correct totalDebt value', async () => { - // total debt is the sum of the principal and interest for all positions in the spoke - expect(positions.totalDebt.current.value).toBeBigDecimalCloseTo( - accountDataOnChain.totalDebtValue, - 1, - ); - }); + expect(totalSupplied).toBeBigDecimalCloseTo( + position.totalSupplied.current.value, + 1, + ); + }); - it('Then it should return the correct netBalance value', async () => { - // net balance is the sum of the total supplied minus the borrows (debt) - const totalSupplied = suppliesPositions.reduce( - (acc, supply) => - acc.plus( - supply.principal.exchange.value.plus( - supply.interest.exchange.value, + it('Then it should return the correct totalCollateral value', async () => { + // Cross check with the account data on chain + expect(position.totalCollateral.current.value).toBeBigDecimalCloseTo( + accountDataOnChain.totalCollateralValue, + 1, + ); + }); + + it('Then it should return the correct netCollateral value', async () => { + // net collateral is the sum of the total collateral minus the total debt + const totalCollateral = suppliesPositions + .filter((supply) => supply.isCollateral) + .reduce( + (acc, supply) => + acc.plus( + supply.principal.exchange.value.plus( + supply.interest.exchange.value, + ), + ), + bigDecimal('0'), + ); + const totalDebt = borrowPositions.reduce( + (acc, borrow) => + acc.plus( + borrow.debt.exchange.value.plus(borrow.interest.exchange.value), ), - ), - bigDecimal('0'), - ); - - const totalDebt = borrowPositions.reduce( - (acc, borrow) => - acc.plus( - borrow.debt.exchange.value.plus(borrow.interest.exchange.value), - ), - bigDecimal('0'), - ); + bigDecimal('0'), + ); - expect(totalSupplied.minus(totalDebt)).toBeBigDecimalCloseTo( - positions.netBalance.current.value, - 1, - ); - }); + // Cross check with the user positions + expect(totalCollateral.minus(totalDebt)).toBeBigDecimalCloseTo( + position.netCollateral.current.value, + 1, + ); + }); - it('Then it should return the correct health factor', async () => { - // Cross check with the user positions - expect(positions.healthFactor.current).toBeBigDecimalCloseTo( - accountDataOnChain.healthFactor, - 2, - ); - }); + it('Then it should return the correct totalDebt value', async () => { + // total debt is the sum of the principal and interest for all positions in the spoke + expect(position.totalDebt.current.value).toBeBigDecimalCloseTo( + accountDataOnChain.totalDebtValue, + 1, + ); + }); - it('Then it should return the correct averageCollateralFactor value', async () => { - const collateralPositions = suppliesPositions.filter( - (supply) => supply.isCollateral, - ); + it('Then it should return the correct netBalance value', async () => { + // net balance is the sum of the total supplied minus the borrows (debt) + const totalSupplied = suppliesPositions.reduce( + (acc, supply) => + acc.plus( + supply.principal.exchange.value.plus( + supply.interest.exchange.value, + ), + ), + bigDecimal('0'), + ); - const { weightedSum, totalValue } = collateralPositions.reduce( - (acc, supply) => { - const collateralValue = supply.principal.exchange.value.plus( - supply.interest.exchange.value, - ); - const collateralFactor = - supply.reserve.settings.collateralFactor.value; - return { - weightedSum: acc.weightedSum.plus( - collateralFactor.times(collateralValue), + const totalDebt = borrowPositions.reduce( + (acc, borrow) => + acc.plus( + borrow.debt.exchange.value.plus(borrow.interest.exchange.value), ), - totalValue: acc.totalValue.plus(collateralValue), - }; - }, - { - weightedSum: bigDecimal('0'), - totalValue: bigDecimal('0'), - }, - ); + bigDecimal('0'), + ); - // Normalize: avgCollateralFactor = weightedSum / totalValue - const averageCollateralFactor = weightedSum.div(totalValue); + expect(totalSupplied.minus(totalDebt)).toBeBigDecimalCloseTo( + position.netBalance.current.value, + 1, + ); + }); - // Cross check with the account data on chain - expect(averageCollateralFactor).toBeBigDecimalCloseTo( - accountDataOnChain.avgCollateralFactor, - 5, - ); - // Cross check with the user positions - expect(averageCollateralFactor).toBeBigDecimalCloseTo( - positions.averageCollateralFactor.value, - 5, - ); - }); + it('Then it should return the correct health factor', async () => { + // Cross check with the user positions + expect(position.healthFactor.current).toBeBigDecimalCloseTo( + accountDataOnChain.healthFactor, + 2, + ); + }); - it.todo('Then it should return the correct netApy value'); - it.todo('Then it should return the correct netSupplyApy value'); - it.todo('Then it should return the correct netBorrowApy value'); - it.todo('Then it should return the correct riskPremium value'); - it.todo('Then it should return the correct liquidationPrice value'); - it.todo('Then it should return the correct borrowingPower value'); + it('Then it should return the correct averageCollateralFactor value', async () => { + // Cross check with the user positions + expect(position.averageCollateralFactor.value).toBeBigDecimalCloseTo( + accountDataOnChain.avgCollateralFactor, + 5, + ); + }); + + it.todo('Then it should return the correct netApy value'); + it.todo('Then it should return the correct netSupplyApy value'); + it.todo('Then it should return the correct netBorrowApy value'); + it.todo('Then it should return the correct riskPremium value'); + it.todo('Then it should return the correct liquidationPrice value'); + it.todo('Then it should return the correct borrowingPower value'); + }); }); }); }); From ecc9895476528e3b4db587907c5a03f1692e1ca1 Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Sun, 21 Dec 2025 17:55:49 +0100 Subject: [PATCH 11/14] fix: remove not needed imports --- packages/spec/positions/helper.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/spec/positions/helper.ts b/packages/spec/positions/helper.ts index c97092e2..a37b59cf 100644 --- a/packages/spec/positions/helper.ts +++ b/packages/spec/positions/helper.ts @@ -16,12 +16,9 @@ import { userSupplies, } from '@aave/client/actions'; import { - ETHEREUM_AAVE_ADDRESS, ETHEREUM_FORK_ID, - ETHEREUM_GHO_ADDRESS, ETHEREUM_SPOKE_CORE_ID, ETHEREUM_SPOKE_ETHENA_ID, - ETHEREUM_USDC_ADDRESS, ETHEREUM_WETH_ADDRESS, fundErc20Address, } from '@aave/client/testing'; @@ -33,7 +30,6 @@ import { findReservesToSupply, } from '../helpers/reserves'; import { - borrowFromRandomReserve, borrowFromReserve, findReserveAndSupply, supplyToReserve, From 44ad3f15af8c8371d94c8932c7d4295a6ed62abd Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Mon, 5 Jan 2026 11:03:14 +0100 Subject: [PATCH 12/14] fix: use a publicClient instead of exporting devnet --- packages/client/src/testing.ts | 9 ++++++++- packages/spec/positions/math/helper.ts | 12 ++++-------- packages/spec/positions/math/userPositions.spec.ts | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/client/src/testing.ts b/packages/client/src/testing.ts index 4da8f0a2..71e1e36a 100644 --- a/packages/client/src/testing.ts +++ b/packages/client/src/testing.ts @@ -118,7 +118,7 @@ export const client = AaveClient.create({ environment, }); -export const devnetChain = await chain(client, { chainId: ETHEREUM_FORK_ID }) +const devnetChain = await chain(client, { chainId: ETHEREUM_FORK_ID }) .map(nonNullable) .map(toViemChain) .match( @@ -126,6 +126,13 @@ export 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> { diff --git a/packages/spec/positions/math/helper.ts b/packages/spec/positions/math/helper.ts index eb1fa260..6161dd08 100644 --- a/packages/spec/positions/math/helper.ts +++ b/packages/spec/positions/math/helper.ts @@ -1,6 +1,6 @@ import { type BigDecimal, bigDecimal } from '@aave/client'; -import { devnetChain, ETHEREUM_FORK_RPC_URL } from '@aave/client/testing'; -import { type Address, createPublicClient, http } from 'viem'; +import { createForkPublicClient } from '@aave/client/testing'; +import type { Address } from 'viem'; // Constants const WAD = 10n ** 18n; // 1e18 = 1.0 in WAD format @@ -60,12 +60,8 @@ export async function getAccountData( address: Address, spoke: Address, ): Promise { - const publicClient = createPublicClient({ - chain: devnetChain, - transport: http(ETHEREUM_FORK_RPC_URL), - }); - - const result = await publicClient.readContract({ + const forkPublicClient = await createForkPublicClient(); + const result = await forkPublicClient.readContract({ address: spoke, abi: userAccountDataABI, functionName: 'getUserAccountData', diff --git a/packages/spec/positions/math/userPositions.spec.ts b/packages/spec/positions/math/userPositions.spec.ts index fd8bebca..1959ed11 100644 --- a/packages/spec/positions/math/userPositions.spec.ts +++ b/packages/spec/positions/math/userPositions.spec.ts @@ -152,7 +152,7 @@ describe('Given a user with a User Position on a Spoke', () => { ); expect(totalSupplied).toBeBigDecimalCloseTo( position.totalSupplied.current.value, - 1, + 2, ); }); From 572e901730705f316ef8287cc099acadf3d04a5d Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Mon, 5 Jan 2026 13:33:03 +0100 Subject: [PATCH 13/14] test: check riskPremium value --- .../spec/positions/math/userPositions.spec.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/spec/positions/math/userPositions.spec.ts b/packages/spec/positions/math/userPositions.spec.ts index 1959ed11..08031adb 100644 --- a/packages/spec/positions/math/userPositions.spec.ts +++ b/packages/spec/positions/math/userPositions.spec.ts @@ -157,7 +157,6 @@ describe('Given a user with a User Position on a Spoke', () => { }); it('Then it should return the correct totalCollateral value', async () => { - // Cross check with the account data on chain expect(position.totalCollateral.current.value).toBeBigDecimalCloseTo( accountDataOnChain.totalCollateralValue, 1, @@ -185,7 +184,6 @@ describe('Given a user with a User Position on a Spoke', () => { bigDecimal('0'), ); - // Cross check with the user positions expect(totalCollateral.minus(totalDebt)).toBeBigDecimalCloseTo( position.netCollateral.current.value, 1, @@ -227,7 +225,6 @@ describe('Given a user with a User Position on a Spoke', () => { }); it('Then it should return the correct health factor', async () => { - // Cross check with the user positions expect(position.healthFactor.current).toBeBigDecimalCloseTo( accountDataOnChain.healthFactor, 2, @@ -235,19 +232,24 @@ describe('Given a user with a User Position on a Spoke', () => { }); it('Then it should return the correct averageCollateralFactor value', async () => { - // Cross check with the user positions expect(position.averageCollateralFactor.value).toBeBigDecimalCloseTo( accountDataOnChain.avgCollateralFactor, - 5, + 2, + ); + }); + + it('Then it should return the correct riskPremium value', async () => { + expect(position.riskPremium?.current.value).toBeBigDecimalCloseTo( + accountDataOnChain.riskPremium, + 2, ); }); it.todo('Then it should return the correct netApy value'); it.todo('Then it should return the correct netSupplyApy value'); it.todo('Then it should return the correct netBorrowApy value'); - it.todo('Then it should return the correct riskPremium value'); - it.todo('Then it should return the correct liquidationPrice value'); it.todo('Then it should return the correct borrowingPower value'); + it.todo('Then it should return the correct liquidationPrice value'); }); }); }); From d8131cf6ffee30714e8b7edc7496322278fe1624 Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Mon, 5 Jan 2026 17:07:03 +0100 Subject: [PATCH 14/14] fix: typo and rename test description --- packages/spec/positions/math/userPositions.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/spec/positions/math/userPositions.spec.ts b/packages/spec/positions/math/userPositions.spec.ts index 08031adb..2c723cbf 100644 --- a/packages/spec/positions/math/userPositions.spec.ts +++ b/packages/spec/positions/math/userPositions.spec.ts @@ -112,7 +112,7 @@ describe('Given a user with a User Position on a Spoke', () => { } }, 180_000); - describe('When fetching the user positions for the user', () => { + describe('When fetching the User Position data', () => { let position: UserPosition; let accountDataOnChain: UserAccountData; @@ -224,7 +224,7 @@ describe('Given a user with a User Position on a Spoke', () => { ); }); - it('Then it should return the correct health factor', async () => { + it('Then it should return the correct healthFactor', async () => { expect(position.healthFactor.current).toBeBigDecimalCloseTo( accountDataOnChain.healthFactor, 2,