Skip to content

Commit 0c91342

Browse files
authored
Merge pull request #1771 from curvefi/feat/add-oracle-function-check-to-pool-creation
feat: add oracle function check to pool creation
2 parents 06a4a1d + d254eee commit 0c91342

File tree

10 files changed

+261
-86
lines changed

10 files changed

+261
-86
lines changed

apps/main/src/dex/components/PageCreatePool/Summary/OracleSummary.tsx

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { styled } from 'styled-components'
22
import { isAddress } from 'viem'
3-
import { NG_ASSET_TYPE } from '@/dex/components/PageCreatePool/constants'
3+
import {
4+
NG_ASSET_TYPE,
5+
TOKEN_A,
6+
TOKEN_B,
7+
TOKEN_C,
8+
TOKEN_D,
9+
TOKEN_E,
10+
TOKEN_F,
11+
TOKEN_G,
12+
TOKEN_H,
13+
} from '@/dex/components/PageCreatePool/constants'
414
import {
515
CategoryDataRow,
616
SummaryDataTitle,
@@ -29,41 +39,24 @@ type OracleTokenSummaryProps = {
2939
}
3040

3141
const OracleSummary = ({ chainId }: Props) => {
32-
const tokenA = useStore((state) => state.createPool.tokensInPool.tokenA)
33-
const tokenB = useStore((state) => state.createPool.tokensInPool.tokenB)
34-
const tokenC = useStore((state) => state.createPool.tokensInPool.tokenC)
35-
const tokenD = useStore((state) => state.createPool.tokensInPool.tokenD)
36-
const tokenE = useStore((state) => state.createPool.tokensInPool.tokenE)
37-
const tokenF = useStore((state) => state.createPool.tokensInPool.tokenF)
38-
const tokenG = useStore((state) => state.createPool.tokensInPool.tokenG)
39-
const tokenH = useStore((state) => state.createPool.tokensInPool.tokenH)
42+
const tokens = useStore((state) => state.createPool.tokensInPool)
43+
44+
const oracleTokens = [
45+
{ token: tokens.tokenA, title: t`Token A`, tokenId: TOKEN_A },
46+
{ token: tokens.tokenB, title: t`Token B`, tokenId: TOKEN_B },
47+
{ token: tokens.tokenC, title: t`Token C`, tokenId: TOKEN_C },
48+
{ token: tokens.tokenD, title: t`Token D`, tokenId: TOKEN_D },
49+
{ token: tokens.tokenE, title: t`Token E`, tokenId: TOKEN_E },
50+
{ token: tokens.tokenF, title: t`Token F`, tokenId: TOKEN_F },
51+
{ token: tokens.tokenG, title: t`Token G`, tokenId: TOKEN_G },
52+
{ token: tokens.tokenH, title: t`Token H`, tokenId: TOKEN_H },
53+
].filter(({ token }) => token.ngAssetType === NG_ASSET_TYPE.ORACLE && token.address !== '')
4054

4155
return (
4256
<OraclesWrapper>
43-
{tokenA.ngAssetType === NG_ASSET_TYPE.ORACLE && tokenA.address !== '' && (
44-
<OracleTokenSummary chainId={chainId} token={tokenA} title={t`Token A`} />
45-
)}
46-
{tokenB.ngAssetType === NG_ASSET_TYPE.ORACLE && tokenB.address !== '' && (
47-
<OracleTokenSummary chainId={chainId} token={tokenB} title={t`Token B`} />
48-
)}
49-
{tokenC.ngAssetType === NG_ASSET_TYPE.ORACLE && tokenC.address !== '' && (
50-
<OracleTokenSummary chainId={chainId} token={tokenC} title={t`Token C`} />
51-
)}
52-
{tokenD.ngAssetType === NG_ASSET_TYPE.ORACLE && tokenD.address !== '' && (
53-
<OracleTokenSummary chainId={chainId} token={tokenD} title={t`Token D`} />
54-
)}
55-
{tokenD.ngAssetType === NG_ASSET_TYPE.ORACLE && tokenE.address !== '' && (
56-
<OracleTokenSummary chainId={chainId} token={tokenE} title={t`Token E`} />
57-
)}
58-
{tokenD.ngAssetType === NG_ASSET_TYPE.ORACLE && tokenF.address !== '' && (
59-
<OracleTokenSummary chainId={chainId} token={tokenF} title={t`Token F`} />
60-
)}
61-
{tokenD.ngAssetType === NG_ASSET_TYPE.ORACLE && tokenG.address !== '' && (
62-
<OracleTokenSummary chainId={chainId} token={tokenG} title={t`Token G`} />
63-
)}
64-
{tokenD.ngAssetType === NG_ASSET_TYPE.ORACLE && tokenH.address !== '' && (
65-
<OracleTokenSummary chainId={chainId} token={tokenH} title={t`Token H`} />
66-
)}
57+
{oracleTokens.map(({ token, title, tokenId }) => (
58+
<OracleTokenSummary key={tokenId} chainId={chainId} token={token} title={title} />
59+
))}
6760
</OraclesWrapper>
6861
)
6962
}
@@ -77,12 +70,12 @@ const OracleTokenSummary = ({ chainId, token, title }: OracleTokenSummaryProps)
7770
</CategoryDataRow>
7871
<CategoryDataRow>
7972
<SummaryDataTitle>{t`Address:`}</SummaryDataTitle>
80-
{token.oracleAddress === '' ? (
73+
{token.oracle.address === '' ? (
8174
<SummaryDataPlaceholder>{t`No address set`}</SummaryDataPlaceholder>
82-
) : isAddress(token.oracleAddress) ? (
75+
) : isAddress(token.oracle.address) ? (
8376
<SummaryData>
84-
<AddressLink href={scanAddressPath(network, token.oracleAddress)}>
85-
{shortenAddress(token.oracleAddress)}
77+
<AddressLink href={scanAddressPath(network, token.oracle.address)}>
78+
{shortenAddress(token.oracle.address)}
8679
<Icon name={'Launch'} size={16} aria-label={t`Link to address`} />
8780
</AddressLink>
8881
</SummaryData>
@@ -92,10 +85,10 @@ const OracleTokenSummary = ({ chainId, token, title }: OracleTokenSummaryProps)
9285
</CategoryDataRow>
9386
<CategoryDataRow>
9487
<SummaryDataTitle>{t`Function:`}</SummaryDataTitle>
95-
{token.oracleFunction === '' ? (
88+
{token.oracle.functionName === '' ? (
9689
<SummaryDataPlaceholder>{t`No function set`}</SummaryDataPlaceholder>
9790
) : (
98-
<SummaryData>{token.oracleFunction}</SummaryData>
91+
<SummaryData>{token.oracle.functionName}</SummaryData>
9992
)}
10093
</CategoryDataRow>
10194
</OracleTokenWrapper>

apps/main/src/dex/components/PageCreatePool/TokensInPool/SetOracle.tsx

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import lodash from 'lodash'
2+
import { useMemo } from 'react'
23
import { styled } from 'styled-components'
3-
import { isAddress } from 'viem'
4+
import { isAddress, formatEther } from 'viem'
45
import TextInput from '@/dex/components/PageCreatePool/components/TextInput'
56
import WarningBox from '@/dex/components/PageCreatePool/components/WarningBox'
67
import {
@@ -13,12 +14,21 @@ import {
1314
TOKEN_G,
1415
TOKEN_H,
1516
NG_ASSET_TYPE,
17+
ORACLE_DECIMALS,
1618
} from '@/dex/components/PageCreatePool/constants'
19+
import { useOracleValidation } from '@/dex/components/PageCreatePool/hooks/useOracleValidation'
1720
import type { TokenState, TokenId } from '@/dex/components/PageCreatePool/types'
1821
import { validateOracleFunction } from '@/dex/components/PageCreatePool/utils'
1922
import useStore from '@/dex/store/useStore'
23+
import Alert from '@mui/material/Alert'
24+
import Stack from '@mui/material/Stack'
25+
import Typography from '@mui/material/Typography'
2026
import Box from '@ui/Box'
2127
import { t } from '@ui-kit/lib/i18n'
28+
import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
29+
import { formatNumber } from '@ui-kit/utils'
30+
31+
const { Spacing } = SizesAndSpaces
2232

2333
type OracleInputProps = {
2434
token: TokenState
@@ -53,36 +63,61 @@ const OracleInputs = ({ token, tokenId, title }: OracleInputProps) => {
5363
const updateOracleAddress = useStore((state) => state.createPool.updateOracleAddress)
5464
const updateOracleFunction = useStore((state) => state.createPool.updateOracleFunction)
5565

66+
const oracleFunction = token.oracle.functionName
67+
const oracleAddress = token.oracle.address
68+
69+
const { isLoading, isSuccess, error, rate, decimals } = useOracleValidation({ token, tokenId })
70+
71+
const formattedRate = useMemo(() => {
72+
if (!isSuccess || !rate || decimals === undefined) return null
73+
return formatNumber(Number(formatEther(BigInt(rate))), {
74+
abbreviate: false,
75+
})
76+
}, [decimals, rate, isSuccess])
77+
5678
return (
5779
<InputContainer>
5880
<TokenTitle>{t`${title} ${token.symbol !== '' ? `(${token.symbol})` : ''} Oracle`}</TokenTitle>
5981
<TextInput
6082
row
61-
defaultValue={token.oracleAddress}
83+
defaultValue={oracleAddress}
6284
onChange={lodash.debounce((value) => updateOracleAddress(tokenId, value), 300)}
6385
maxLength={42}
6486
label={t`Address (e.g 0x123...)`}
6587
/>
66-
{token.oracleAddress.length !== 0 && !token.oracleAddress.startsWith('0x') && (
67-
<WarningBox message={t`Oracle address needs to start with '0x'.`} />
68-
)}
69-
{token.oracleAddress.length !== 0 && token.oracleAddress.length < 42 && (
70-
<WarningBox message={t`Oracle address needs to be 42 characters long.`} />
71-
)}
72-
{token.oracleAddress.length === 42 && !isAddress(token.oracleAddress) && (
73-
<WarningBox message={t`Invalid EVM address.`} />
88+
{!isAddress(oracleAddress) && oracleAddress.length > 0 && (
89+
<WarningBox message={t`Invalid EVM address. Needs to start with '0x', needs to be 42 characters long.`} />
7490
)}
7591
<TextInput
7692
row
77-
defaultValue={token.oracleFunction}
93+
defaultValue={oracleFunction}
7894
onChange={lodash.debounce((value) => updateOracleFunction(tokenId, value), 300)}
7995
maxLength={42}
8096
label={t`Function (e.g exchangeRate())`}
8197
/>
82-
{token.oracleFunction !== '' && !validateOracleFunction(token.oracleFunction) && (
98+
{oracleFunction !== '' && !validateOracleFunction(oracleFunction) && (
8399
<WarningBox message={t`Oracle function name needs to end with '()'.`} />
84100
)}
85-
<WarningBox message={t`Oracle must have a precision of 18 decimals.`} informational />
101+
{decimals !== ORACLE_DECIMALS && oracleFunction !== '' && !isLoading && !error && (
102+
<WarningBox message={t`Oracle must have a precision of ${ORACLE_DECIMALS} decimals.`} informational />
103+
)}
104+
{isLoading && <WarningBox message={t`Validating oracle...`} informational />}
105+
{error && <WarningBox message={t`Unable to validate oracle.`} />}
106+
{isSuccess && formattedRate !== null && (
107+
<Alert severity="info" variant="standard" sx={{ marginTop: Spacing.sm }}>
108+
<Stack gap={Spacing.xs}>
109+
<Typography variant="bodySRegular">
110+
{t`Oracle rate:`} {formattedRate}
111+
</Typography>
112+
<Typography variant="bodySRegular">
113+
{t`Decimals:`} {decimals}
114+
</Typography>
115+
<Typography variant="bodySRegular">
116+
{t`Raw rate:`} {rate}
117+
</Typography>
118+
</Stack>
119+
</Alert>
120+
)}
86121
</InputContainer>
87122
)
88123
}

apps/main/src/dex/components/PageCreatePool/TokensInPool/index.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import { CreateToken, TokenId, TokensInPoolState } from '@/dex/components/PageCr
2323
import { checkMetaPool, containsOracle, getBasepoolCoins } from '@/dex/components/PageCreatePool/utils'
2424
import { useNetworkByChain } from '@/dex/entities/networks'
2525
import useTokensMapper from '@/dex/hooks/useTokensMapper'
26-
import { DEFAULT_CREATE_POOL_STATE, DEFAULT_ERC4626_STATUS } from '@/dex/store/createCreatePoolSlice'
26+
import {
27+
DEFAULT_CREATE_POOL_STATE,
28+
DEFAULT_ERC4626_STATUS,
29+
DEFAULT_ORACLE_STATUS,
30+
} from '@/dex/store/createCreatePoolSlice'
2731
import useStore from '@/dex/store/useStore'
2832
import { CurveApi, ChainId, BasePool } from '@/dex/types/main.types'
2933
import Box from '@ui/Box'
@@ -548,8 +552,7 @@ const TokensInPool = ({ curve, chainId, haveSigner }: Props) => {
548552
address: tokenId === token ? '' : tokensInPoolState[token].address,
549553
symbol: tokenId === token ? '' : tokensInPoolState[token].symbol,
550554
ngAssetType: tokenId === token ? NG_ASSET_TYPE.STANDARD : tokensInPoolState[token].ngAssetType,
551-
oracleAddress: tokenId === token ? '' : tokensInPoolState[token].oracleAddress,
552-
oracleFunction: tokenId === token ? '' : tokensInPoolState[token].oracleFunction,
555+
oracle: tokenId === token ? { ...DEFAULT_ORACLE_STATUS } : tokensInPoolState[token].oracle,
553556
erc4626: tokenId === token ? { ...DEFAULT_ERC4626_STATUS } : tokensInPoolState[token].erc4626,
554557
})
555558

@@ -569,7 +572,7 @@ const TokensInPool = ({ curve, chainId, haveSigner }: Props) => {
569572
)
570573

571574
// check if the tokens are withing 0.95 and 1.05 threshold
572-
const checkThreshold = useMemo(() => {
575+
const checkStableswapThreshold = useMemo(() => {
573576
// Array of token IDs you want to check
574577
const tokenIds: TokenId[] = [TOKEN_A, TOKEN_B, TOKEN_C, TOKEN_D, TOKEN_E, TOKEN_F, TOKEN_G, TOKEN_H]
575578

@@ -578,6 +581,17 @@ const TokensInPool = ({ curve, chainId, haveSigner }: Props) => {
578581
(tokenId) => tokensInPool[tokenId].address !== '' && initialPrice[tokenId] !== 0,
579582
)
580583

584+
// Skip threshold check for tokens with special asset types (they have their own rate mechanisms)
585+
const hasSpecialAssetType = tokenIds.some((tokenId) => {
586+
const assetType = tokensInPool[tokenId].ngAssetType
587+
return (
588+
assetType === NG_ASSET_TYPE.ERC4626 ||
589+
assetType === NG_ASSET_TYPE.ORACLE ||
590+
assetType === NG_ASSET_TYPE.REBASING
591+
)
592+
})
593+
if (hasSpecialAssetType) return false
594+
581595
if (validTokens.length <= 1) {
582596
// Not enough tokens for comparison
583597
return false
@@ -760,7 +774,7 @@ const TokensInPool = ({ curve, chainId, haveSigner }: Props) => {
760774
{!chainId && <WarningBox message={t`Please connect a wallet to select tokens`} />}
761775
{swapType === STABLESWAP ? (
762776
<>
763-
{checkThreshold && twocryptoFactory && (
777+
{checkStableswapThreshold && twocryptoFactory && (
764778
<>
765779
<WarningBox
766780
message={t`Tokens appear to be unpegged (above 5% deviation from 1:1).

apps/main/src/dex/components/PageCreatePool/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type PRESETS = {
3636
}
3737
}
3838

39+
export const ORACLE_DECIMALS = 18
40+
3941
export const NG_ASSET_TYPE: Record<string, NgAssetType> = {
4042
STANDARD: 0,
4143
ORACLE: 1,

apps/main/src/dex/components/PageCreatePool/hooks/useAutoDetectErc4626.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo } from 'react'
1+
import { useEffect } from 'react'
22
import { type Address } from 'viem'
33
import { NG_ASSET_TYPE } from '@/dex/components/PageCreatePool/constants'
44
import { useIsErc4626 } from '@/dex/components/PageCreatePool/hooks/useIsErc4626'
@@ -20,7 +20,7 @@ export const useAutoDetectErc4626 = ({ tokenId, address }: UseAutoDetectErc4626P
2020
const statusAlreadySet = useStore((state) => state.createPool.tokensInPool[tokenId].erc4626.isSuccess)
2121
const { isErc4626, isLoading, error, isSuccess } = useIsErc4626({ address })
2222

23-
useMemo(() => {
23+
useEffect(() => {
2424
if (isLoading || error || isSuccess) {
2525
updateTokenErc4626Status(tokenId, { isErc4626, isLoading, error, isSuccess })
2626
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { type Address, erc20Abi } from 'viem'
2+
import { useReadContract } from 'wagmi'
3+
import { decimal } from '@ui-kit/utils'
4+
5+
const buildOracleAbi = (fnName: string) =>
6+
[
7+
{
8+
name: fnName,
9+
type: 'function',
10+
stateMutability: 'view',
11+
inputs: [],
12+
outputs: [{ name: 'rate', type: 'uint256' }],
13+
},
14+
] as const
15+
16+
type UseOracleRateParams = {
17+
address?: Address
18+
functionName?: string
19+
enabled?: boolean
20+
}
21+
22+
export function useOracleRate({ address, functionName, enabled = true }: UseOracleRateParams) {
23+
const cleanFunctionName = functionName?.replace('()', '') ?? ''
24+
25+
const {
26+
data: rate,
27+
isFetching: isLoading,
28+
isSuccess,
29+
error,
30+
} = useReadContract({
31+
address,
32+
abi: buildOracleAbi(cleanFunctionName),
33+
functionName: cleanFunctionName,
34+
query: { enabled: enabled && !!address && !!cleanFunctionName, retry: false },
35+
})
36+
37+
const { data: decimals } = useReadContract({
38+
address,
39+
abi: erc20Abi,
40+
functionName: 'decimals',
41+
query: { enabled: enabled && !!address && isSuccess, retry: false },
42+
})
43+
44+
return {
45+
rate: decimal(rate?.toString()),
46+
decimals,
47+
isLoading,
48+
isSuccess,
49+
error,
50+
}
51+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useEffect } from 'react'
2+
import { isAddress, type Address } from 'viem'
3+
import { useOracleRate } from '@/dex/components/PageCreatePool/hooks/useOracleRate'
4+
import { TokenId, TokenState } from '@/dex/components/PageCreatePool/types'
5+
import { validateOracleFunction } from '@/dex/components/PageCreatePool/utils'
6+
import useStore from '@/dex/store/useStore'
7+
8+
type UseOracleValidationParams = {
9+
token: TokenState
10+
tokenId: TokenId
11+
}
12+
13+
/**
14+
* This hook validates oracle configuration, syncs results to store, and returns them
15+
*/
16+
export const useOracleValidation = ({ token, tokenId }: UseOracleValidationParams) => {
17+
const updateOracleState = useStore((state) => state.createPool.updateOracleState)
18+
19+
const oracleAddress = token.oracle.address
20+
const oracleFunction = token.oracle.functionName
21+
22+
const enabled = isAddress(oracleAddress) && validateOracleFunction(oracleFunction)
23+
24+
const { rate, decimals, isLoading, isSuccess, error } = useOracleRate({
25+
address: oracleAddress as Address,
26+
functionName: oracleFunction,
27+
enabled,
28+
})
29+
30+
// Sync to store when query results change
31+
useEffect(() => {
32+
updateOracleState(tokenId, {
33+
address: oracleAddress,
34+
functionName: oracleFunction,
35+
isLoading,
36+
isSuccess,
37+
error,
38+
rate,
39+
decimals,
40+
})
41+
}, [tokenId, oracleAddress, oracleFunction, isLoading, isSuccess, error, rate, decimals, updateOracleState])
42+
43+
return { isLoading, isSuccess, error, rate, decimals }
44+
}

0 commit comments

Comments
 (0)