Skip to content
Merged
37 changes: 33 additions & 4 deletions tasks/validation/proposal-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions tasks/validation/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand All @@ -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)
Expand Down Expand Up @@ -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
},
},
]
57 changes: 57 additions & 0 deletions tasks/validation/utils/oracles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<BigNumber> => {
// 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`)
}
Loading