From 920b82b57865a170992d0b280e7ad07fb7efe4ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Tue, 29 Oct 2024 18:13:07 +0100 Subject: [PATCH] feat: add tests for OETHL2 --- common/configuration.ts | 3 + .../origin/OETHCollateralL2Base.test.ts | 258 ++++++++++++++++++ .../individual-collateral/origin/constants.ts | 31 +++ .../individual-collateral/origin/helpers.ts | 20 ++ 4 files changed, 312 insertions(+) create mode 100644 test/plugins/individual-collateral/origin/OETHCollateralL2Base.test.ts create mode 100644 test/plugins/individual-collateral/origin/constants.ts create mode 100644 test/plugins/individual-collateral/origin/helpers.ts diff --git a/common/configuration.ts b/common/configuration.ts index d5164fa63..d431b5c3b 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -86,6 +86,7 @@ export interface ITokens { wsgUSDbC?: string yvCurveUSDPcrvUSD?: string yvCurveUSDCcrvUSD?: string + wsuperOETHb?: string pyUSD?: string aEthPyUSD?: string @@ -513,6 +514,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { sUSDbC: '0x4c80e24119cfb836cdf0a6b53dc23f04f7e652ca', wstETH: '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452', STG: '0xE3B53AF74a4BF62Ae5511055290838050bf764Df', + wsuperOETHb: '0x7FcD174E80f264448ebeE8c88a7C4476AAF58Ea6' }, chainlinkFeeds: { DAI: '0x591e79239a7d679378ec8c847e5038150364c78f', // 0.3%, 24hr @@ -529,6 +531,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { stETHETH: '0xf586d0728a47229e747d824a939000Cf21dEF5A0', // 0.5%, 24h ETHUSD: '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70', // 0.15%, 20min wstETHstETH: '0xB88BAc61a4Ca37C43a3725912B1f472c9A5bc061', // 0.5%, 24h + wsuperOETHb: '0x28C964c985fe84736fAdc7Cf0bBd58B54bc7CF93' }, GNOSIS_EASY_AUCTION: '0xb1875Feaeea32Bbb02DE83D81772e07E37A40f02', // mock COMET_REWARDS: '0x123964802e6ABabBE1Bc9547D72Ef1B69B00A6b1', diff --git a/test/plugins/individual-collateral/origin/OETHCollateralL2Base.test.ts b/test/plugins/individual-collateral/origin/OETHCollateralL2Base.test.ts new file mode 100644 index 000000000..62330b864 --- /dev/null +++ b/test/plugins/individual-collateral/origin/OETHCollateralL2Base.test.ts @@ -0,0 +1,258 @@ +import collateralTests from '../collateralTests' +import { setStorageAt, getStorageAt } from '@nomicfoundation/hardhat-network-helpers' +import { CollateralFixtureContext, CollateralOpts, MintCollateralFunc } from '../pluginTestTypes' +import { mintWSUPEROETHB } from './helpers' +import { expect } from 'chai' +import { ethers } from 'hardhat' +import { ContractFactory, BigNumber, BigNumberish } from 'ethers' +import { + ERC20Mock, + MockV3Aggregator, + MockV3Aggregator__factory, + TestICollateral, + IWSuperOETHb, +} from '../../../../typechain' +import { pushOracleForward } from '../../../utils/oracles' +import { bn, fp } from '../../../../common/numbers' +import { ZERO_ADDRESS } from '../../../../common/constants' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + PRICE_TIMEOUT, + MAX_TRADE_VOL, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + FORK_BLOCK_BASE, + BASE_PRICE_FEEDS, + BASE_FEEDS_TIMEOUT, + BASE_ORACLE_ERROR, + BASE_WSUPEROETHB, + BASE_WSUPEROETHB_WHALE, +} from './constants' +import { getResetFork } from '../helpers' + +/* + Define interfaces +*/ +interface WSUPEROETHBCollateralFixtureContext extends CollateralFixtureContext { + wsuperoethb: IWSuperOETHb + targetPerRefChainlinkFeed: MockV3Aggregator + uoaPerTargetChainlinkFeed: MockV3Aggregator +} + +/* + Define deployment functions +*/ + +interface WSUPEROETHBCollateralOpts extends CollateralOpts { + targetPerTokChainlinkFeed?: string + uoaPerTargetChainlinkFeed?: string + uoaPerTargetChainlinkTimeout?: BigNumberish +} + +export const defaultWSUPEROETHBCollateralOpts: WSUPEROETHBCollateralOpts = { + erc20: BASE_WSUPEROETHB, + targetName: ethers.utils.formatBytes32String('ETH'), + rewardERC20: ZERO_ADDRESS, + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: BASE_PRICE_FEEDS.wsuperOETHb_ETH, // ignored + oracleTimeout: '1000', // ignored + oracleError: BASE_ORACLE_ERROR, + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + targetPerTokChainlinkFeed: BASE_PRICE_FEEDS.wsuperOETHb_ETH, + uoaPerTargetChainlinkFeed: BASE_PRICE_FEEDS.ETH_USD, + uoaPerTargetChainlinkTimeout: BASE_FEEDS_TIMEOUT.ETH_USD, + revenueHiding: fp('1e-4'), +} + +export const deployCollateral = async ( + opts: WSUPEROETHBCollateralOpts = {} +): Promise => { + opts = { ...defaultWSUPEROETHBCollateralOpts, ...opts } + + const WSuperOETHbCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'OETHCollateralL2Base' + ) + + const collateral = await WSuperOETHbCollateralFactory.deploy( + { + erc20: opts.erc20, + targetName: opts.targetName, + rewardERC20: opts.rewardERC20, + priceTimeout: opts.priceTimeout, + chainlinkFeed: opts.chainlinkFeed, + oracleError: opts.oracleError, + oracleTimeout: opts.oracleTimeout, + maxTradeVolume: opts.maxTradeVolume, + defaultThreshold: opts.defaultThreshold, + delayUntilDefault: opts.delayUntilDefault, + }, + opts.revenueHiding, + opts.targetPerTokChainlinkFeed, + opts.chainlinkFeed ?? opts.uoaPerTargetChainlinkFeed, + opts.uoaPerTargetChainlinkTimeout, + { gasLimit: 2000000000 } + ) + + // Push forward chainlink feed + await pushOracleForward(opts.uoaPerTargetChainlinkFeed!) + + await collateral.deployed() + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return collateral +} + +const defaultAnswers = { + targetPerRefChainlinkFeed: bn('1e18'), + uoaPerTargetChainlinkFeed: bn('2000e8'), + refPerTokenChainlinkFeed: bn('1.1e18'), +} + +type Fixture = () => Promise + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultWSUPEROETHBCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + const targetPerRefChainlinkFeed = await MockV3AggregatorFactory.deploy( + 18, + defaultAnswers.targetPerRefChainlinkFeed + ) + const uoaPerTargetChainlinkFeed = await MockV3AggregatorFactory.deploy(8, defaultAnswers.uoaPerTargetChainlinkFeed) + + collateralOpts.chainlinkFeed = uoaPerTargetChainlinkFeed.address + collateralOpts.uoaPerTargetChainlinkFeed = uoaPerTargetChainlinkFeed.address + + const wsuperOETHb = (await ethers.getContractAt('IWSuperOETHb', BASE_WSUPEROETHB)) as IWSuperOETHb + const rewardToken = (await ethers.getContractAt('ERC20Mock', ZERO_ADDRESS)) as ERC20Mock + const collateral = await deployCollateral(collateralOpts) + + return { + alice, + collateral, + wsuperoethb: wsuperOETHb, + tok: wsuperOETHb, + rewardToken, + chainlinkFeed: uoaPerTargetChainlinkFeed, + targetPerRefChainlinkFeed: targetPerRefChainlinkFeed, + uoaPerTargetChainlinkFeed, + } + } + + return makeCollateralFixtureContext +} + +/* + Define helper functions +*/ + +const mintCollateralTo: MintCollateralFunc = async ( + ctx: WSUPEROETHBCollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintWSUPEROETHB(ctx.wsuperoethb, user, amount, recipient, BASE_WSUPEROETHB_WHALE) +} + +const reduceTargetPerRef = async ( + ctx: WSUPEROETHBCollateralFixtureContext, + pctDecrease: BigNumberish +) => { + const lastRound = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = lastRound.answer.sub(lastRound.answer.mul(pctDecrease).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) +} + +const increaseTargetPerRef = async ( + ctx: WSUPEROETHBCollateralFixtureContext, + pctIncrease: BigNumberish +) => { + const lastRound = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = lastRound.answer.add(lastRound.answer.mul(pctIncrease).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) +} + +const reduceRefPerTok = async (ctx: WSUPEROETHBCollateralFixtureContext, pctDecrease: BigNumberish) => { + const slot = 2 + const storedTotalSupply = BigNumber.from(await getStorageAt(ctx.tok.address, slot)) + const newStoredTotalAssets = storedTotalSupply.add(storedTotalSupply.mul(pctDecrease).div(100)) + await setStorageAt(ctx.tok.address, slot, newStoredTotalAssets) +} + +const increaseRefPerTok = async ( + ctx: WSUPEROETHBCollateralFixtureContext, + pctIncrease: BigNumberish +) => { + const slot = 2 + const storedTotalSupply = BigNumber.from(await getStorageAt(ctx.tok.address, slot)) + const newStoredTotalAssets = storedTotalSupply.sub(storedTotalSupply.mul(pctIncrease).div(100)) + await setStorageAt(ctx.tok.address, slot, newStoredTotalAssets) +} + +const getExpectedPrice = async (ctx: WSUPEROETHBCollateralFixtureContext): Promise => { + const uoaPerTargetChainlinkFeedAnswer = await ctx.uoaPerTargetChainlinkFeed.latestAnswer() + const uoaPerTargetChainlinkFeedDecimals = await ctx.uoaPerTargetChainlinkFeed.decimals() + + const refPerTok = await ctx.collateral.refPerTok() + + const result = uoaPerTargetChainlinkFeedAnswer + .mul(bn(10).pow(18 - uoaPerTargetChainlinkFeedDecimals)) + .mul(refPerTok) + .div(fp('1')) + + return result +} + +const collateralSpecificConstructorTests = () => { } + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificStatusTests = () => { } + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const beforeEachRewardsTest = async () => { } + +/* + Run the test suite +*/ + +const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + beforeEachRewardsTest, + makeCollateralFixtureContext, + mintCollateralTo, + reduceTargetPerRef, + increaseTargetPerRef, + reduceRefPerTok, + increaseRefPerTok, + getExpectedPrice, + itClaimsRewards: it.skip, + itChecksTargetPerRefDefault: it.skip, + itChecksTargetPerRefDefaultUp: it.skip, + itChecksRefPerTokDefault: it, + itChecksPriceChanges: it, + itChecksNonZeroDefaultThreshold: it, + itHasRevenueHiding: it, + resetFork: getResetFork(FORK_BLOCK_BASE), + collateralName: 'OETHCollateralL2Base', + chainlinkDefaultAnswer: defaultAnswers.uoaPerTargetChainlinkFeed, + itIsPricedByPeg: true, + itHasOracleRefPerTok: true, + targetNetwork: 'base', + toleranceDivisor: bn('1e2'), +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/origin/constants.ts b/test/plugins/individual-collateral/origin/constants.ts new file mode 100644 index 000000000..bdeb9a1f2 --- /dev/null +++ b/test/plugins/individual-collateral/origin/constants.ts @@ -0,0 +1,31 @@ +import { bn, fp } from '../../../../common/numbers' +import { networkConfig } from '../../../../common/configuration' +import { combinedError } from '../../../../scripts/deployment/utils' + +// Mainnet Addresses + +// Base Addresses +export const BASE_WSUPEROETHB = networkConfig['8453'].tokens.wsuperOETHb as string +export const BASE_WSUPEROETHB_WHALE = '0x190e5C6AabB2BeC4eB0B9b2274e9b62cdaEDF356' // Silo +export const FORK_BLOCK_BASE = 21698000 +export const BASE_PRICE_FEEDS = { + // traditional finance notation, opposite of our unit system + wsuperOETHb_ETH: networkConfig['8453'].chainlinkFeeds.wsuperOETHb, // {ETH/wsuperOETHb} + ETH_USD: networkConfig['8453'].chainlinkFeeds.ETHUSD, // {USD/ETH} +} +export const BASE_FEEDS_TIMEOUT = { + wsuperOETHb_ETH: bn(86400), + ETH_USD: bn(1200), +} +export const BASE_ORACLE_ERROR = combinedError( + fp('0.0015'), + combinedError(fp('0.005'), fp('0.005')) +) + +// Data +export const PRICE_TIMEOUT = bn('604800') // 1 week +export const ORACLE_TIMEOUT = bn(86400) // 24 hours in seconds +export const ORACLE_ERROR = fp('0.005') +export const DEFAULT_THRESHOLD = bn(5).mul(bn(10).pow(16)) // 0.05 +export const DELAY_UNTIL_DEFAULT = bn(86400) +export const MAX_TRADE_VOL = bn(1000) diff --git a/test/plugins/individual-collateral/origin/helpers.ts b/test/plugins/individual-collateral/origin/helpers.ts new file mode 100644 index 000000000..4f573f153 --- /dev/null +++ b/test/plugins/individual-collateral/origin/helpers.ts @@ -0,0 +1,20 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { IERC20 } from '../../../../typechain' +import { whileImpersonating } from '../../../utils/impersonation' +import { BigNumberish } from 'ethers' +import { FORK_BLOCK_BASE, BASE_WSUPEROETHB_WHALE } from './constants' +import { getResetFork } from '../helpers' + +export const mintWSUPEROETHB = async ( + wsuperoethb: IERC20, + account: SignerWithAddress, + amount: BigNumberish, + recipient: string, + whale: string = BASE_WSUPEROETHB_WHALE +) => { + await whileImpersonating(whale, async (wsuperoethbWhale) => { + await wsuperoethb.connect(wsuperoethbWhale).transfer(recipient, amount) + }) +} + +export const resetFork = getResetFork(FORK_BLOCK_BASE)