-
Notifications
You must be signed in to change notification settings - Fork 5
test: check userPositions match (totalCollateral, totalSupplied, totalDebt, netBalance) #212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| amount: BigDecimal; | ||
| spoke?: SpokeId; | ||
| asCollateral?: boolean; | ||
| autoFund?: boolean; |
There was a problem hiding this comment.
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).
packages/client/src/testing.ts
Outdated
| }); | ||
|
|
||
| const devnetChain = await chain(client, { chainId: ETHEREUM_FORK_ID }) | ||
| export const devnetChain = await chain(client, { chainId: ETHEREUM_FORK_ID }) |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| let positions: UserPosition; | |
| let position: UserPosition; |
| 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, | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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, | |
| ); | |
| }); |
| // 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, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // 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.
| // 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, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // 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, | |
| ); |
| // 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, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // 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], | ||
| }, | ||
| }), |
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
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.
No description provided.