v3MigrationSelectors.ts
1 import { 2 InterestRate, 3 PoolBaseCurrencyHumanized, 4 ReserveDataHumanized, 5 ReservesIncentiveDataHumanized, 6 UserReserveDataHumanized, 7 valueToWei, 8 } from '@aave/contract-helpers'; 9 import { 10 MigrationRepayAsset, 11 MigrationSupplyAsset, 12 V3MigrationHelperSignedCreditDelegationPermit, 13 V3MigrationHelperSignedPermit, 14 } from '@aave/contract-helpers/dist/esm/v3-migration-contract/v3MigrationTypes'; 15 import { 16 ComputedUserReserve, 17 formatReservesAndIncentives, 18 FormatReserveUSDResponse, 19 formatUserSummary, 20 FormatUserSummaryResponse, 21 valueToBigNumber, 22 } from '@aave/math-utils'; 23 import { 24 CalculateReserveIncentivesResponse, 25 ReserveIncentiveResponse, 26 } from '@aave/math-utils/dist/esm/formatters/incentive/calculate-reserve-incentives'; 27 import { SignatureLike } from '@ethersproject/bytes'; 28 import { BigNumber } from 'bignumber.js'; 29 import { BigNumberish } from 'ethers'; 30 import { Approval } from 'src/helpers/useTransactionHandler'; 31 import { 32 BorrowMigrationReserve, 33 SupplyMigrationReserve, 34 UserMigrationReserves, 35 } from 'src/hooks/migration/useUserMigrationReserves'; 36 import { UserSummaryForMigration } from 'src/hooks/migration/useUserSummaryForMigration'; 37 import { FormattedUserReserves } from 'src/hooks/pool/useUserSummaryAndIncentives'; 38 39 import { RootStore } from './root'; 40 import { 41 MigrationException, 42 MigrationSelectedAsset, 43 MigrationSelectedBorrowAsset, 44 } from './v3MigrationSlice'; 45 46 export const selectIsolationModeForMigration = ( 47 poolReserveV3Summary: Pick< 48 FormatUserSummaryResponse<ReserveDataHumanized & FormatReserveUSDResponse>, 49 'totalCollateralMarketReferenceCurrency' | 'isolatedReserve' 50 > 51 ) => { 52 if (poolReserveV3Summary.totalCollateralMarketReferenceCurrency !== '0') { 53 return poolReserveV3Summary.isolatedReserve; 54 } 55 return undefined; 56 }; 57 58 export const selectMigrationSelectedSupplyIndex = ( 59 selectedMigrationSupplyAssets: MigrationSelectedAsset[], 60 underlyingAsset: string 61 ) => { 62 return selectedMigrationSupplyAssets.findIndex( 63 (supplyAsset) => supplyAsset.underlyingAsset == underlyingAsset 64 ); 65 }; 66 67 export const selectMigrationSelectedBorrowIndex = ( 68 selectedBorrowAssets: MigrationSelectedBorrowAsset[], 69 borrowAsset: MigrationSelectedBorrowAsset 70 ) => { 71 return selectedBorrowAssets.findIndex((asset) => asset.debtKey == borrowAsset.debtKey); 72 }; 73 74 export type MigrationUserReserve = FormattedUserReserves & { 75 increasedVariableBorrows: string; 76 interestRate: InterestRate; 77 debtKey: string; 78 usageAsCollateralEnabledOnUserV3?: boolean; 79 isolatedOnV3?: boolean; 80 canBeEnforced?: boolean; 81 migrationDisabled?: MigrationDisabled; 82 V3Rates?: V3Rates; 83 }; 84 85 export type V3Rates = { 86 variableBorrowAPY: string; 87 supplyAPY: string; 88 aIncentivesData?: ReserveIncentiveResponse[]; 89 vIncentivesData?: ReserveIncentiveResponse[]; 90 priceInUSD: string; 91 ltv?: string; 92 liquidationThreshold?: string; 93 }; 94 95 type ReserveDebtApprovalPayload = { 96 [underlyingAsset: string]: { 97 variableDebtTokenAddress: string; 98 decimals: number; 99 variableDebtAmount: string; 100 }; 101 }; 102 103 export const selectSplittedBorrowsForMigration = (userReserves: FormattedUserReserves[]) => { 104 const splittedUserReserves: MigrationUserReserve[] = []; 105 userReserves.forEach((userReserve) => { 106 if (userReserve.variableBorrows !== '0') { 107 splittedUserReserves.push({ 108 ...userReserve, 109 interestRate: InterestRate.Variable, 110 increasedVariableBorrows: userReserve.variableBorrows, 111 debtKey: userReserve.reserve.variableDebtTokenAddress, 112 }); 113 } 114 }); 115 return splittedUserReserves; 116 }; 117 118 export const selectDefinitiveSupplyAssetForMigration = ( 119 selectedMigrationSupplyAssets: MigrationSelectedAsset[], 120 migrationExceptions: Record<string, MigrationException>, 121 userReservesV3Map: Record< 122 string, 123 ComputedUserReserve<ReserveDataHumanized & FormatReserveUSDResponse> 124 > 125 ) => { 126 const enforcedAssets = selectedMigrationSupplyAssets.filter( 127 (supplyAsset) => supplyAsset.enforced 128 ); 129 130 if (enforcedAssets.length > 0) { 131 return enforcedAssets; 132 } 133 134 const nonIsolatedAssets = selectedMigrationSupplyAssets.filter((supplyAsset) => { 135 const underlyingAssetAddress = selectMigrationUnderlyingAssetWithExceptions( 136 migrationExceptions, 137 supplyAsset 138 ); 139 const v3UserReserve = userReservesV3Map[underlyingAssetAddress]; 140 const v3ReserveBalanceWithExceptions = selectMigrationAssetBalanceWithExceptions( 141 migrationExceptions, 142 v3UserReserve 143 ); 144 if (v3UserReserve) { 145 return v3ReserveBalanceWithExceptions == '0' && !v3UserReserve.reserve.isIsolated; 146 } else { 147 return false; 148 } 149 }); 150 151 if (nonIsolatedAssets.length > 0) { 152 return nonIsolatedAssets; 153 } 154 155 const isolatedAssets = selectedMigrationSupplyAssets.filter((supplyAsset) => { 156 const underlyingAssetAddress = selectMigrationUnderlyingAssetWithExceptions( 157 migrationExceptions, 158 supplyAsset 159 ); 160 const v3UserReserve = userReservesV3Map[underlyingAssetAddress]; 161 const v3ReserveBalanceWithExceptions = selectMigrationAssetBalanceWithExceptions( 162 migrationExceptions, 163 v3UserReserve 164 ); 165 return v3ReserveBalanceWithExceptions == '0' && v3UserReserve.reserve.isIsolated; 166 }); 167 168 return isolatedAssets; 169 }; 170 171 export const selectUserReservesMapFromUserReserves = ( 172 userReservesData: ComputedUserReserve< 173 ReserveDataHumanized & FormatReserveUSDResponse & Partial<CalculateReserveIncentivesResponse> 174 >[] 175 ) => { 176 const v3ReservesMap = userReservesData.reduce((obj, item) => { 177 obj[item.underlyingAsset] = item; 178 return obj; 179 }, {} as Record<string, ComputedUserReserve<ReserveDataHumanized & FormatReserveUSDResponse & Partial<CalculateReserveIncentivesResponse>>>); 180 181 return v3ReservesMap; 182 }; 183 184 export enum MigrationDisabled { 185 IsolationModeBorrowDisabled, 186 EModeBorrowDisabled, 187 V3AssetMissing, 188 InsufficientLiquidity, 189 AssetNotFlashloanable, 190 ReserveFrozen, 191 NotEnoughtSupplies, 192 } 193 194 export const selectMigrationUnderlyingAssetWithExceptions = ( 195 migrationExceptions: Record<string, MigrationException>, 196 reserve: { 197 underlyingAsset: string; 198 } 199 ): string => { 200 const defaultUnderlyingAsset = reserve?.underlyingAsset; 201 if (migrationExceptions[defaultUnderlyingAsset]) { 202 return migrationExceptions[defaultUnderlyingAsset].v3UnderlyingAsset; 203 } 204 return defaultUnderlyingAsset; 205 }; 206 207 export const selectMigrationUnderluingAssetWithExceptionsByV3Key = ( 208 migrationExceptions: Record<string, MigrationException>, 209 reserveV3: { 210 underlyingAsset: string; 211 } 212 ) => { 213 const exceptionItem = Object.values(migrationExceptions).find( 214 (exception) => exception.v3UnderlyingAsset == reserveV3.underlyingAsset 215 ); 216 return exceptionItem?.v2UnderlyingAsset || reserveV3.underlyingAsset; 217 }; 218 219 export const selectMigrationAssetBalanceWithExceptions = ( 220 migrationExceptions: Record<string, MigrationException>, 221 reserve: { 222 underlyingAsset: string; 223 underlyingBalance: string; 224 } 225 ) => { 226 const underlyingAssetAddress = selectMigrationUnderlyingAssetWithExceptions( 227 migrationExceptions, 228 reserve 229 ); 230 const exceptionAsset = migrationExceptions[underlyingAssetAddress]; 231 if (exceptionAsset) { 232 return exceptionAsset.amount; 233 } 234 return reserve.underlyingBalance; 235 }; 236 237 export type IsolatedReserve = FormatReserveUSDResponse & { enteringIsolationMode?: boolean }; 238 239 export const selectedUserSupplyReservesForMigration = ( 240 selectedMigrationSupplyAssets: MigrationSelectedAsset[], 241 supplyReserves: SupplyMigrationReserve[], 242 isolatedReserveV3: IsolatedReserve | undefined 243 ) => { 244 const selectedUserReserves = supplyReserves.filter( 245 (userReserve) => 246 selectMigrationSelectedSupplyIndex( 247 selectedMigrationSupplyAssets, 248 userReserve.underlyingAsset 249 ) >= 0 250 ); 251 selectedUserReserves.sort((userReserve) => { 252 if (!isolatedReserveV3) { 253 if (userReserve.isolatedOnV3) { 254 return 1; 255 } 256 return -1; 257 } else { 258 if (isolatedReserveV3.underlyingAsset == userReserve.underlyingAsset) { 259 return -1; 260 } else { 261 return 1; 262 } 263 } 264 }); 265 266 return selectedUserReserves; 267 }; 268 269 export const selectUserSupplyIncreasedReservesForMigrationPermits = ( 270 store: RootStore, 271 supplyReserves: SupplyMigrationReserve[], 272 isolatedReserveV3: IsolatedReserve | undefined 273 ) => { 274 return selectedUserSupplyReservesForMigration( 275 store.selectedMigrationSupplyAssets, 276 supplyReserves, 277 isolatedReserveV3 278 ).map((userReserve) => { 279 const increasedAmount = addPercent(userReserve.underlyingBalance); 280 const valueInWei = valueToWei(increasedAmount, userReserve.reserve.decimals); 281 return { ...userReserve, increasedAmount: valueInWei }; 282 }); 283 }; 284 285 export const selectUserSupplyAssetsForMigrationNoPermit = ( 286 store: RootStore, 287 supplyReserves: SupplyMigrationReserve[], 288 isolatedReserveV3: IsolatedReserve | undefined 289 ): MigrationSupplyAsset[] => { 290 const selectedUserSupplyReserves = selectUserSupplyIncreasedReservesForMigrationPermits( 291 store, 292 supplyReserves, 293 isolatedReserveV3 294 ); 295 return selectedUserSupplyReserves.map(({ underlyingAsset, reserve, increasedAmount }) => { 296 const deadline = Math.floor(Date.now() / 1000 + 3600); 297 return { 298 amount: increasedAmount, 299 aToken: reserve.aTokenAddress, 300 underlyingAsset: underlyingAsset, 301 deadline, 302 }; 303 }); 304 }; 305 306 export const selectMigrationRepayAssets = ( 307 store: RootStore, 308 borrowReserves: BorrowMigrationReserve[] 309 ): MigrationRepayAsset[] => { 310 const deadline = Math.floor(Date.now() / 1000 + 3600); 311 return selectSelectedBorrowReservesForMigration( 312 borrowReserves, 313 store.selectedMigrationBorrowAssets 314 ).map((userReserve) => ({ 315 underlyingAsset: userReserve.underlyingAsset, 316 amount: userReserve.increasedVariableBorrows, 317 deadline, 318 debtToken: userReserve.debtKey, 319 rateMode: userReserve.interestRate, 320 })); 321 }; 322 323 export const selectMigrationSignedPermits = ( 324 store: RootStore, 325 signatures: SignatureLike[], 326 deadline: BigNumberish 327 ): { 328 supplyPermits: V3MigrationHelperSignedPermit[]; 329 creditDelegationPermits: V3MigrationHelperSignedCreditDelegationPermit[]; 330 } => { 331 const approvalsWithSignatures = store.approvalPermitsForMigrationAssets.map((approval, index) => { 332 return { 333 ...approval, 334 signedPermit: signatures[index], 335 }; 336 }); 337 338 const supplyPermits: V3MigrationHelperSignedPermit[] = approvalsWithSignatures 339 .filter((approval) => approval.permitType === 'SUPPLY_MIGRATOR_V3') 340 .map(({ signedPermit, underlyingAsset, amount }) => ({ 341 deadline, 342 aToken: underlyingAsset, 343 value: amount, 344 signedPermit, 345 })); 346 347 const creditDelegationPermits: V3MigrationHelperSignedCreditDelegationPermit[] = 348 approvalsWithSignatures 349 .filter((approval) => approval.permitType === 'BORROW_MIGRATOR_V3') 350 .map(({ amount, signedPermit, underlyingAsset }) => ({ 351 deadline, 352 debtToken: underlyingAsset, 353 signedPermit, 354 value: amount, 355 })); 356 357 return { 358 supplyPermits, 359 creditDelegationPermits, 360 }; 361 }; 362 363 const addPercent = (amount: string) => { 364 const convertedAmount = valueToBigNumber(amount); 365 return convertedAmount.plus(convertedAmount.div(1000)).toString(); 366 }; 367 368 export const selectSelectedBorrowReservesForMigration = ( 369 borrowReserves: BorrowMigrationReserve[], 370 selectedMigrationBorrowAssets: MigrationSelectedBorrowAsset[] 371 ) => { 372 return borrowReserves.filter( 373 (userReserve) => 374 selectMigrationSelectedBorrowIndex(selectedMigrationBorrowAssets, userReserve) >= 0 375 ); 376 }; 377 378 export const selectSelectedBorrowReservesForMigrationV3 = ( 379 selectedMigrationBorrowAssets: MigrationSelectedBorrowAsset[], 380 toUserSummary: UserSummaryForMigration, 381 userMigrationReserves: UserMigrationReserves 382 ) => { 383 const { userReservesData: userReservesDataV3 } = toUserSummary; 384 const selectedUserReserves = selectSelectedBorrowReservesForMigration( 385 userMigrationReserves.borrowReserves, 386 selectedMigrationBorrowAssets 387 ) 388 .filter((userReserve) => userReserve.migrationDisabled === undefined) 389 // debtKey should be mapped for v3Migration 390 .map((borrowReserve) => { 391 let debtKey = borrowReserve.debtKey; 392 const borrowReserveV3 = userReservesDataV3.find( 393 (userReserve) => userReserve.underlyingAsset == borrowReserve.underlyingAsset 394 ); 395 396 if (borrowReserveV3) { 397 debtKey = borrowReserveV3.reserve.variableDebtTokenAddress; 398 } 399 400 return { 401 ...borrowReserve, 402 debtKey, 403 }; 404 }); 405 406 return selectedUserReserves; 407 }; 408 409 export const selectFormatUserSummaryForMigration = ( 410 reserves: ReserveDataHumanized[] = [], 411 reserveIncentives: ReservesIncentiveDataHumanized[] = [], 412 userReserves: UserReserveDataHumanized[] = [], 413 baseCurrencyData: PoolBaseCurrencyHumanized, 414 currentTimestamp: number, 415 userEmodeCategoryId = 0 416 ) => { 417 const { marketReferenceCurrencyDecimals, marketReferenceCurrencyPriceInUsd } = baseCurrencyData; 418 const formattedReserves = formatReservesAndIncentives({ 419 reserves: reserves, 420 reserveIncentives, 421 currentTimestamp, 422 marketReferenceCurrencyDecimals: marketReferenceCurrencyDecimals, 423 marketReferencePriceInUsd: marketReferenceCurrencyPriceInUsd, 424 }); 425 426 const formattedSummary = formatUserSummary({ 427 currentTimestamp, 428 formattedReserves, 429 marketReferenceCurrencyDecimals: marketReferenceCurrencyDecimals, 430 marketReferencePriceInUsd: marketReferenceCurrencyPriceInUsd, 431 userReserves, 432 userEmodeCategoryId, 433 }); 434 435 return formattedSummary; 436 }; 437 438 /** 439 * Returns the required approval/permit payload to migrate the selected borrow reserves. 440 * The amount to migrate is the sum of the variable debt and the stable debt positions for an asset. 441 * Since all debt migrated to V3 becomes variable, the credit delegation approval/permit will be for the variable debt token address. 442 * @param store - root store 443 * @param timestamp - current timestamp 444 * @returns array of approval payloads 445 */ 446 export const selectMigrationBorrowPermitPayloads = ( 447 store: RootStore, 448 toUserSummary: UserSummaryForMigration, 449 borrowReserves: BorrowMigrationReserve[], 450 buffer?: boolean 451 ): Approval[] => { 452 const { userReservesData: userReservesDataV3 } = toUserSummary; 453 const selectedUserReserves = selectSelectedBorrowReservesForMigration( 454 borrowReserves, 455 store.selectedMigrationBorrowAssets 456 ); 457 458 const reserveDebts: ReserveDebtApprovalPayload = {}; 459 460 selectedUserReserves 461 .filter((userReserve) => userReserve.migrationDisabled === undefined) 462 .forEach((userReserve) => { 463 const borrowReserveV3 = userReservesDataV3.find( 464 (v3Reserve) => v3Reserve.underlyingAsset === userReserve.underlyingAsset 465 ); 466 if (!borrowReserveV3) { 467 return; 468 } 469 470 if (!reserveDebts[userReserve.underlyingAsset]) { 471 reserveDebts[userReserve.underlyingAsset] = { 472 variableDebtTokenAddress: borrowReserveV3.reserve.variableDebtTokenAddress, 473 decimals: borrowReserveV3.reserve.decimals, 474 variableDebtAmount: '0', 475 }; 476 } 477 478 const debt = reserveDebts[userReserve.underlyingAsset]; 479 480 debt.variableDebtAmount = valueToBigNumber(debt.variableDebtAmount) 481 .plus(valueToBigNumber(userReserve.increasedVariableBorrows)) 482 .toString(); 483 }); 484 485 return Object.keys(reserveDebts).map<Approval>((key) => { 486 const debt = reserveDebts[key]; 487 const totalDebt = valueToBigNumber(debt.variableDebtAmount); 488 const combinedAmountInWei = valueToWei(totalDebt.toString(), debt.decimals); 489 let bufferedAmount = combinedAmountInWei; 490 if (buffer) { 491 const amountBN = new BigNumber(bufferedAmount); 492 const tenPercent = amountBN.dividedBy(10); 493 bufferedAmount = amountBN.plus(tenPercent).toFixed(0); 494 } 495 496 return { 497 amount: bufferedAmount, 498 underlyingAsset: debt.variableDebtTokenAddress, 499 permitType: 'BORROW_MIGRATOR_V3', 500 }; 501 }); 502 }; 503 504 export const selectIsMigrationAvailable = (store: RootStore) => { 505 return Boolean(store.currentMarketData.addresses.V3_MIGRATOR); 506 }; 507 508 export type MigrationReserve = FormattedUserReserves & { migrationDisabled?: MigrationDisabled }; 509 type MigrationSelectedReserve = { 510 underlyingAsset: string; 511 enforced?: boolean; 512 debtKey?: string; 513 interestRate?: InterestRate; 514 }; 515 type ComputedMigrationSelections = { 516 activeSelections: MigrationReserve[]; 517 activeUnselected: MigrationReserve[]; 518 }; 519 export const assetSelected = ( 520 reserve: MigrationReserve, 521 selectedAssets: MigrationSelectedReserve[] 522 ) => { 523 const selectedReserve = selectedAssets.find( 524 (selectedAsset: MigrationSelectedReserve) => 525 selectedAsset.underlyingAsset === reserve.underlyingAsset 526 ); 527 return selectedReserve !== undefined; 528 }; 529 export const assetEnabled = (selected: MigrationSelectedReserve, reserves: MigrationReserve[]) => { 530 const selectedReserve = reserves.find( 531 (reserve: MigrationReserve) => selected.underlyingAsset === reserve.underlyingAsset 532 ); 533 return selectedReserve !== undefined && selectedReserve.migrationDisabled === undefined; 534 }; 535 536 export const computeSelections = ( 537 reserves: MigrationReserve[], 538 selections: MigrationSelectedReserve[] 539 ): ComputedMigrationSelections => { 540 const enabledReserves = reserves.filter((reserve) => reserve.migrationDisabled === undefined); 541 542 const selectedEnabledReserves = enabledReserves.filter((reserve) => 543 assetSelected(reserve, selections) 544 ); 545 const unselectedEnabledReserves = enabledReserves.filter( 546 (reserve) => !assetSelected(reserve, selections) 547 ); 548 549 return { 550 activeSelections: selectedEnabledReserves, 551 activeUnselected: unselectedEnabledReserves, 552 }; 553 };