diff --git a/tasks/validation/proposal-validator.ts b/tasks/validation/proposal-validator.ts index 21e9102be..3cfe9f1c4 100644 --- a/tasks/validation/proposal-validator.ts +++ b/tasks/validation/proposal-validator.ts @@ -9,7 +9,12 @@ import { MAX_UINT256, TradeKind } from '#/common/constants' import { formatEther, formatUnits } from 'ethers/lib/utils' import { recollateralize, redeemRTokens } from './utils/rtokens' import { processRevenue } from './utils/rewards' -import { pushOraclesForward } from './utils/oracles' +import { + pushOraclesForward, + getRTokenOracle, + getRTokenOraclePrice, + validateRTokenOraclePriceChange, +} from './utils/oracles' import { passProposal, executeProposal, @@ -60,13 +65,37 @@ task('proposal-validator', 'Runs a proposal and confirms can fully rebalance + r console.log(`Network Block: ${await getLatestBlockNumber(hre)}`) + const proposalData = JSON.parse( + fs.readFileSync(`./tasks/validation/proposals/proposal-${params.proposalid}.json`, 'utf-8') + ) + + // Get RToken oracle config (if exists) for price validation + const oracleConfig = getRTokenOracle(proposalData.rtoken) + let priceBefore + if (oracleConfig) { + console.log( + `\nšŸ”® RToken oracle found: ${oracleConfig.address} (threshold: ${oracleConfig.threshold}%)` + ) + priceBefore = await getRTokenOraclePrice(hre, oracleConfig.address) + console.log(`Price (before): ${priceBefore.toString()}`) + } + await hre.run('propose', { pid: params.proposalid, }) - const proposalData = JSON.parse( - fs.readFileSync(`./tasks/validation/proposals/proposal-${params.proposalid}.json`, 'utf-8') - ) + // Validate RToken oracle + if (oracleConfig && priceBefore) { + const priceAfter = await getRTokenOraclePrice(hre, oracleConfig.address) + console.log(`\nšŸ”® RToken Price (after): ${priceAfter.toString()}`) + validateRTokenOraclePriceChange( + priceBefore, + priceAfter, + proposalData.rtoken, + oracleConfig.threshold + ) + } + await hre.run('recollateralize', { rtoken: proposalData.rtoken, governor: proposalData.governor, diff --git a/tasks/validation/utils/constants.ts b/tasks/validation/utils/constants.ts index 4d4bdd1a8..f3cc79405 100644 --- a/tasks/validation/utils/constants.ts +++ b/tasks/validation/utils/constants.ts @@ -45,10 +45,16 @@ export const collateralToUnderlying: { [key: string]: string } = { networkConfig['1'].tokens.aEthUSDC!.toLowerCase(), } +export interface OracleConfig { + address: string + threshold: number // Allowed deviation percentage (e.g., 0.5 = 0.5%) +} + export interface RTokenDeployment { rToken: string governor: string timelock: string + oracle?: OracleConfig // Optional (RToken oracle) } export const MAINNET_DEPLOYMENTS: RTokenDeployment[] = [ @@ -61,6 +67,10 @@ export const MAINNET_DEPLOYMENTS: RTokenDeployment[] = [ rToken: '0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8', // ETH+ governor: '0x868Fe81C276d730A1995Dc84b642E795dFb8F753', timelock: '0x5d8A7DC9405F08F14541BA918c1Bf7eb2dACE556', + oracle: { + address: '0xf87d2F4d42856f0B6Eae140Aaf78bF0F777e9936', + threshold: 0.5, + }, }, /*{ rToken: '0xaCdf0DBA4B9839b96221a8487e9ca660a48212be', // hyUSD (mainnet) @@ -89,5 +99,9 @@ export const BASE_DEPLOYMENTS: RTokenDeployment[] = [ rToken: '0xCb327b99fF831bF8223cCEd12B1338FF3aA322Ff', // bsdETH governor: '0x21fBa52dA03e1F964fa521532f8B8951fC212055', timelock: '0xe664d294824C2A8C952A10c4034e1105d2907F46', + oracle: { + address: '0xD41310aCF5fA54CDd1970155ac32D708B376Dff6', + threshold: 1.25, // Higher threshold to account for melting and time elapsed + }, }, ] diff --git a/tasks/validation/utils/oracles.ts b/tasks/validation/utils/oracles.ts index 8bbb72506..f632737b7 100644 --- a/tasks/validation/utils/oracles.ts +++ b/tasks/validation/utils/oracles.ts @@ -6,6 +6,7 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types' import { BigNumber } from 'ethers' import { AggregatorV3Interface } from '@typechain/index' import { ONE_ADDRESS } from '../../../common/constants' +import { MAINNET_DEPLOYMENTS, BASE_DEPLOYMENTS, RTokenDeployment, OracleConfig } from './constants' export const overrideOracle = async ( hre: HardhatRuntimeEnvironment, @@ -269,3 +270,59 @@ export const setOraclePrice = async ( await oracle.updateAnswer(value) } + +export const getRTokenOracle = (rTokenAddress: string): OracleConfig | undefined => { + const allDeployments: RTokenDeployment[] = [...MAINNET_DEPLOYMENTS, ...BASE_DEPLOYMENTS] + const deployment = allDeployments.find( + (d) => d.rToken.toLowerCase() === rTokenAddress.toLowerCase() + ) + return deployment?.oracle +} + +export const getRTokenOraclePrice = async ( + hre: HardhatRuntimeEnvironment, + oracleAddress: string +): Promise => { + // Try Chainlink interface first + try { + const oracle = await hre.ethers.getContractAt('AggregatorV3Interface', oracleAddress) + const roundData = await oracle.latestRoundData() + return roundData.answer + } catch { + // Fallback to price() interface + const oracle = await hre.ethers.getContractAt( + ['function price() external view returns (uint256)'], + oracleAddress + ) + return await oracle.price() + } +} + +export const validateRTokenOraclePriceChange = ( + priceBefore: BigNumber, + priceAfter: BigNumber, + rTokenAddress: string, + threshold: number +): void => { + if (priceBefore.isZero()) { + throw new Error(`Invalid price for RToken ${rTokenAddress}`) + } + + // Calculate bounds (e.g., 0.5% -> 9950/10000, 1.25% -> 9875/10000) + const lowerMultiplier = 10000 - threshold * 100 + const upperMultiplier = 10000 + threshold * 100 + const lowerBound = priceBefore.mul(lowerMultiplier).div(10000) + const upperBound = priceBefore.mul(upperMultiplier).div(10000) + + if (priceAfter.lt(lowerBound) || priceAfter.gt(upperBound)) { + throw new Error( + `RToken Oracle price outside allowed ${threshold}% range.\n` + + ` Price before: ${priceBefore.toString()}\n` + + ` Price after: ${priceAfter.toString()}\n` + + ` Allowed range: ${lowerBound.toString()} - ${upperBound.toString()}\n` + + ` RToken: ${rTokenAddress}` + ) + } + + console.log(`āœ… RToken Oracle price validation passed!\n`) +}