From 784d16ec85954c257cfa2f9fb4be346235fbcd7d Mon Sep 17 00:00:00 2001 From: Halaprix Date: Tue, 12 Nov 2024 16:03:28 +0100 Subject: [PATCH] chore: handle new morpho token and wrapper (#4050) * chore: handle new morpho token and wrapper * chore: remove console logs * chore: remove more logs --- blockchain/abi/erc20-proxy-actions.json | 28 +++ blockchain/better-calls/erc20.ts | 65 ++++++ .../token-metadata-list/token-configs.ts | 9 + blockchain/tokens/mainnet.ts | 1 + features/notices/VaultsNoticesView.tsx | 1 + .../OmniDetailSectionRewardsClaims.tsx | 203 ++++++++++++------ .../omni-kit/protocols/erc-4626/settings.ts | 18 +- .../reclaimCollateralView.tsx | 1 - package.json | 2 +- yarn.lock | 8 +- 10 files changed, 252 insertions(+), 84 deletions(-) diff --git a/blockchain/abi/erc20-proxy-actions.json b/blockchain/abi/erc20-proxy-actions.json index bbb4ff9d58..900c30a769 100644 --- a/blockchain/abi/erc20-proxy-actions.json +++ b/blockchain/abi/erc20-proxy-actions.json @@ -22,6 +22,34 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "oldToken", + "type": "address" + }, + { + "internalType": "address", + "name": "newToken", + "type": "address" + }, + { + "internalType": "address", + "name": "wrapper", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approveAndWrap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/blockchain/better-calls/erc20.ts b/blockchain/better-calls/erc20.ts index bb3c1f163a..99c1487388 100644 --- a/blockchain/better-calls/erc20.ts +++ b/blockchain/better-calls/erc20.ts @@ -200,3 +200,68 @@ export async function encodeTransferToOwnerProxyAction({ value: '0', } } + +/** + * Encodes a transaction to approve and wrap an ERC20 token using OpenZeppelin's ERC20Wrapper pattern. + * This function prepares a transaction that will: + * 1. Approve the wrapper contract to spend the old token + * 2. Deposit the old token into the wrapper contract ( from proxy) + * 3.Send the new wrapped token to the owner + * + * The wrapper contract must implement the IERC20Wrapper interface which includes: + * - depositFor(address account, uint256 value) + * - withdrawTo(address account, uint256 value) + * + * @param {object} params - The parameters object + * @param {NetworkIds} params.networkId - The network ID where the transaction will be executed + * @param {string} params.oldToken - The symbol of the token to be wrapped (underlying token) + * @param {string} params.newToken - The symbol of the wrapped token to receive + * @param {string} params.wrapper - The address of the ERC20Wrapper contract + * @param {BigNumber} params.amount - The amount of tokens to wrap + * @returns {Promise} The encoded transaction data ready to be executed + * @throws Will throw if the contracts or tokens don't exist in the network configuration + * @throws Will throw if the token addresses cannot be resolved + */ +export async function encodeApproveAndWrapProxyAction({ + networkId, + oldToken, + newToken, + wrapper, + amount, +}: { + networkId: NetworkIds + oldToken: string + newToken: string + wrapper: string + amount: BigNumber +}): Promise { + const contracts = getNetworkContracts(networkId) + + ensureContractsExist(networkId, contracts, ['erc20ProxyActions']) + ensureGivenTokensExist(networkId, contracts, [oldToken, newToken]) + + const { erc20ProxyActions, tokens } = contracts + + const oldTokenAddress = tokens[oldToken].address + const newTokenAddress = tokens[newToken].address + + const proxyActionContract = Erc20ProxyActions__factory.connect( + erc20ProxyActions.address, + getRpcProvider(networkId), + ) + + const amountInWei = amountToWei(amount, oldToken).toFixed() + + const encodeFunctionData = proxyActionContract.interface.encodeFunctionData('approveAndWrap', [ + oldTokenAddress, + newTokenAddress, + wrapper, + ethers.BigNumber.from(amountInWei), + ]) + + return { + to: erc20ProxyActions.address, + data: encodeFunctionData, + value: '0', + } +} diff --git a/blockchain/token-metadata-list/token-configs.ts b/blockchain/token-metadata-list/token-configs.ts index 4df8751a0b..54238d0a2d 100644 --- a/blockchain/token-metadata-list/token-configs.ts +++ b/blockchain/token-metadata-list/token-configs.ts @@ -997,6 +997,15 @@ export const tokenConfigs: TokenConfig[] = [ iconCircle: morpho_circle_color, tags: [], }, + { + symbol: 'MORPHO_LEGACY', + precision: 18, + digits: 5, + name: 'Legacy Morpho Blue', + icon: morpho_circle_color, + iconCircle: morpho_circle_color, + tags: [], + }, { symbol: 'RBN', precision: 18, diff --git a/blockchain/tokens/mainnet.ts b/blockchain/tokens/mainnet.ts index b60273cb5d..e2de13b2e9 100644 --- a/blockchain/tokens/mainnet.ts +++ b/blockchain/tokens/mainnet.ts @@ -74,6 +74,7 @@ export const tokensMainnet = { LUSD: contractDesc(erc20, mainnet.common.LUSD), MKR: contractDesc(erc20, mainnet.maker.common.McdGov), MORPHO: contractDesc(erc20, mainnet.common.MORPHO), + MORPHO_LEGACY: contractDesc(erc20, mainnet.common.MORPHO_LEGACY), RENBTC: contractDesc(erc20, mainnet.common.RENBTC), RETH: contractDesc(erc20, mainnet.common.RETH), RSETH: contractDesc(erc20, mainnet.common.RSETH), diff --git a/features/notices/VaultsNoticesView.tsx b/features/notices/VaultsNoticesView.tsx index 98e05536e5..081b8b3f69 100644 --- a/features/notices/VaultsNoticesView.tsx +++ b/features/notices/VaultsNoticesView.tsx @@ -325,6 +325,7 @@ export function VaultLiquidatedNotice({ })} + {console.debug('ReclaimCollateralButton props:', { token, id, amount: unlockedCollateral })} ) : ( fallbackSubheader diff --git a/features/omni-kit/components/details-section/OmniDetailSectionRewardsClaims.tsx b/features/omni-kit/components/details-section/OmniDetailSectionRewardsClaims.tsx index 2b40394030..e0456cd116 100644 --- a/features/omni-kit/components/details-section/OmniDetailSectionRewardsClaims.tsx +++ b/features/omni-kit/components/details-section/OmniDetailSectionRewardsClaims.tsx @@ -3,7 +3,11 @@ import { Network } from '@oasisdex/dma-library' import { networkIdToLibraryNetwork } from 'actions/aave-like/helpers' import type BigNumber from 'bignumber.js' import { encodeClaimAllRewards, getAllUserRewards } from 'blockchain/better-calls/aave-like-rewards' -import { encodeTransferToOwnerProxyAction, tokenBalance } from 'blockchain/better-calls/erc20' +import { + encodeApproveAndWrapProxyAction, + encodeTransferToOwnerProxyAction, + tokenBalance, +} from 'blockchain/better-calls/erc20' import { NetworkIds } from 'blockchain/networks' import { tokenPriceStore } from 'blockchain/prices.constants' import { getTokenByAddress } from 'blockchain/tokensMetadata' @@ -17,7 +21,13 @@ import React, { useEffect, useReducer } from 'react' import { OmniDetailsSectionContentRewardsLoadingState } from './OmniDetailsSectionContentRewardsLoadingState' import { OmniRewardsClaims } from './OmniRewardsClaims' -const claimableErc20: Record = { +interface OmniDetailSectionRewardsClaimsInternalProps { + isEligibleForErc20Claims: boolean + isEligibleForProtocolRewards: boolean + isEligibleForMorphoLegacy: boolean +} + +const claimableErc20ByNetwork: Record = { [NetworkIds.MAINNET]: ['ENA', 'SENA'], [NetworkIds.OPTIMISMMAINNET]: [], [NetworkIds.ARBITRUMMAINNET]: [], @@ -32,13 +42,21 @@ const claimableErc20: Record = { [NetworkIds.OPTIMISMGOERLI]: [], } +const morphoLegacyByNetwork: Partial> = { + [NetworkIds.MAINNET]: 'MORPHO_LEGACY', +} + type Claim = { token: string claimable: BigNumber tx: OmniTxData } -const OmniDetailSectionRewardsClaimsInternal: FC = () => { +const OmniDetailSectionRewardsClaimsInternal: FC = ({ + isEligibleForErc20Claims, + isEligibleForProtocolRewards, + isEligibleForMorphoLegacy, +}) => { const { environment: { dpmProxy, networkId, protocol, quoteAddress }, } = useOmniGeneralContext() @@ -48,9 +66,9 @@ const OmniDetailSectionRewardsClaimsInternal: FC = () => { }, []) useEffect(() => { - if (dpmProxy) { - // Existing ERC20 claims logic - claimableErc20[networkId].forEach((token) => { + if (!dpmProxy) return + if (isEligibleForErc20Claims) { + claimableErc20ByNetwork[networkId].forEach((token) => { tokenBalance({ token, account: dpmProxy, networkId: networkId }) .then((balance) => { if (balance.gt(zero)) { @@ -72,64 +90,93 @@ const OmniDetailSectionRewardsClaimsInternal: FC = () => { console.error(`Error fetching token balance for ${token}: ${error}`) }) }) - - // New Aave and Spark rewards check - if ([LendingProtocol.AaveV3, LendingProtocol.SparkV3].includes(protocol)) { - let rewardsControllerAddress: string | undefined - let poolDataProviderAddress: string | undefined + } + if (isEligibleForMorphoLegacy) { + const morphoLegacyToken = morphoLegacyByNetwork[networkId] + if (morphoLegacyToken) { const network = networkIdToLibraryNetwork(networkId) - if ( - protocol === LendingProtocol.AaveV3 && - network !== Network.HARDHAT && - network !== Network.LOCAL && - network !== Network.TENDERLY - ) { - rewardsControllerAddress = ADDRESSES[network].aave.v3.RewardsController - poolDataProviderAddress = ADDRESSES[network].aave.v3.PoolDataProvider - } else if ( - protocol === LendingProtocol.SparkV3 && - network !== Network.HARDHAT && - network !== Network.LOCAL && - network !== Network.TENDERLY - ) { - rewardsControllerAddress = ADDRESSES[network].spark.RewardsController - poolDataProviderAddress = ADDRESSES[network].spark.PoolDataProvider - } else { - console.warn(`Unsupported protocol or network for rewards: ${protocol} on ${network}`) - throw new Error(`Unsupported protocol or network for rewards: ${protocol} on ${network}`) + if (network === Network.MAINNET) { + tokenBalance({ token: morphoLegacyToken, account: dpmProxy, networkId }) + .then((balance) => { + if (balance.gt(zero)) { + encodeApproveAndWrapProxyAction({ + oldToken: morphoLegacyToken, + newToken: 'MORPHO', + wrapper: ADDRESSES[network].morphoblue.Wrapper, + amount: balance, + networkId, + }) + .then((tx) => { + dispatchClaim({ token: 'MORPHO', claimable: balance, tx }) + }) + .catch((error) => { + console.error( + `Error encoding approve and wrap action for MORPHO_LEGACY: ${error}`, + ) + }) + } + }) + .catch((error) => { + console.error(`Error fetching MORPHO_LEGACY balance: ${error}`) + }) } + } + } + if (isEligibleForProtocolRewards) { + let rewardsControllerAddress: string | undefined + let poolDataProviderAddress: string | undefined + const network = networkIdToLibraryNetwork(networkId) + if ( + protocol === LendingProtocol.AaveV3 && + network !== Network.HARDHAT && + network !== Network.LOCAL && + network !== Network.TENDERLY + ) { + rewardsControllerAddress = ADDRESSES[network].aave.v3.RewardsController + poolDataProviderAddress = ADDRESSES[network].aave.v3.PoolDataProvider + } else if ( + protocol === LendingProtocol.SparkV3 && + network !== Network.HARDHAT && + network !== Network.LOCAL && + network !== Network.TENDERLY + ) { + rewardsControllerAddress = ADDRESSES[network].spark.RewardsController + poolDataProviderAddress = ADDRESSES[network].spark.PoolDataProvider + } else { + console.warn(`Unsupported protocol or network for rewards: ${protocol} on ${network}`) + throw new Error(`Unsupported protocol or network for rewards: ${protocol} on ${network}`) + } - getAllUserRewards({ - networkId, - token: quoteAddress, - account: dpmProxy, - rewardsController: rewardsControllerAddress as string, - poolDataProvider: poolDataProviderAddress as string, - }) - .then(async ({ rewardsList, unclaimedAmounts, assets }) => { - if (unclaimedAmounts.some((amount) => amount.gt(zero))) { - const tx = encodeClaimAllRewards({ - networkId, - assets: assets as string[], - dpmAccount: dpmProxy, - rewardsController: rewardsControllerAddress as string, - }) + getAllUserRewards({ + networkId, + token: quoteAddress, + account: dpmProxy, + rewardsController: rewardsControllerAddress as string, + poolDataProvider: poolDataProviderAddress as string, + }) + .then(async ({ rewardsList, unclaimedAmounts, assets }) => { + if (unclaimedAmounts.some((amount) => amount.gt(zero))) { + const tx = encodeClaimAllRewards({ + networkId, + assets: assets as string[], + dpmAccount: dpmProxy, + rewardsController: rewardsControllerAddress as string, + }) - rewardsList.forEach((token, index) => { - if (unclaimedAmounts[index].gt(zero)) { - dispatchClaim({ - token: getTokenByAddress(token, networkId).symbol, - claimable: unclaimedAmounts[index], - tx, - }) - } - }) - } - }) - .catch((error) => { - console.error(`Error fetching ${protocol} rewards:`, error) - }) - } + rewardsList.forEach((token, index) => { + if (unclaimedAmounts[index].gt(zero)) { + dispatchClaim({ + token: getTokenByAddress(token, networkId).symbol, + claimable: unclaimedAmounts[index], + tx, + }) + } + }) + } + }) + .catch((error) => { + console.error(`Error fetching ${protocol} rewards:`, error) + }) } }, [dpmProxy, networkId, protocol, quoteAddress]) @@ -152,19 +199,33 @@ const OmniDetailSectionRewardsClaimsInternal: FC = () => { export const OmniDetailSectionRewardsClaims: FC = () => { const { - environment: { protocol, collateralToken, quoteToken }, + environment: { protocol, collateralToken, quoteToken, networkId }, } = useOmniGeneralContext() - const eligibleTokens = ['SUSDE', 'USDE', 'WETH', 'ETH'] + const rewardsEligibleTokens = ['SUSDE', 'USDE', 'WETH', 'ETH'] + + // Regular ERC20 claims eligibility + const isEligibleForErc20Claims = claimableErc20ByNetwork[networkId].length > 0 + + // Aave/Spark rewards eligibility + const isEligibleForProtocolRewards = + [LendingProtocol.AaveV3, LendingProtocol.SparkV3].includes(protocol) && + (rewardsEligibleTokens.includes(collateralToken) || rewardsEligibleTokens.includes(quoteToken)) - const isEligible = - [ - LendingProtocol.MorphoBlue, - LendingProtocol.Ajna, - LendingProtocol.AaveV3, - LendingProtocol.SparkV3, - ].includes(protocol) && - (eligibleTokens.includes(collateralToken) || eligibleTokens.includes(quoteToken)) + // Legacy Morpho claims eligibility + const isEligibleForMorphoLegacy = + networkId === NetworkIds.MAINNET && protocol === LendingProtocol.MorphoBlue - return isEligible ? : <> + const hasAnyEligibleClaims = + isEligibleForErc20Claims || isEligibleForProtocolRewards || isEligibleForMorphoLegacy + + return hasAnyEligibleClaims ? ( + + ) : ( + <> + ) } diff --git a/features/omni-kit/protocols/erc-4626/settings.ts b/features/omni-kit/protocols/erc-4626/settings.ts index c729bf216b..705ccf5161 100644 --- a/features/omni-kit/protocols/erc-4626/settings.ts +++ b/features/omni-kit/protocols/erc-4626/settings.ts @@ -92,6 +92,10 @@ const morphoRewards = { token: 'MORPHO', label: 'Morpho token rewards', } +const legacyMorphoRewards = { + token: 'MORPHO_LEGACY', + label: 'Legacy Morpho token rewards', +} const wstethRewards = { token: 'WSTETH', label: 'Lido rewards in WSTETH', @@ -112,7 +116,7 @@ export const erc4626Vaults: Erc4626Config[] = [ networkId: NetworkIds.MAINNET, pricePicker: morphoPricePicker, protocol: LendingProtocol.MorphoBlue, - rewards: [morphoRewards, wstethRewards], + rewards: [morphoRewards, wstethRewards, legacyMorphoRewards], rewardsType: Erc4626RewardsType.MetaMorpho, strategy: 'Steakhouse MetaMorpho Vault', token: { @@ -129,7 +133,7 @@ export const erc4626Vaults: Erc4626Config[] = [ protocol: LendingProtocol.MorphoBlue, networkId: NetworkIds.MAINNET, pricePicker: morphoPricePicker, - rewards: [morphoRewards], + rewards: [morphoRewards, legacyMorphoRewards], rewardsType: Erc4626RewardsType.MetaMorpho, strategy: 'Steakhouse MetaMorpho Vault', token: { @@ -146,7 +150,7 @@ export const erc4626Vaults: Erc4626Config[] = [ protocol: LendingProtocol.MorphoBlue, networkId: NetworkIds.MAINNET, pricePicker: morphoPricePicker, - rewards: [morphoRewards], + rewards: [morphoRewards, legacyMorphoRewards], rewardsType: Erc4626RewardsType.MetaMorpho, strategy: 'Steakhouse MetaMorpho Vault', token: { @@ -163,7 +167,7 @@ export const erc4626Vaults: Erc4626Config[] = [ protocol: LendingProtocol.MorphoBlue, networkId: NetworkIds.MAINNET, pricePicker: morphoPricePicker, - rewards: [morphoRewards], + rewards: [morphoRewards, legacyMorphoRewards], rewardsType: Erc4626RewardsType.MetaMorpho, strategy: 'Steakhouse MetaMorpho Vault', token: { @@ -180,7 +184,7 @@ export const erc4626Vaults: Erc4626Config[] = [ protocol: LendingProtocol.MorphoBlue, networkId: NetworkIds.MAINNET, pricePicker: morphoPricePicker, - rewards: [morphoRewards], + rewards: [morphoRewards, legacyMorphoRewards], rewardsType: Erc4626RewardsType.MetaMorpho, strategy: 'Flagship MetaMorpho Vault', token: { @@ -197,7 +201,7 @@ export const erc4626Vaults: Erc4626Config[] = [ protocol: LendingProtocol.MorphoBlue, networkId: NetworkIds.MAINNET, pricePicker: morphoPricePicker, - rewards: [morphoRewards], + rewards: [morphoRewards, legacyMorphoRewards], rewardsType: Erc4626RewardsType.MetaMorpho, strategy: 'Flagship MetaMorpho Vault', token: { @@ -214,7 +218,7 @@ export const erc4626Vaults: Erc4626Config[] = [ protocol: LendingProtocol.MorphoBlue, networkId: NetworkIds.MAINNET, pricePicker: morphoPricePicker, - rewards: [morphoRewards], + rewards: [morphoRewards, legacyMorphoRewards], rewardsType: Erc4626RewardsType.MetaMorpho, strategy: 'Flagship MetaMorpho Vault', token: { diff --git a/features/reclaimCollateral/reclaimCollateralView.tsx b/features/reclaimCollateral/reclaimCollateralView.tsx index 88aa2be5aa..41c6668b29 100644 --- a/features/reclaimCollateral/reclaimCollateralView.tsx +++ b/features/reclaimCollateral/reclaimCollateralView.tsx @@ -19,7 +19,6 @@ export function ReclaimCollateralButton({ amount, token, id }: ReclaimCollateral if (!state) return null const { reclaim, txStatus } = state const isLoading = txStatus === 'reclaimWaitingForApproval' || txStatus === 'reclaimInProgress' - return (