Skip to content

Commit

Permalink
chore: handle new morpho token and wrapper (#4050)
Browse files Browse the repository at this point in the history
* chore: handle new morpho token and wrapper

* chore: remove console logs

* chore: remove more logs
  • Loading branch information
halaprix authored Nov 12, 2024
1 parent b86d5cc commit 784d16e
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 84 deletions.
28 changes: 28 additions & 0 deletions blockchain/abi/erc20-proxy-actions.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
65 changes: 65 additions & 0 deletions blockchain/better-calls/erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OmniTxData>} 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<OmniTxData> {
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',
}
}
9 changes: 9 additions & 0 deletions blockchain/token-metadata-list/token-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions blockchain/tokens/mainnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions features/notices/VaultsNoticesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ export function VaultLiquidatedNotice({
})}
</Text>
<ReclaimCollateralButton {...{ token, id, amount: unlockedCollateral }} />
{console.debug('ReclaimCollateralButton props:', { token, id, amount: unlockedCollateral })}
</>
) : (
fallbackSubheader
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -17,7 +21,13 @@ import React, { useEffect, useReducer } from 'react'
import { OmniDetailsSectionContentRewardsLoadingState } from './OmniDetailsSectionContentRewardsLoadingState'
import { OmniRewardsClaims } from './OmniRewardsClaims'

const claimableErc20: Record<NetworkIds, string[]> = {
interface OmniDetailSectionRewardsClaimsInternalProps {
isEligibleForErc20Claims: boolean
isEligibleForProtocolRewards: boolean
isEligibleForMorphoLegacy: boolean
}

const claimableErc20ByNetwork: Record<NetworkIds, string[]> = {
[NetworkIds.MAINNET]: ['ENA', 'SENA'],
[NetworkIds.OPTIMISMMAINNET]: [],
[NetworkIds.ARBITRUMMAINNET]: [],
Expand All @@ -32,13 +42,21 @@ const claimableErc20: Record<NetworkIds, string[]> = {
[NetworkIds.OPTIMISMGOERLI]: [],
}

const morphoLegacyByNetwork: Partial<Record<NetworkIds, string>> = {
[NetworkIds.MAINNET]: 'MORPHO_LEGACY',
}

type Claim = {
token: string
claimable: BigNumber
tx: OmniTxData
}

const OmniDetailSectionRewardsClaimsInternal: FC = () => {
const OmniDetailSectionRewardsClaimsInternal: FC<OmniDetailSectionRewardsClaimsInternalProps> = ({
isEligibleForErc20Claims,
isEligibleForProtocolRewards,
isEligibleForMorphoLegacy,
}) => {
const {
environment: { dpmProxy, networkId, protocol, quoteAddress },
} = useOmniGeneralContext()
Expand All @@ -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)) {
Expand All @@ -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])

Expand All @@ -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 ? <OmniDetailSectionRewardsClaimsInternal /> : <></>
const hasAnyEligibleClaims =
isEligibleForErc20Claims || isEligibleForProtocolRewards || isEligibleForMorphoLegacy

return hasAnyEligibleClaims ? (
<OmniDetailSectionRewardsClaimsInternal
isEligibleForErc20Claims={isEligibleForErc20Claims}
isEligibleForProtocolRewards={isEligibleForProtocolRewards}
isEligibleForMorphoLegacy={isEligibleForMorphoLegacy}
/>
) : (
<></>
)
}
Loading

0 comments on commit 784d16e

Please sign in to comment.