Skip to content

Conversation

@juangm
Copy link
Collaborator

@juangm juangm commented Dec 18, 2025

No description provided.

@juangm juangm requested a review from cesarenaldi December 18, 2025 11:09
amount: BigDecimal;
spoke?: SpokeId;
asCollateral?: boolean;
autoFund?: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this and make this function always fund the given wallet prior to supply.

Need to review other places this helper is used, or should be used (e.g., withdraw scenarios).

});

const devnetChain = await chain(client, { chainId: ETHEREUM_FORK_ID })
export const devnetChain = await chain(client, { chainId: ETHEREUM_FORK_ID })
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to export a PublicClient already wired instead of the devnetChain.

}, 180_000);

describe('When fetching the user positions for the user', () => {
let positions: UserPosition;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let positions: UserPosition;
let position: UserPosition;

Comment on lines 260 to 298
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,
);
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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('Then it should return the correct averageCollateralFactor value', async () => {
expect(position.averageCollateralFactor.value).toBeBigDecimalCloseTo(
accountDataOnChain.avgCollateralFactor,
5,
);
});

Comment on lines 213 to 257
// 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'),
);

// 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,
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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'),
);
// 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,
);
}
expect(position.healthFactor.current).toBeBigDecimalCloseTo(
accountDataOnChain.healthFactor,
2
);
}

And make the toBeBigDecimalCloseTo have a mandatory second parameter.

Comment on lines 165 to 183
// 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,
1,
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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,
1,
);
expect(position.totalDebt.current.value).toBeBigDecimalCloseTo(
accountDataOnChain.totalDebtValue,
1,
);

Comment on lines 111 to 133
// 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,
1,
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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,
1,
);
expect(positions.totalCollateral.current.value).toBeBigDecimalCloseTo(
accountDataOnChain.totalCollateralValue,
1,
);

filter: {
chainIds: [ETHEREUM_FORK_ID],
},
}),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a userPosition by UserSpokeInput so we save ourself another 3 LOC, include .map(nonNullable)

);

describe('Check User Positions Math on Aave V4', () => {
describe('Given a user with multiple deposits and at least two borrows in one spoke', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given a user with a User Position on a Spoke
  With 3 supply positions, 2 of which set as collateral
    [before all creates deposits]
    And 2 borrow positions
      [before all creates loans]
      When fetching the User Position data
         [fetch data]
          ...

}),
userSupplies(client, {
query: {
userChains: {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leverage Spoke Id once the setup is reframed as before alls in this same file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants