-
Notifications
You must be signed in to change notification settings - Fork 88
feat: add campaign apy #5803
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
Open
cuzz-venus
wants to merge
4
commits into
main
Choose a base branch
from
feat/collateral-gated-borrow-rewards
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
feat: add campaign apy #5803
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@venusprotocol/evm": minor | ||
| --- | ||
|
|
||
| display the per-user reward APY of collateral-gated Merkl borrow campaigns |
202 changes: 202 additions & 0 deletions
202
.../useGetPools/useGetPoolsQuery/getPools/appendMerklCollateralGates/__tests__/index.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,202 @@ | ||
| import BigNumber from 'bignumber.js'; | ||
|
|
||
| import { assetData } from '__mocks__/models/asset'; | ||
| import { isolatedPool, legacyCorePool } from '__mocks__/models/pools'; | ||
| import { xvs } from '__mocks__/models/tokens'; | ||
| import { vUsdc, vUsdtCorePool, vXvs } from '__mocks__/models/vTokens'; | ||
| import type { Asset, MerklDistribution, Pool, VToken } from 'types'; | ||
|
|
||
| import { appendMerklCollateralGates } from '..'; | ||
|
|
||
| const buildGatedDistribution = (): MerklDistribution => ({ | ||
| type: 'merkl', | ||
| token: xvs, | ||
| apyPercentage: new BigNumber(20), | ||
| dailyDistributedTokens: new BigNumber(0), | ||
| isActive: true, | ||
| rewardDetails: { | ||
| appName: 'Merkl', | ||
| claimUrl: 'https://app.merkl.xyz/', | ||
| marketAddress: vUsdtCorePool.address, | ||
| merklCampaignIdentifier: '0xfake', | ||
| description: 'Merkl campaign', | ||
| tags: [], | ||
| aprPercentage: 20, | ||
| participatingCollateralAddresses: [vXvs.address], | ||
| eligibleBorrowMarketAddresses: [vUsdtCorePool.address, vUsdc.address], | ||
| }, | ||
| }); | ||
|
|
||
| const buildAsset = ({ vToken, ...overrides }: { vToken: VToken } & Partial<Asset>): Asset => ({ | ||
| ...assetData[0], | ||
| vToken, | ||
| isCollateralOfUser: false, | ||
| userSupplyBalanceCents: new BigNumber(0), | ||
| userBorrowBalanceCents: new BigNumber(0), | ||
| supplyTokenDistributions: [], | ||
| borrowTokenDistributions: [], | ||
| ...overrides, | ||
| }); | ||
|
|
||
| // $1000 of collateral at a 60% collateral factor, against $500 of USDT and $500 of USDC borrows | ||
| const buildPools = ({ | ||
| collateralCents = new BigNumber(100000), | ||
| isCollateralOfUser = true, | ||
| usdtBorrowCents = new BigNumber(50000), | ||
| usdcBorrowCents = new BigNumber(50000), | ||
| }: { | ||
| collateralCents?: BigNumber; | ||
| isCollateralOfUser?: boolean; | ||
| usdtBorrowCents?: BigNumber; | ||
| usdcBorrowCents?: BigNumber; | ||
| } = {}): Pool[] => [ | ||
| { | ||
| ...legacyCorePool, | ||
| assets: [ | ||
| buildAsset({ | ||
| vToken: vXvs, | ||
| isCollateralOfUser, | ||
| userSupplyBalanceCents: collateralCents, | ||
| userCollateralFactor: 0.6, | ||
| }), | ||
| buildAsset({ | ||
| vToken: vUsdtCorePool, | ||
| userBorrowBalanceCents: usdtBorrowCents, | ||
| borrowTokenDistributions: [buildGatedDistribution()], | ||
| }), | ||
| buildAsset({ | ||
| vToken: vUsdc, | ||
| userBorrowBalanceCents: usdcBorrowCents, | ||
| borrowTokenDistributions: [buildGatedDistribution()], | ||
| }), | ||
| ], | ||
| }, | ||
| ]; | ||
|
|
||
| const getGate = (pools: Pool[], assetIndex: number) => { | ||
| const distribution = pools[0].assets[assetIndex].borrowTokenDistributions[0]; | ||
| return distribution.type === 'merkl' ? distribution : undefined; | ||
| }; | ||
|
|
||
| describe('appendMerklCollateralGates', () => { | ||
| it('scales the campaign APR down by the share of the loan the collateral covers', () => { | ||
| const pools = buildPools(); | ||
|
|
||
| appendMerklCollateralGates({ pools }); | ||
|
|
||
| const distribution = getGate(pools, 1); | ||
| expect(distribution?.collateralGate?.isUserEligible).toBe(true); | ||
| expect(distribution?.collateralGate?.maxApyPercentage.toFixed()).toBe('20'); | ||
| // 20% * min($600, $1000) / $1000 | ||
| expect(distribution?.apyPercentage.toFixed()).toBe('12'); | ||
| }); | ||
|
|
||
| it('applies the same reward APY to every eligible borrow market of the campaign', () => { | ||
| const pools = buildPools(); | ||
|
|
||
| appendMerklCollateralGates({ pools }); | ||
|
|
||
| expect(getGate(pools, 2)?.apyPercentage.toFixed()).toBe('12'); | ||
| }); | ||
|
|
||
| it('awards the full campaign APR when the collateral covers the whole loan', () => { | ||
| const pools = buildPools({ collateralCents: new BigNumber(1000000) }); | ||
|
|
||
| appendMerklCollateralGates({ pools }); | ||
|
|
||
| expect(getGate(pools, 1)?.apyPercentage.toFixed()).toBe('20'); | ||
| }); | ||
|
|
||
| it('marks the user as ineligible when they hold none of the participating collateral', () => { | ||
| const pools = buildPools({ collateralCents: new BigNumber(0) }); | ||
|
|
||
| appendMerklCollateralGates({ pools }); | ||
|
|
||
| const distribution = getGate(pools, 1); | ||
| expect(distribution?.collateralGate?.isUserEligible).toBe(false); | ||
| expect(distribution?.apyPercentage.toFixed()).toBe('0'); | ||
| }); | ||
|
|
||
| it('marks the user as ineligible when the participating collateral is not enabled', () => { | ||
| const pools = buildPools({ isCollateralOfUser: false }); | ||
|
|
||
| appendMerklCollateralGates({ pools }); | ||
|
|
||
| expect(getGate(pools, 1)?.collateralGate?.isUserEligible).toBe(false); | ||
| }); | ||
|
|
||
| it('marks the user as ineligible when they borrow none of the eligible markets', () => { | ||
| const pools = buildPools({ | ||
| usdtBorrowCents: new BigNumber(0), | ||
| usdcBorrowCents: new BigNumber(0), | ||
| }); | ||
|
|
||
| appendMerklCollateralGates({ pools }); | ||
|
|
||
| const distribution = getGate(pools, 1); | ||
| expect(distribution?.collateralGate?.isUserEligible).toBe(false); | ||
| expect(distribution?.apyPercentage.toFixed()).toBe('0'); | ||
| }); | ||
|
|
||
| it('leaves ungated Merkl campaigns untouched', () => { | ||
| const ungatedDistribution: MerklDistribution = { | ||
| ...buildGatedDistribution(), | ||
| apyPercentage: new BigNumber(5), | ||
| rewardDetails: { | ||
| ...buildGatedDistribution().rewardDetails, | ||
| participatingCollateralAddresses: [], | ||
| eligibleBorrowMarketAddresses: [], | ||
| }, | ||
| }; | ||
|
|
||
| const pools: Pool[] = [ | ||
| { | ||
| ...legacyCorePool, | ||
| assets: [ | ||
| buildAsset({ | ||
| vToken: vUsdtCorePool, | ||
| borrowTokenDistributions: [ungatedDistribution], | ||
| }), | ||
| ], | ||
| }, | ||
| ]; | ||
|
|
||
| appendMerklCollateralGates({ pools }); | ||
|
|
||
| const distribution = getGate(pools, 0); | ||
| expect(distribution?.collateralGate).toBeUndefined(); | ||
| expect(distribution?.apyPercentage.toFixed()).toBe('5'); | ||
| }); | ||
|
|
||
| it('ignores collateral held in another pool, which cannot back the borrow', () => { | ||
| const pools: Pool[] = [ | ||
| { | ||
| ...legacyCorePool, | ||
| assets: [ | ||
| buildAsset({ | ||
| vToken: vUsdtCorePool, | ||
| userBorrowBalanceCents: new BigNumber(50000), | ||
| borrowTokenDistributions: [buildGatedDistribution()], | ||
| }), | ||
| ], | ||
| }, | ||
| { | ||
| ...isolatedPool, | ||
| assets: [ | ||
| buildAsset({ | ||
| vToken: vXvs, | ||
| isCollateralOfUser: true, | ||
| userSupplyBalanceCents: new BigNumber(100000), | ||
| userCollateralFactor: 0.6, | ||
| }), | ||
| ], | ||
| }, | ||
| ]; | ||
|
|
||
| appendMerklCollateralGates({ pools }); | ||
|
|
||
| const distribution = getGate(pools, 0); | ||
| expect(distribution?.collateralGate?.isUserEligible).toBe(false); | ||
| expect(distribution?.apyPercentage.toFixed()).toBe('0'); | ||
| }); | ||
| }); |
62 changes: 62 additions & 0 deletions
62
...nts/api/queries/useGetPools/useGetPoolsQuery/getPools/appendMerklCollateralGates/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import BigNumber from 'bignumber.js'; | ||
|
|
||
| import type { Pool } from 'types'; | ||
| import { areAddressesEqual } from 'utilities'; | ||
|
|
||
| export const appendMerklCollateralGates = ({ pools }: { pools: Pool[] }) => { | ||
| // Collateral only backs borrows made within the same pool, so each pool is resolved on its own | ||
| for (const pool of pools) { | ||
| for (const asset of pool.assets) { | ||
| for (const distribution of asset.borrowTokenDistributions) { | ||
| if (distribution.type !== 'merkl') { | ||
| continue; | ||
| } | ||
|
|
||
| const { | ||
| aprPercentage = 0, | ||
| participatingCollateralAddresses = [], | ||
| eligibleBorrowMarketAddresses = [], | ||
| } = distribution.rewardDetails; | ||
|
|
||
| if (eligibleBorrowMarketAddresses.length === 0) { | ||
| continue; | ||
| } | ||
|
|
||
| const collateralCents = pool.assets.reduce((acc, collateralAsset) => { | ||
| const isParticipating = participatingCollateralAddresses.some(address => | ||
| areAddressesEqual(address, collateralAsset.vToken.address), | ||
| ); | ||
|
|
||
| return collateralAsset.isCollateralOfUser && isParticipating | ||
| ? acc.plus( | ||
| collateralAsset.userSupplyBalanceCents.multipliedBy( | ||
| collateralAsset.userCollateralFactor, | ||
| ), | ||
| ) | ||
| : acc; | ||
| }, new BigNumber(0)); | ||
|
|
||
| const eligibleLoanCents = pool.assets.reduce( | ||
| (acc, borrowAsset) => | ||
| eligibleBorrowMarketAddresses.some(address => | ||
| areAddressesEqual(address, borrowAsset.vToken.address), | ||
| ) | ||
| ? acc.plus(borrowAsset.userBorrowBalanceCents) | ||
| : acc, | ||
| new BigNumber(0), | ||
| ); | ||
|
|
||
| const maxApyPercentage = new BigNumber(aprPercentage); | ||
| const isUserEligible = | ||
| collateralCents.isGreaterThan(0) && eligibleLoanCents.isGreaterThan(0); | ||
|
|
||
| distribution.collateralGate = { isUserEligible, maxApyPercentage }; | ||
| distribution.apyPercentage = isUserEligible | ||
| ? maxApyPercentage | ||
| .multipliedBy(BigNumber.minimum(collateralCents, eligibleLoanCents)) | ||
| .dividedBy(eligibleLoanCents) | ||
| : new BigNumber(0); | ||
| } | ||
| } | ||
| } | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
When a transaction preview changes participating collateral, collateral status, or eligible debt,
getSimulatedPoolcopies the Merkl gate and prorated APY computed for the current pool instead of recalculating them. The form therefore shows stale eligibility, reward APY, and total post-transaction APY—for example, withdrawing the qualifying collateral can leave the reward displayed as active.Knowledge Base Used: Lending transaction flows
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.
fixed