From aba6a62861f45f3a3766ea0431e16a3c318a3464 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Wed, 18 Oct 2023 16:49:41 -0400 Subject: [PATCH 1/8] initial integration test setup --- .../individual-collateral/collateralTests.ts | 282 +++++++++++++++++- 1 file changed, 272 insertions(+), 10 deletions(-) diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index 9de866af6a..5d8ade67b1 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -2,24 +2,28 @@ import { expect } from 'chai' import hre, { ethers } from 'hardhat' import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { BigNumber } from 'ethers' +import { BigNumber, ContractFactory } from 'ethers' import { useEnv } from '#/utils/env' import { getChainId } from '../../../common/blockchain-utils' -import { networkConfig } from '../../../common/configuration' import { bn, fp } from '../../../common/numbers' +import { DefaultFixture, Fixture, getDefaultFixture, ORACLE_TIMEOUT } from './fixtures' +import { expectInIndirectReceipt } from '../../../common/events' import { - IERC20Metadata, - InvalidMockV3Aggregator, - MockV3Aggregator, - TestICollateral, -} from '../../../typechain' + IConfig, + IGovParams, + IGovRoles, + IRevenueShare, + IRTokenConfig, + IRTokenSetup, + networkConfig, +} from '../../../common/configuration' import { advanceTime, advanceBlocks, getLatestBlockTimestamp, setNextBlockTimestamp, } from '../../utils/time' -import { MAX_UINT48, MAX_UINT192 } from '../../../common/constants' +import { MAX_UINT48, MAX_UINT192, ZERO_ADDRESS } from '../../../common/constants' import { CollateralFixtureContext, CollateralTestSuiteFixtures, @@ -31,10 +35,42 @@ import { expectPrice, expectUnpriced, } from '../../utils/oracles' +import { + Asset, + BadERC20, + ComptrollerMock, + CTokenFiatCollateral, + CTokenMock, + CTokenWrapper, + CTokenWrapperMock, + ERC20Mock, + FacadeRead, + FacadeTest, + FacadeWrite, + FiatCollateral, + IAssetRegistry, + IERC20Metadata, + InvalidMockV3Aggregator, + MockV3Aggregator, + NonFiatCollateral, + RTokenAsset, + SelfReferentialCollateral, + TestIBackingManager, + TestIBasketHandler, + TestICollateral, + TestIDeployer, + TestIMain, + TestIRToken, +} from '../../../typechain' import snapshotGasCost from '../../utils/snapshotGasCost' -import { IMPLEMENTATION, Implementation } from '../../fixtures' +import { + IMPLEMENTATION, + Implementation, + ORACLE_ERROR, + PRICE_TIMEOUT, + REVENUE_HIDING, +} from '../../fixtures' -// const describeFork = useEnv('FORK') ? describe : describe.skip const getDescribeFork = (targetNetwork = 'mainnet') => { return useEnv('FORK') && useEnv('FORK_NETWORK') === targetNetwork ? describe : describe.skip } @@ -576,5 +612,231 @@ export default function fn( }) }) }) + + describe.only('integration tests', () => { + let ctx: X + let owner: SignerWithAddress + + let chainId: number + + let defaultFixture: Fixture + + // Tokens/Assets + let rsr: ERC20Mock + let rsrAsset: Asset + let pairedColl: TestICollateral + + // Core Contracts + let main: TestIMain + let rToken: TestIRToken + let rTokenAsset: RTokenAsset + let assetRegistry: IAssetRegistry + let backingManager: TestIBackingManager + let basketHandler: TestIBasketHandler + + let deployer: TestIDeployer + let facade: FacadeRead + let facadeTest: FacadeTest + let facadeWrite: FacadeWrite + let govParams: IGovParams + let govRoles: IGovRoles + + const config = { + dist: { + rTokenDist: bn(40), // 2/5 RToken + rsrDist: bn(60), // 3/5 RSR + }, + minTradeVolume: fp('1e4'), // $10k + rTokenMaxTradeVolume: fp('1e6'), // $1M + shortFreeze: bn('259200'), // 3 days + longFreeze: bn('2592000'), // 30 days + rewardRatio: bn('1069671574938'), // approx. half life of 90 days + unstakingDelay: bn('1209600'), // 2 weeks + withdrawalLeak: fp('0'), // 0%; always refresh + warmupPeriod: bn('60'), // (the delay _after_ SOUND was regained) + tradingDelay: bn('0'), // (the delay _after_ default has been confirmed) + batchAuctionLength: bn('900'), // 15 minutes + dutchAuctionLength: bn('1800'), // 30 minutes + backingBuffer: fp('0'), // 0% + maxTradeSlippage: fp('0.01'), // 1% + issuanceThrottle: { + amtRate: fp('1e6'), // 1M RToken + pctRate: fp('0.05'), // 5% + }, + redemptionThrottle: { + amtRate: fp('1e6'), // 1M RToken + pctRate: fp('0.05'), // 5% + }, + } + + const setPairedCollateral = async (target: string) => { + const MockV3AggregatorFactory: ContractFactory = await ethers.getContractFactory( + 'MockV3Aggregator' + ) + const chainlinkFeed: MockV3Aggregator = ( + await MockV3AggregatorFactory.deploy(8, bn('1e8')) + ) + + if (target == ethers.utils.formatBytes32String('USD')) { + // USD + + const erc20Addr = networkConfig[chainId].tokens.USDC + const FiatCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'FiatCollateral' + ) + pairedColl = await FiatCollateralFactory.deploy({ + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20Addr, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT, + target: ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01'), // 1% + delayUntilDefault: bn('86400'), // 24h, + }) + } else if (target == ethers.utils.formatBytes32String('ETH')) { + const erc20Addr = networkConfig[chainId].tokens.WETH + const SelfReferentialFactory: ContractFactory = await ethers.getContractFactory( + 'SelfReferentialCollateral' + ) + pairedColl = await SelfReferentialFactory.deploy({ + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20Addr, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('ETH'), + defaultThreshold: fp('0'), // 0% + delayUntilDefault: bn('0'), // 0, + }) + } else if (target == ethers.utils.formatBytes32String('BTC')) { + // BTC + const targetUnitOracle: MockV3Aggregator = ( + await MockV3AggregatorFactory.deploy(8, bn('1e8')) + ) + const erc20Addr = networkConfig[chainId].tokens.WBTC + const NonFiatFactory: ContractFactory = await ethers.getContractFactory( + 'NonFiatCollateral' + ) + pairedColl = await NonFiatFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20Addr, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('BTC'), + defaultThreshold: fp('0.01'), // 1% + delayUntilDefault: bn('86400'), // 24h, + }, + targetUnitOracle.address, + ORACLE_TIMEOUT + ) + } else { + throw new Error(`Unknown target: ${target}`) + } + + // Should be SOUND after setup + await pairedColl.refresh() + expect(await pairedColl.status()).to.equal(CollateralStatus.SOUND) + } + + before(async () => { + defaultFixture = await getDefaultFixture(collateralName) + chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + }) + + beforeEach(async () => { + ;[, owner] = await ethers.getSigners() + ctx = await loadFixture(makeCollateralFixtureContext(owner, {})) + + // Set up protocol + ;({ rsr, rsrAsset, deployer, facade, facadeTest, facadeWrite, govParams } = + await loadFixture(defaultFixture)) + + // Set a paired collateral of the same targetName + await setPairedCollateral(await ctx.collateral.targetName()) + + // Set primary basket + const rTokenSetup: IRTokenSetup = { + assets: [], + primaryBasket: [ctx.collateral.address, pairedColl.address], + weights: [fp('0.5'), fp('0.5')], + backups: [ + { + backupUnit: await ctx.collateral.targetName(), + diversityFactor: bn('1'), + backupCollateral: [pairedColl.address], + }, + ], + beneficiaries: [], + } + + // Deploy RToken via FacadeWrite + const receipt = await ( + await facadeWrite.connect(owner).deployRToken( + { + name: 'RTKN RToken', + symbol: 'RTKN', + mandate: 'mandate', + params: config, + }, + rTokenSetup + ) + ).wait() + + // Get Main + const mainAddr = expectInIndirectReceipt(receipt, deployer.interface, 'RTokenCreated').args + .main + main = await ethers.getContractAt('TestIMain', mainAddr) + + // Get core contracts + assetRegistry = ( + await ethers.getContractAt('IAssetRegistry', await main.assetRegistry()) + ) + backingManager = ( + await ethers.getContractAt('TestIBackingManager', await main.backingManager()) + ) + basketHandler = ( + await ethers.getContractAt('TestIBasketHandler', await main.basketHandler()) + ) + rToken = await ethers.getContractAt('TestIRToken', await main.rToken()) + rTokenAsset = ( + await ethers.getContractAt('RTokenAsset', await assetRegistry.toAsset(rToken.address)) + ) + + // Set initial governance roles + govRoles = { + owner: owner.address, + guardian: ZERO_ADDRESS, + pausers: [], + shortFreezers: [], + longFreezers: [], + } + // Setup owner and unpause + await facadeWrite.connect(owner).setupGovernance( + rToken.address, + false, // do not deploy governance + true, // unpaused + govParams, // mock values, not relevant + govRoles + ) + + // Advance past warmup period + await setNextBlockTimestamp( + (await getLatestBlockTimestamp()) + (await basketHandler.warmupPeriod()) + ) + }) + + it('does setup correctly', async () => { + expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) + }) + }) }) } From f6d403360f1c0a243ab03fbd7ccfe43691b43a34 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Wed, 18 Oct 2023 17:04:47 -0400 Subject: [PATCH 2/8] do it the right way with a dual fixture --- .../individual-collateral/collateralTests.ts | 173 ++++++++++-------- 1 file changed, 97 insertions(+), 76 deletions(-) diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index 5d8ade67b1..0ffe474b03 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -621,6 +621,8 @@ export default function fn( let defaultFixture: Fixture + let amt: BigNumber + // Tokens/Assets let rsr: ERC20Mock let rsrAsset: Asset @@ -669,79 +671,16 @@ export default function fn( }, } - const setPairedCollateral = async (target: string) => { - const MockV3AggregatorFactory: ContractFactory = await ethers.getContractFactory( - 'MockV3Aggregator' - ) - const chainlinkFeed: MockV3Aggregator = ( - await MockV3AggregatorFactory.deploy(8, bn('1e8')) - ) - - if (target == ethers.utils.formatBytes32String('USD')) { - // USD + interface DualFixture { + ctx: X + protocol: DefaultFixture + } - const erc20Addr = networkConfig[chainId].tokens.USDC - const FiatCollateralFactory: ContractFactory = await ethers.getContractFactory( - 'FiatCollateral' - ) - pairedColl = await FiatCollateralFactory.deploy({ - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: chainlinkFeed.address, - oracleError: ORACLE_ERROR, - erc20: erc20Addr, - maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, - target: ethers.utils.formatBytes32String('USD'), - defaultThreshold: fp('0.01'), // 1% - delayUntilDefault: bn('86400'), // 24h, - }) - } else if (target == ethers.utils.formatBytes32String('ETH')) { - const erc20Addr = networkConfig[chainId].tokens.WETH - const SelfReferentialFactory: ContractFactory = await ethers.getContractFactory( - 'SelfReferentialCollateral' - ) - pairedColl = await SelfReferentialFactory.deploy({ - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: chainlinkFeed.address, - oracleError: ORACLE_ERROR, - erc20: erc20Addr, - maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, - targetName: ethers.utils.formatBytes32String('ETH'), - defaultThreshold: fp('0'), // 0% - delayUntilDefault: bn('0'), // 0, - }) - } else if (target == ethers.utils.formatBytes32String('BTC')) { - // BTC - const targetUnitOracle: MockV3Aggregator = ( - await MockV3AggregatorFactory.deploy(8, bn('1e8')) - ) - const erc20Addr = networkConfig[chainId].tokens.WBTC - const NonFiatFactory: ContractFactory = await ethers.getContractFactory( - 'NonFiatCollateral' - ) - pairedColl = await NonFiatFactory.deploy( - { - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: chainlinkFeed.address, - oracleError: ORACLE_ERROR, - erc20: erc20Addr, - maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, - targetName: ethers.utils.formatBytes32String('BTC'), - defaultThreshold: fp('0.01'), // 1% - delayUntilDefault: bn('86400'), // 24h, - }, - targetUnitOracle.address, - ORACLE_TIMEOUT - ) - } else { - throw new Error(`Unknown target: ${target}`) + const dualFixture: Fixture = async function (): Promise { + return { + ctx: await loadFixture(makeCollateralFixtureContext(owner, {})), + protocol: await loadFixture(defaultFixture), } - - // Should be SOUND after setup - await pairedColl.refresh() - expect(await pairedColl.status()).to.equal(CollateralStatus.SOUND) } before(async () => { @@ -750,15 +689,15 @@ export default function fn( if (!networkConfig[chainId]) { throw new Error(`Missing network configuration for ${hre.network.name}`) } + ;[, owner] = await ethers.getSigners() }) beforeEach(async () => { - ;[, owner] = await ethers.getSigners() - ctx = await loadFixture(makeCollateralFixtureContext(owner, {})) + let protocol: DefaultFixture + ;({ ctx, protocol } = await loadFixture(dualFixture)) + ;({ rsr, rsrAsset, deployer, facade, facadeTest, facadeWrite, govParams } = protocol) - // Set up protocol - ;({ rsr, rsrAsset, deployer, facade, facadeTest, facadeWrite, govParams } = - await loadFixture(defaultFixture)) + amt = fp('1000') // Set a paired collateral of the same targetName await setPairedCollateral(await ctx.collateral.targetName()) @@ -837,6 +776,88 @@ export default function fn( it('does setup correctly', async () => { expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) }) + + it('does issuance', async () => { + const quantities = await facade.callStatic.issue(rToken.address, amt) + console.log(quantities) + }) + + // === Helpers === + + const setPairedCollateral = async (target: string) => { + const MockV3AggregatorFactory: ContractFactory = await ethers.getContractFactory( + 'MockV3Aggregator' + ) + const chainlinkFeed: MockV3Aggregator = ( + await MockV3AggregatorFactory.deploy(8, bn('1e8')) + ) + + if (target == ethers.utils.formatBytes32String('USD')) { + // USD + + const erc20Addr = networkConfig[chainId].tokens.USDC + const FiatCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'FiatCollateral' + ) + pairedColl = await FiatCollateralFactory.deploy({ + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20Addr, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT, + target: ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01'), // 1% + delayUntilDefault: bn('86400'), // 24h, + }) + } else if (target == ethers.utils.formatBytes32String('ETH')) { + const erc20Addr = networkConfig[chainId].tokens.WETH + const SelfReferentialFactory: ContractFactory = await ethers.getContractFactory( + 'SelfReferentialCollateral' + ) + pairedColl = await SelfReferentialFactory.deploy({ + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20Addr, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('ETH'), + defaultThreshold: fp('0'), // 0% + delayUntilDefault: bn('0'), // 0, + }) + } else if (target == ethers.utils.formatBytes32String('BTC')) { + // BTC + const targetUnitOracle: MockV3Aggregator = ( + await MockV3AggregatorFactory.deploy(8, bn('1e8')) + ) + const erc20Addr = networkConfig[chainId].tokens.WBTC + const NonFiatFactory: ContractFactory = await ethers.getContractFactory( + 'NonFiatCollateral' + ) + pairedColl = await NonFiatFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20Addr, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('BTC'), + defaultThreshold: fp('0.01'), // 1% + delayUntilDefault: bn('86400'), // 24h, + }, + targetUnitOracle.address, + ORACLE_TIMEOUT + ) + } else { + throw new Error(`Unknown target: ${target}`) + } + + // Should be SOUND after setup + await pairedColl.refresh() + expect(await pairedColl.status()).to.equal(CollateralStatus.SOUND) + } }) }) } From eb1d35afd14110c0c0780974f3f18c29f4ee0c15 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Fri, 20 Oct 2023 12:23:31 -0400 Subject: [PATCH 3/8] test issuance + redemption --- .../individual-collateral/collateralTests.ts | 106 ++++++++++++------ 1 file changed, 73 insertions(+), 33 deletions(-) diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index 0ffe474b03..456d010b74 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -8,6 +8,7 @@ import { getChainId } from '../../../common/blockchain-utils' import { bn, fp } from '../../../common/numbers' import { DefaultFixture, Fixture, getDefaultFixture, ORACLE_TIMEOUT } from './fixtures' import { expectInIndirectReceipt } from '../../../common/events' +import { whileImpersonating } from '../../utils/impersonation' import { IConfig, IGovParams, @@ -23,7 +24,7 @@ import { getLatestBlockTimestamp, setNextBlockTimestamp, } from '../../utils/time' -import { MAX_UINT48, MAX_UINT192, ZERO_ADDRESS } from '../../../common/constants' +import { MAX_UINT48, MAX_UINT192, MAX_UINT256, ZERO_ADDRESS } from '../../../common/constants' import { CollateralFixtureContext, CollateralTestSuiteFixtures, @@ -616,6 +617,7 @@ export default function fn( describe.only('integration tests', () => { let ctx: X let owner: SignerWithAddress + let addr1: SignerWithAddress let chainId: number @@ -627,6 +629,9 @@ export default function fn( let rsr: ERC20Mock let rsrAsset: Asset let pairedColl: TestICollateral + let pairedERC20: ERC20Mock + let collateralERC20: IERC20Metadata + let collateral: TestICollateral // Core Contracts let main: TestIMain @@ -671,17 +676,18 @@ export default function fn( }, } - interface DualFixture { + interface IntegrationFixture { ctx: X protocol: DefaultFixture } - const dualFixture: Fixture = async function (): Promise { - return { - ctx: await loadFixture(makeCollateralFixtureContext(owner, {})), - protocol: await loadFixture(defaultFixture), + const integrationFixture: Fixture = + async function (): Promise { + return { + ctx: await loadFixture(makeCollateralFixtureContext(owner, {})), + protocol: await loadFixture(defaultFixture), + } } - } before(async () => { defaultFixture = await getDefaultFixture(collateralName) @@ -689,27 +695,35 @@ export default function fn( if (!networkConfig[chainId]) { throw new Error(`Missing network configuration for ${hre.network.name}`) } - ;[, owner] = await ethers.getSigners() + ;[, owner, addr1] = await ethers.getSigners() }) beforeEach(async () => { let protocol: DefaultFixture - ;({ ctx, protocol } = await loadFixture(dualFixture)) + ;({ ctx, protocol } = await loadFixture(integrationFixture)) + ;({ collateral } = ctx) ;({ rsr, rsrAsset, deployer, facade, facadeTest, facadeWrite, govParams } = protocol) - amt = fp('1000') + amt = fp('1') - // Set a paired collateral of the same targetName - await setPairedCollateral(await ctx.collateral.targetName()) + // Create a paired collateral of the same targetName + pairedColl = await makePairedCollateral(await ctx.collateral.targetName()) + await pairedColl.refresh() + expect(await pairedColl.status()).to.equal(CollateralStatus.SOUND) + pairedERC20 = await ethers.getContractAt('ERC20Mock', await pairedColl.erc20()) + + // Prep collateral + await mintCollateralTo(ctx, fp('1000'), addr1, addr1.address) + collateralERC20 = await ethers.getContractAt('IERC20Metadata', await collateral.erc20()) // Set primary basket const rTokenSetup: IRTokenSetup = { assets: [], - primaryBasket: [ctx.collateral.address, pairedColl.address], + primaryBasket: [collateral.address, pairedColl.address], weights: [fp('0.5'), fp('0.5')], backups: [ { - backupUnit: await ctx.collateral.targetName(), + backupUnit: await collateral.targetName(), diversityFactor: bn('1'), backupCollateral: [pairedColl.address], }, @@ -774,17 +788,23 @@ export default function fn( }) it('does setup correctly', async () => { + await assetRegistry.refresh() expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) }) - it('does issuance', async () => { - const quantities = await facade.callStatic.issue(rToken.address, amt) - console.log(quantities) + it('does issuance + redemption', async () => { + // Should issue + await collateralERC20.connect(addr1).approve(rToken.address, MAX_UINT256) + await pairedERC20.connect(addr1).approve(rToken.address, MAX_UINT256) + await rToken.connect(addr1).issue(amt) + + // Should redeem + await rToken.connect(addr1).redeem(amt) }) - // === Helpers === + // === Integration Helpers === - const setPairedCollateral = async (target: string) => { + const makePairedCollateral = async (target: string): Promise => { const MockV3AggregatorFactory: ContractFactory = await ethers.getContractFactory( 'MockV3Aggregator' ) @@ -794,16 +814,23 @@ export default function fn( if (target == ethers.utils.formatBytes32String('USD')) { // USD - - const erc20Addr = networkConfig[chainId].tokens.USDC + const erc20 = await ethers.getContractAt( + 'IERC20Metadata', + networkConfig[chainId].tokens.USDC! + ) + await whileImpersonating('0x40ec5b33f54e0e8a33a975908c5ba1c14e5bbbdf', async (signer) => { + await erc20 + .connect(signer) + .transfer(addr1.address, await erc20.balanceOf(signer.address)) + }) const FiatCollateralFactory: ContractFactory = await ethers.getContractFactory( 'FiatCollateral' ) - pairedColl = await FiatCollateralFactory.deploy({ + return await FiatCollateralFactory.deploy({ priceTimeout: PRICE_TIMEOUT, chainlinkFeed: chainlinkFeed.address, oracleError: ORACLE_ERROR, - erc20: erc20Addr, + erc20: erc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, oracleTimeout: ORACLE_TIMEOUT, target: ethers.utils.formatBytes32String('USD'), @@ -811,15 +838,24 @@ export default function fn( delayUntilDefault: bn('86400'), // 24h, }) } else if (target == ethers.utils.formatBytes32String('ETH')) { - const erc20Addr = networkConfig[chainId].tokens.WETH + // ETH + const erc20 = await ethers.getContractAt( + 'IERC20Metadata', + networkConfig[chainId].tokens.WETH! + ) + await whileImpersonating('0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E', async (signer) => { + await erc20 + .connect(signer) + .transfer(addr1.address, await erc20.balanceOf(signer.address)) + }) const SelfReferentialFactory: ContractFactory = await ethers.getContractFactory( 'SelfReferentialCollateral' ) - pairedColl = await SelfReferentialFactory.deploy({ + return await SelfReferentialFactory.deploy({ priceTimeout: PRICE_TIMEOUT, chainlinkFeed: chainlinkFeed.address, oracleError: ORACLE_ERROR, - erc20: erc20Addr, + erc20: erc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, oracleTimeout: ORACLE_TIMEOUT, targetName: ethers.utils.formatBytes32String('ETH'), @@ -831,16 +867,24 @@ export default function fn( const targetUnitOracle: MockV3Aggregator = ( await MockV3AggregatorFactory.deploy(8, bn('1e8')) ) - const erc20Addr = networkConfig[chainId].tokens.WBTC + const erc20 = await ethers.getContractAt( + 'IERC20Metadata', + networkConfig[chainId].tokens.WBTC! + ) + await whileImpersonating('0xccf4429db6322d5c611ee964527d42e5d685dd6a', async (signer) => { + await erc20 + .connect(signer) + .transfer(addr1.address, await erc20.balanceOf(signer.address)) + }) const NonFiatFactory: ContractFactory = await ethers.getContractFactory( 'NonFiatCollateral' ) - pairedColl = await NonFiatFactory.deploy( + return await NonFiatFactory.deploy( { priceTimeout: PRICE_TIMEOUT, chainlinkFeed: chainlinkFeed.address, oracleError: ORACLE_ERROR, - erc20: erc20Addr, + erc20: erc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, oracleTimeout: ORACLE_TIMEOUT, targetName: ethers.utils.formatBytes32String('BTC'), @@ -853,10 +897,6 @@ export default function fn( } else { throw new Error(`Unknown target: ${target}`) } - - // Should be SOUND after setup - await pairedColl.refresh() - expect(await pairedColl.status()).to.equal(CollateralStatus.SOUND) } }) }) From 108ef958103e74ece7f87e8dfa58edc958f83de1 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Fri, 20 Oct 2023 18:47:44 -0400 Subject: [PATCH 4/8] plugin integration tests with the rest of protocol; wcUSDCv3 failing as expected --- common/numbers.ts | 4 +- .../individual-collateral/collateralTests.ts | 182 ++++---- .../curve/collateralTests.ts | 392 +++++++++++++++++- 3 files changed, 501 insertions(+), 77 deletions(-) diff --git a/common/numbers.ts b/common/numbers.ts index 6d53f464d1..d49a2a6606 100644 --- a/common/numbers.ts +++ b/common/numbers.ts @@ -16,7 +16,9 @@ export const pow10 = (exponent: BigNumberish): BigNumber => { // Convert `x` to a new BigNumber with decimals = `decimals`. // Input should have SCALE_DECIMALS (18) decimal places, and `decimals` should be less than 18. export const toBNDecimals = (x: BigNumberish, decimals: number): BigNumber => { - return BigNumber.from(x).div(pow10(SCALE_DECIMALS - decimals)) + return decimals < SCALE_DECIMALS + ? BigNumber.from(x).div(pow10(SCALE_DECIMALS - decimals)) + : BigNumber.from(x).mul(pow10(decimals - SCALE_DECIMALS)) } // Convert to the BigNumber representing a Fix from a BigNumberish. diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index 456d010b74..3facc4c92b 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -1,30 +1,30 @@ import { expect } from 'chai' import hre, { ethers } from 'hardhat' import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { BigNumber, ContractFactory } from 'ethers' import { useEnv } from '#/utils/env' import { getChainId } from '../../../common/blockchain-utils' -import { bn, fp } from '../../../common/numbers' +import { bn, fp, toBNDecimals } from '../../../common/numbers' import { DefaultFixture, Fixture, getDefaultFixture, ORACLE_TIMEOUT } from './fixtures' import { expectInIndirectReceipt } from '../../../common/events' import { whileImpersonating } from '../../utils/impersonation' -import { - IConfig, - IGovParams, - IGovRoles, - IRevenueShare, - IRTokenConfig, - IRTokenSetup, - networkConfig, -} from '../../../common/configuration' +import { IGovParams, IGovRoles, IRTokenSetup, networkConfig } from '../../../common/configuration' import { advanceTime, advanceBlocks, + getLatestBlockNumber, getLatestBlockTimestamp, setNextBlockTimestamp, } from '../../utils/time' -import { MAX_UINT48, MAX_UINT192, MAX_UINT256, ZERO_ADDRESS } from '../../../common/constants' +import { + MAX_UINT48, + MAX_UINT192, + MAX_UINT256, + TradeKind, + ZERO_ADDRESS, +} from '../../../common/constants' import { CollateralFixtureContext, CollateralTestSuiteFixtures, @@ -37,40 +37,22 @@ import { expectUnpriced, } from '../../utils/oracles' import { - Asset, - BadERC20, - ComptrollerMock, - CTokenFiatCollateral, - CTokenMock, - CTokenWrapper, - CTokenWrapperMock, ERC20Mock, - FacadeRead, - FacadeTest, FacadeWrite, - FiatCollateral, IAssetRegistry, IERC20Metadata, InvalidMockV3Aggregator, MockV3Aggregator, - NonFiatCollateral, - RTokenAsset, - SelfReferentialCollateral, TestIBackingManager, TestIBasketHandler, TestICollateral, TestIDeployer, TestIMain, + TestIRevenueTrader, TestIRToken, } from '../../../typechain' import snapshotGasCost from '../../utils/snapshotGasCost' -import { - IMPLEMENTATION, - Implementation, - ORACLE_ERROR, - PRICE_TIMEOUT, - REVENUE_HIDING, -} from '../../fixtures' +import { IMPLEMENTATION, Implementation, ORACLE_ERROR, PRICE_TIMEOUT } from '../../fixtures' const getDescribeFork = (targetNetwork = 'mainnet') => { return useEnv('FORK') && useEnv('FORK_NETWORK') === targetNetwork ? describe : describe.skip @@ -614,7 +596,9 @@ export default function fn( }) }) - describe.only('integration tests', () => { + describe('integration tests', () => { + before(resetFork) + let ctx: X let owner: SignerWithAddress let addr1: SignerWithAddress @@ -623,11 +607,9 @@ export default function fn( let defaultFixture: Fixture - let amt: BigNumber + let supply: BigNumber // Tokens/Assets - let rsr: ERC20Mock - let rsrAsset: Asset let pairedColl: TestICollateral let pairedERC20: ERC20Mock let collateralERC20: IERC20Metadata @@ -636,25 +618,23 @@ export default function fn( // Core Contracts let main: TestIMain let rToken: TestIRToken - let rTokenAsset: RTokenAsset let assetRegistry: IAssetRegistry let backingManager: TestIBackingManager let basketHandler: TestIBasketHandler + let rTokenTrader: TestIRevenueTrader let deployer: TestIDeployer - let facade: FacadeRead - let facadeTest: FacadeTest let facadeWrite: FacadeWrite let govParams: IGovParams let govRoles: IGovRoles const config = { dist: { - rTokenDist: bn(40), // 2/5 RToken - rsrDist: bn(60), // 3/5 RSR + rTokenDist: bn(100), // 100% RToken + rsrDist: bn(0), // 0% RSR }, - minTradeVolume: fp('1e4'), // $10k - rTokenMaxTradeVolume: fp('1e6'), // $1M + minTradeVolume: bn('0'), // $0 + rTokenMaxTradeVolume: MAX_UINT192, // +inf shortFreeze: bn('259200'), // 3 days longFreeze: bn('2592000'), // 30 days rewardRatio: bn('1069671574938'), // approx. half life of 90 days @@ -684,7 +664,9 @@ export default function fn( const integrationFixture: Fixture = async function (): Promise { return { - ctx: await loadFixture(makeCollateralFixtureContext(owner, {})), + ctx: await loadFixture( + makeCollateralFixtureContext(owner, { maxTradeVolume: MAX_UINT192 }) + ), protocol: await loadFixture(defaultFixture), } } @@ -702,32 +684,31 @@ export default function fn( let protocol: DefaultFixture ;({ ctx, protocol } = await loadFixture(integrationFixture)) ;({ collateral } = ctx) - ;({ rsr, rsrAsset, deployer, facade, facadeTest, facadeWrite, govParams } = protocol) + ;({ deployer, facadeWrite, govParams } = protocol) - amt = fp('1') + supply = fp('1') // Create a paired collateral of the same targetName - pairedColl = await makePairedCollateral(await ctx.collateral.targetName()) + pairedColl = await makePairedCollateral(await collateral.targetName()) await pairedColl.refresh() expect(await pairedColl.status()).to.equal(CollateralStatus.SOUND) pairedERC20 = await ethers.getContractAt('ERC20Mock', await pairedColl.erc20()) // Prep collateral - await mintCollateralTo(ctx, fp('1000'), addr1, addr1.address) collateralERC20 = await ethers.getContractAt('IERC20Metadata', await collateral.erc20()) + await mintCollateralTo( + ctx, + toBNDecimals(fp('1'), await collateralERC20.decimals()), + addr1, + addr1.address + ) // Set primary basket const rTokenSetup: IRTokenSetup = { assets: [], primaryBasket: [collateral.address, pairedColl.address], - weights: [fp('0.5'), fp('0.5')], - backups: [ - { - backupUnit: await collateral.targetName(), - diversityFactor: bn('1'), - backupCollateral: [pairedColl.address], - }, - ], + weights: [fp('0.5e-4'), fp('0.5e-4')], + backups: [], beneficiaries: [], } @@ -760,8 +741,8 @@ export default function fn( await ethers.getContractAt('TestIBasketHandler', await main.basketHandler()) ) rToken = await ethers.getContractAt('TestIRToken', await main.rToken()) - rTokenAsset = ( - await ethers.getContractAt('RTokenAsset', await assetRegistry.toAsset(rToken.address)) + rTokenTrader = ( + await ethers.getContractAt('TestIRevenueTrader', await main.rTokenTrader()) ) // Set initial governance roles @@ -785,24 +766,85 @@ export default function fn( await setNextBlockTimestamp( (await getLatestBlockTimestamp()) + (await basketHandler.warmupPeriod()) ) + + // Should issue + await collateralERC20.connect(addr1).approve(rToken.address, MAX_UINT256) + await pairedERC20.connect(addr1).approve(rToken.address, MAX_UINT256) + await rToken.connect(addr1).issue(supply) }) - it('does setup correctly', async () => { + it('can be put into an RToken basket', async () => { await assetRegistry.refresh() expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) }) - it('does issuance + redemption', async () => { - // Should issue - await collateralERC20.connect(addr1).approve(rToken.address, MAX_UINT256) - await pairedERC20.connect(addr1).approve(rToken.address, MAX_UINT256) - await rToken.connect(addr1).issue(amt) + it('issues', async () => { + // Issuance tested in beforeEach + }) + + it('redeems', async () => { + await rToken.connect(addr1).redeem(supply) + }) + + it('rebalances out of the collateral', async () => { + // Remove collateral from basket + await basketHandler.connect(owner).setPrimeBasket([pairedERC20.address], [fp('1e-4')]) + await expect(basketHandler.connect(owner).refreshBasket()) + .to.emit(basketHandler, 'BasketSet') + .withArgs(anyValue, [pairedERC20.address], [fp('1e-4')], false) + await setNextBlockTimestamp( + (await getLatestBlockTimestamp()) + config.warmupPeriod.toNumber() + ) - // Should redeem - await rToken.connect(addr1).redeem(amt) + // Run rebalancing auction + await expect(backingManager.rebalance(TradeKind.DUTCH_AUCTION)) + .to.emit(backingManager, 'TradeStarted') + .withArgs(anyValue, collateralERC20.address, pairedERC20.address, anyValue, anyValue) + const tradeAddr = await backingManager.trades(collateralERC20.address) + expect(tradeAddr).to.not.equal(ZERO_ADDRESS) + const trade = await ethers.getContractAt('DutchTrade', tradeAddr) + expect(await trade.sell()).to.equal(collateralERC20.address) + expect(await trade.buy()).to.equal(pairedERC20.address) + const buyAmt = await trade.bidAmount(await trade.endBlock()) + await pairedERC20.connect(addr1).approve(trade.address, buyAmt) + await advanceBlocks((await trade.endBlock()).sub(await getLatestBlockNumber()).sub(1)) + const pairedBal = await pairedERC20.balanceOf(backingManager.address) + await expect(trade.connect(addr1).bid()).to.emit(backingManager, 'TradeSettled') + expect(await pairedERC20.balanceOf(backingManager.address)).to.be.gt(pairedBal) + expect(await backingManager.tradesOpen()).to.equal(0) + }) + + it('forwards revenue and sells in a revenue auction', async () => { + // Send excess collateral to the RToken trader via forwardRevenue() + const mintAmt = toBNDecimals(fp('1e-6'), await collateralERC20.decimals()) + await mintCollateralTo( + ctx, + mintAmt.gt('150') ? mintAmt : bn('150'), + addr1, + backingManager.address + ) + await backingManager.forwardRevenue([collateralERC20.address]) + expect(await collateralERC20.balanceOf(rTokenTrader.address)).to.be.gt(0) + + // Run revenue auction + await expect( + rTokenTrader.manageTokens([collateralERC20.address], [TradeKind.DUTCH_AUCTION]) + ) + .to.emit(rTokenTrader, 'TradeStarted') + .withArgs(anyValue, collateralERC20.address, rToken.address, anyValue, anyValue) + const tradeAddr = await rTokenTrader.trades(collateralERC20.address) + expect(tradeAddr).to.not.equal(ZERO_ADDRESS) + const trade = await ethers.getContractAt('DutchTrade', tradeAddr) + expect(await trade.sell()).to.equal(collateralERC20.address) + expect(await trade.buy()).to.equal(rToken.address) + const buyAmt = await trade.bidAmount(await trade.endBlock()) + await rToken.connect(addr1).approve(trade.address, buyAmt) + await advanceBlocks((await trade.endBlock()).sub(await getLatestBlockNumber()).sub(1)) + await expect(trade.connect(addr1).bid()).to.emit(rTokenTrader, 'TradeSettled') + expect(await rTokenTrader.tradesOpen()).to.equal(0) }) - // === Integration Helpers === + // === Integration Test Helpers === const makePairedCollateral = async (target: string): Promise => { const MockV3AggregatorFactory: ContractFactory = await ethers.getContractFactory( @@ -831,9 +873,9 @@ export default function fn( chainlinkFeed: chainlinkFeed.address, oracleError: ORACLE_ERROR, erc20: erc20.address, - maxTradeVolume: config.rTokenMaxTradeVolume, + maxTradeVolume: MAX_UINT192, oracleTimeout: ORACLE_TIMEOUT, - target: ethers.utils.formatBytes32String('USD'), + targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01'), // 1% delayUntilDefault: bn('86400'), // 24h, }) @@ -856,7 +898,7 @@ export default function fn( chainlinkFeed: chainlinkFeed.address, oracleError: ORACLE_ERROR, erc20: erc20.address, - maxTradeVolume: config.rTokenMaxTradeVolume, + maxTradeVolume: MAX_UINT192, oracleTimeout: ORACLE_TIMEOUT, targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: fp('0'), // 0% @@ -885,7 +927,7 @@ export default function fn( chainlinkFeed: chainlinkFeed.address, oracleError: ORACLE_ERROR, erc20: erc20.address, - maxTradeVolume: config.rTokenMaxTradeVolume, + maxTradeVolume: MAX_UINT192, oracleTimeout: ORACLE_TIMEOUT, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: fp('0.01'), // 1% diff --git a/test/plugins/individual-collateral/curve/collateralTests.ts b/test/plugins/individual-collateral/curve/collateralTests.ts index 33285835f7..e6e521a91f 100644 --- a/test/plugins/individual-collateral/curve/collateralTests.ts +++ b/test/plugins/individual-collateral/curve/collateralTests.ts @@ -4,23 +4,57 @@ import { CurveCollateralTestSuiteFixtures, } from './pluginTestTypes' import { CollateralStatus } from '../pluginTestTypes' -import { ethers } from 'hardhat' -import { ERC20Mock, InvalidMockV3Aggregator } from '../../../../typechain' -import { BigNumber } from 'ethers' -import { bn, fp } from '../../../../common/numbers' -import { MAX_UINT48, MAX_UINT192, ZERO_ADDRESS, ONE_ADDRESS } from '../../../../common/constants' +import hre, { ethers } from 'hardhat' +import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { BigNumber, ContractFactory } from 'ethers' +import { getChainId } from '../../../../common/blockchain-utils' +import { bn, fp, toBNDecimals } from '../../../../common/numbers' +import { DefaultFixture, Fixture, getDefaultFixture, ORACLE_TIMEOUT } from '../fixtures' +import { expectInIndirectReceipt } from '../../../../common/events' +import { whileImpersonating } from '../../../utils/impersonation' +import { + MAX_UINT48, + MAX_UINT192, + MAX_UINT256, + TradeKind, + ZERO_ADDRESS, + ONE_ADDRESS, +} from '../../../../common/constants' import { expect } from 'chai' import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { useEnv } from '#/utils/env' import { expectDecayedPrice, expectExactPrice, expectUnpriced } from '../../../utils/oracles' +import { + IGovParams, + IGovRoles, + IRTokenSetup, + networkConfig, +} from '../../../../common/configuration' import { advanceBlocks, advanceTime, + getLatestBlockNumber, getLatestBlockTimestamp, setNextBlockTimestamp, } from '#/test/utils/time' +import { + ERC20Mock, + FacadeWrite, + IAssetRegistry, + IERC20Metadata, + InvalidMockV3Aggregator, + MockV3Aggregator, + TestIBackingManager, + TestIBasketHandler, + TestICollateral, + TestIDeployer, + TestIMain, + TestIRevenueTrader, + TestIRToken, +} from '../../../../typechain' import snapshotGasCost from '../../../utils/snapshotGasCost' -import { IMPLEMENTATION, Implementation } from '../../../fixtures' +import { IMPLEMENTATION, Implementation, ORACLE_ERROR, PRICE_TIMEOUT } from '../../../fixtures' const describeGas = IMPLEMENTATION == Implementation.P1 && useEnv('REPORT_GAS') ? describe.only : describe.skip @@ -716,5 +750,351 @@ export default function fn( }) }) }) + + describe('integration tests', () => { + before(resetFork) + + let ctx: X + let owner: SignerWithAddress + let addr1: SignerWithAddress + + let chainId: number + + let defaultFixture: Fixture + + let supply: BigNumber + + // Tokens/Assets + let pairedColl: TestICollateral + let pairedERC20: ERC20Mock + let collateralERC20: IERC20Metadata + let collateral: TestICollateral + + // Core Contracts + let main: TestIMain + let rToken: TestIRToken + let assetRegistry: IAssetRegistry + let backingManager: TestIBackingManager + let basketHandler: TestIBasketHandler + let rTokenTrader: TestIRevenueTrader + + let deployer: TestIDeployer + let facadeWrite: FacadeWrite + let govParams: IGovParams + let govRoles: IGovRoles + + const config = { + dist: { + rTokenDist: bn(100), // 100% RToken + rsrDist: bn(0), // 0% RSR + }, + minTradeVolume: bn('0'), // $0 + rTokenMaxTradeVolume: MAX_UINT192, // +inf + shortFreeze: bn('259200'), // 3 days + longFreeze: bn('2592000'), // 30 days + rewardRatio: bn('1069671574938'), // approx. half life of 90 days + unstakingDelay: bn('1209600'), // 2 weeks + withdrawalLeak: fp('0'), // 0%; always refresh + warmupPeriod: bn('60'), // (the delay _after_ SOUND was regained) + tradingDelay: bn('0'), // (the delay _after_ default has been confirmed) + batchAuctionLength: bn('900'), // 15 minutes + dutchAuctionLength: bn('1800'), // 30 minutes + backingBuffer: fp('0'), // 0% + maxTradeSlippage: fp('0.01'), // 1% + issuanceThrottle: { + amtRate: fp('1e6'), // 1M RToken + pctRate: fp('0.05'), // 5% + }, + redemptionThrottle: { + amtRate: fp('1e6'), // 1M RToken + pctRate: fp('0.05'), // 5% + }, + } + + interface IntegrationFixture { + ctx: X + protocol: DefaultFixture + } + + const integrationFixture: Fixture = + async function (): Promise { + return { + ctx: await loadFixture( + makeCollateralFixtureContext(owner, { maxTradeVolume: MAX_UINT192 }) + ), + protocol: await loadFixture(defaultFixture), + } + } + + before(async () => { + defaultFixture = await getDefaultFixture(collateralName) + chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + ;[, owner, addr1] = await ethers.getSigners() + }) + + beforeEach(async () => { + let protocol: DefaultFixture + ;({ ctx, protocol } = await loadFixture(integrationFixture)) + ;({ collateral } = ctx) + ;({ deployer, facadeWrite, govParams } = protocol) + + supply = fp('1') + + // Create a paired collateral of the same targetName + pairedColl = await makePairedCollateral(await collateral.targetName()) + await pairedColl.refresh() + expect(await pairedColl.status()).to.equal(CollateralStatus.SOUND) + pairedERC20 = await ethers.getContractAt('ERC20Mock', await pairedColl.erc20()) + + // Prep collateral + collateralERC20 = await ethers.getContractAt('IERC20Metadata', await collateral.erc20()) + await mintCollateralTo( + ctx, + toBNDecimals(fp('1'), await collateralERC20.decimals()), + addr1, + addr1.address + ) + + // Set primary basket + const rTokenSetup: IRTokenSetup = { + assets: [], + primaryBasket: [collateral.address, pairedColl.address], + weights: [fp('0.5e-4'), fp('0.5e-4')], + backups: [], + beneficiaries: [], + } + + // Deploy RToken via FacadeWrite + const receipt = await ( + await facadeWrite.connect(owner).deployRToken( + { + name: 'RTKN RToken', + symbol: 'RTKN', + mandate: 'mandate', + params: config, + }, + rTokenSetup + ) + ).wait() + + // Get Main + const mainAddr = expectInIndirectReceipt(receipt, deployer.interface, 'RTokenCreated').args + .main + main = await ethers.getContractAt('TestIMain', mainAddr) + + // Get core contracts + assetRegistry = ( + await ethers.getContractAt('IAssetRegistry', await main.assetRegistry()) + ) + backingManager = ( + await ethers.getContractAt('TestIBackingManager', await main.backingManager()) + ) + basketHandler = ( + await ethers.getContractAt('TestIBasketHandler', await main.basketHandler()) + ) + rToken = await ethers.getContractAt('TestIRToken', await main.rToken()) + rTokenTrader = ( + await ethers.getContractAt('TestIRevenueTrader', await main.rTokenTrader()) + ) + + // Set initial governance roles + govRoles = { + owner: owner.address, + guardian: ZERO_ADDRESS, + pausers: [], + shortFreezers: [], + longFreezers: [], + } + // Setup owner and unpause + await facadeWrite.connect(owner).setupGovernance( + rToken.address, + false, // do not deploy governance + true, // unpaused + govParams, // mock values, not relevant + govRoles + ) + + // Advance past warmup period + await setNextBlockTimestamp( + (await getLatestBlockTimestamp()) + (await basketHandler.warmupPeriod()) + ) + + // Should issue + await collateralERC20.connect(addr1).approve(rToken.address, MAX_UINT256) + await pairedERC20.connect(addr1).approve(rToken.address, MAX_UINT256) + await rToken.connect(addr1).issue(supply) + }) + + it('can be put into an RToken basket', async () => { + await assetRegistry.refresh() + expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) + }) + + it('issues', async () => { + // Issuance tested in beforeEach + }) + + it('redeems', async () => { + await rToken.connect(addr1).redeem(supply) + }) + + it('rebalances out of the collateral', async () => { + // Remove collateral from basket + await basketHandler.connect(owner).setPrimeBasket([pairedERC20.address], [fp('1e-4')]) + await expect(basketHandler.connect(owner).refreshBasket()) + .to.emit(basketHandler, 'BasketSet') + .withArgs(anyValue, [pairedERC20.address], [fp('1e-4')], false) + await setNextBlockTimestamp( + (await getLatestBlockTimestamp()) + config.warmupPeriod.toNumber() + ) + + // Run rebalancing auction + await expect(backingManager.rebalance(TradeKind.DUTCH_AUCTION)) + .to.emit(backingManager, 'TradeStarted') + .withArgs(anyValue, collateralERC20.address, pairedERC20.address, anyValue, anyValue) + const tradeAddr = await backingManager.trades(collateralERC20.address) + expect(tradeAddr).to.not.equal(ZERO_ADDRESS) + const trade = await ethers.getContractAt('DutchTrade', tradeAddr) + expect(await trade.sell()).to.equal(collateralERC20.address) + expect(await trade.buy()).to.equal(pairedERC20.address) + const buyAmt = await trade.bidAmount(await trade.endBlock()) + await pairedERC20.connect(addr1).approve(trade.address, buyAmt) + await advanceBlocks((await trade.endBlock()).sub(await getLatestBlockNumber()).sub(1)) + const pairedBal = await pairedERC20.balanceOf(backingManager.address) + await expect(trade.connect(addr1).bid()).to.emit(backingManager, 'TradeSettled') + expect(await pairedERC20.balanceOf(backingManager.address)).to.be.gt(pairedBal) + expect(await backingManager.tradesOpen()).to.equal(0) + }) + + it('forwards revenue and sells in a revenue auction', async () => { + // Send excess collateral to the RToken trader via forwardRevenue() + const mintAmt = toBNDecimals(fp('1e-6'), await collateralERC20.decimals()) + await mintCollateralTo( + ctx, + mintAmt.gt('150') ? mintAmt : bn('150'), + addr1, + backingManager.address + ) + await backingManager.forwardRevenue([collateralERC20.address]) + expect(await collateralERC20.balanceOf(rTokenTrader.address)).to.be.gt(0) + + // Run revenue auction + await expect( + rTokenTrader.manageTokens([collateralERC20.address], [TradeKind.DUTCH_AUCTION]) + ) + .to.emit(rTokenTrader, 'TradeStarted') + .withArgs(anyValue, collateralERC20.address, rToken.address, anyValue, anyValue) + const tradeAddr = await rTokenTrader.trades(collateralERC20.address) + expect(tradeAddr).to.not.equal(ZERO_ADDRESS) + const trade = await ethers.getContractAt('DutchTrade', tradeAddr) + expect(await trade.sell()).to.equal(collateralERC20.address) + expect(await trade.buy()).to.equal(rToken.address) + const buyAmt = await trade.bidAmount(await trade.endBlock()) + await rToken.connect(addr1).approve(trade.address, buyAmt) + await advanceBlocks((await trade.endBlock()).sub(await getLatestBlockNumber()).sub(1)) + await expect(trade.connect(addr1).bid()).to.emit(rTokenTrader, 'TradeSettled') + expect(await rTokenTrader.tradesOpen()).to.equal(0) + }) + + // === Integration Test Helpers === + + const makePairedCollateral = async (target: string): Promise => { + const MockV3AggregatorFactory: ContractFactory = await ethers.getContractFactory( + 'MockV3Aggregator' + ) + const chainlinkFeed: MockV3Aggregator = ( + await MockV3AggregatorFactory.deploy(8, bn('1e8')) + ) + + if (target == ethers.utils.formatBytes32String('USD')) { + // USD + const erc20 = await ethers.getContractAt( + 'IERC20Metadata', + networkConfig[chainId].tokens.USDC! + ) + await whileImpersonating('0x40ec5b33f54e0e8a33a975908c5ba1c14e5bbbdf', async (signer) => { + await erc20 + .connect(signer) + .transfer(addr1.address, await erc20.balanceOf(signer.address)) + }) + const FiatCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'FiatCollateral' + ) + return await FiatCollateralFactory.deploy({ + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20.address, + maxTradeVolume: MAX_UINT192, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01'), // 1% + delayUntilDefault: bn('86400'), // 24h, + }) + } else if (target == ethers.utils.formatBytes32String('ETH')) { + // ETH + const erc20 = await ethers.getContractAt( + 'IERC20Metadata', + networkConfig[chainId].tokens.WETH! + ) + await whileImpersonating('0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E', async (signer) => { + await erc20 + .connect(signer) + .transfer(addr1.address, await erc20.balanceOf(signer.address)) + }) + const SelfReferentialFactory: ContractFactory = await ethers.getContractFactory( + 'SelfReferentialCollateral' + ) + return await SelfReferentialFactory.deploy({ + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20.address, + maxTradeVolume: MAX_UINT192, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('ETH'), + defaultThreshold: fp('0'), // 0% + delayUntilDefault: bn('0'), // 0, + }) + } else if (target == ethers.utils.formatBytes32String('BTC')) { + // BTC + const targetUnitOracle: MockV3Aggregator = ( + await MockV3AggregatorFactory.deploy(8, bn('1e8')) + ) + const erc20 = await ethers.getContractAt( + 'IERC20Metadata', + networkConfig[chainId].tokens.WBTC! + ) + await whileImpersonating('0xccf4429db6322d5c611ee964527d42e5d685dd6a', async (signer) => { + await erc20 + .connect(signer) + .transfer(addr1.address, await erc20.balanceOf(signer.address)) + }) + const NonFiatFactory: ContractFactory = await ethers.getContractFactory( + 'NonFiatCollateral' + ) + return await NonFiatFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20.address, + maxTradeVolume: MAX_UINT192, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('BTC'), + defaultThreshold: fp('0.01'), // 1% + delayUntilDefault: bn('86400'), // 24h, + }, + targetUnitOracle.address, + ORACLE_TIMEOUT + ) + } else { + throw new Error(`Unknown target: ${target}`) + } + } + }) }) } From d997a11fd42d680d4ec34b0f0e949aab0379b681 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Mon, 23 Oct 2023 14:11:53 -0700 Subject: [PATCH 5/8] try to fix failing morpho test --- .../morpho-aave/MorphoAaveV2TokenisedDeposit.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAaveV2TokenisedDeposit.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAaveV2TokenisedDeposit.test.ts index 20d9a1406a..90942c427b 100644 --- a/test/plugins/individual-collateral/morpho-aave/MorphoAaveV2TokenisedDeposit.test.ts +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAaveV2TokenisedDeposit.test.ts @@ -179,7 +179,6 @@ const execTestForToken = ({ type ITestContext = ReturnType extends Promise ? U : never let context: ITestContext - // const resetFork = getResetFork(17591000) beforeEach(async () => { context = await loadFixture(beforeEachFn) }) @@ -217,14 +216,15 @@ const execTestForToken = ({ await methods.deposit(bob, fraction(20)) await closeTo(methods.balanceUnderlying(alice), fraction(90)) - const aliceShares = await methods.shares(alice) + let aliceShares = await methods.shares(alice) await closeTo(Promise.resolve(aliceShares), fraction(10)) await closeTo(methods.assets(alice), fraction(10)) await methods.withdraw(alice, (parseFloat(aliceShares) / 2).toString()) await closeTo(methods.shares(alice), fraction(5)) await closeTo(methods.assets(alice), fraction(5)) await closeTo(methods.balanceUnderlying(alice), fraction(95)) - await methods.withdraw(alice, (parseFloat(aliceShares) / 2).toString()) + aliceShares = await methods.shares(alice) // remaining half + await methods.withdraw(alice, parseFloat(aliceShares).toString()) await closeTo(methods.shares(alice), fraction(0)) await closeTo(methods.assets(alice), fraction(0)) await closeTo(methods.balanceUnderlying(alice), fraction(100)) From e1429abad913f69b1cb34a56a78681aad8f33e7e Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Mon, 23 Oct 2023 16:00:42 -0700 Subject: [PATCH 6/8] reset fork at start of morpho wrapper tests --- .../morpho-aave/MorphoAaveV2TokenisedDeposit.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAaveV2TokenisedDeposit.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAaveV2TokenisedDeposit.test.ts index 90942c427b..e40322631c 100644 --- a/test/plugins/individual-collateral/morpho-aave/MorphoAaveV2TokenisedDeposit.test.ts +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAaveV2TokenisedDeposit.test.ts @@ -7,6 +7,8 @@ import { formatUnits, parseUnits } from 'ethers/lib/utils' import { expect } from 'chai' import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { bn } from '#/common/numbers' +import { getResetFork } from '../helpers' +import { FORK_BLOCK } from './constants' type ITokenSymbol = keyof ITokens const networkConfigToUse = networkConfig[31337] @@ -179,6 +181,8 @@ const execTestForToken = ({ type ITestContext = ReturnType extends Promise ? U : never let context: ITestContext + before(getResetFork(FORK_BLOCK)) + beforeEach(async () => { context = await loadFixture(beforeEachFn) }) @@ -216,15 +220,14 @@ const execTestForToken = ({ await methods.deposit(bob, fraction(20)) await closeTo(methods.balanceUnderlying(alice), fraction(90)) - let aliceShares = await methods.shares(alice) + const aliceShares = await methods.shares(alice) await closeTo(Promise.resolve(aliceShares), fraction(10)) await closeTo(methods.assets(alice), fraction(10)) await methods.withdraw(alice, (parseFloat(aliceShares) / 2).toString()) await closeTo(methods.shares(alice), fraction(5)) await closeTo(methods.assets(alice), fraction(5)) await closeTo(methods.balanceUnderlying(alice), fraction(95)) - aliceShares = await methods.shares(alice) // remaining half - await methods.withdraw(alice, parseFloat(aliceShares).toString()) + await methods.withdraw(alice, (parseFloat(aliceShares) / 2).toString()) await closeTo(methods.shares(alice), fraction(0)) await closeTo(methods.assets(alice), fraction(0)) await closeTo(methods.balanceUnderlying(alice), fraction(100)) From 8b6e164d1785209b6d09e25c665ab0145e3af0d5 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Mon, 23 Oct 2023 17:49:29 -0700 Subject: [PATCH 7/8] make plugin protocol integration tests work for base --- .../individual-collateral/collateralTests.ts | 16 +++++++++++++--- .../curve/collateralTests.ts | 8 +++++++- test/plugins/individual-collateral/fixtures.ts | 9 +++++---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index da7f894688..540366bf7d 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -688,6 +688,7 @@ export default function fn( before(async () => { defaultFixture = await getDefaultFixture(collateralName) chainId = await getChainId(hre) + if (useEnv('FORK_NETWORK').toLowerCase() === 'base') chainId = 8453 if (!networkConfig[chainId]) { throw new Error(`Missing network configuration for ${hre.network.name}`) } @@ -861,6 +862,7 @@ export default function fn( // === Integration Test Helpers === const makePairedCollateral = async (target: string): Promise => { + const onBase = useEnv('FORK_NETWORK').toLowerCase() == 'base' const MockV3AggregatorFactory: ContractFactory = await ethers.getContractFactory( 'MockV3Aggregator' ) @@ -872,9 +874,12 @@ export default function fn( // USD const erc20 = await ethers.getContractAt( 'IERC20Metadata', - networkConfig[chainId].tokens.USDC! + onBase ? networkConfig[chainId].tokens.USDbC! : networkConfig[chainId].tokens.USDC! ) - await whileImpersonating('0x40ec5b33f54e0e8a33a975908c5ba1c14e5bbbdf', async (signer) => { + const whale = onBase + ? '0xb4885bc63399bf5518b994c1d0c153334ee579d0' + : '0x40ec5b33f54e0e8a33a975908c5ba1c14e5bbbdf' + await whileImpersonating(whale, async (signer) => { await erc20 .connect(signer) .transfer(addr1.address, await erc20.balanceOf(signer.address)) @@ -899,7 +904,10 @@ export default function fn( 'IERC20Metadata', networkConfig[chainId].tokens.WETH! ) - await whileImpersonating('0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E', async (signer) => { + const whale = onBase + ? '0xb4885bc63399bf5518b994c1d0c153334ee579d0' + : '0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E' + await whileImpersonating(whale, async (signer) => { await erc20 .connect(signer) .transfer(addr1.address, await erc20.balanceOf(signer.address)) @@ -919,6 +927,8 @@ export default function fn( delayUntilDefault: bn('0'), // 0, }) } else if (target == ethers.utils.formatBytes32String('BTC')) { + // No official WBTC on base yet + if (onBase) throw new Error('no WBTC on base') // BTC const targetUnitOracle: MockV3Aggregator = ( await MockV3AggregatorFactory.deploy(8, bn('1e8')) diff --git a/test/plugins/individual-collateral/curve/collateralTests.ts b/test/plugins/individual-collateral/curve/collateralTests.ts index d4636bccc2..cc607eaab3 100644 --- a/test/plugins/individual-collateral/curve/collateralTests.ts +++ b/test/plugins/individual-collateral/curve/collateralTests.ts @@ -61,6 +61,10 @@ const describeGas = const describeFork = useEnv('FORK') ? describe : describe.skip +const getDescribeFork = (targetNetwork = 'mainnet') => { + return useEnv('FORK') && useEnv('FORK_NETWORK') === targetNetwork ? describe : describe.skip +} + export default function fn( fixtures: CurveCollateralTestSuiteFixtures ) { @@ -763,7 +767,9 @@ export default function fn( }) }) - describe('integration tests', () => { + // Only run full protocol integration tests on mainnet + // Protocol integration fixture not currently set up to deploy onto base + getDescribeFork('mainnet')('integration tests', () => { before(resetFork) let ctx: X diff --git a/test/plugins/individual-collateral/fixtures.ts b/test/plugins/individual-collateral/fixtures.ts index b1b5e9e1fa..bd51d9bd4f 100644 --- a/test/plugins/individual-collateral/fixtures.ts +++ b/test/plugins/individual-collateral/fixtures.ts @@ -3,6 +3,7 @@ import hre, { ethers } from 'hardhat' import { getChainId } from '../../../common/blockchain-utils' import { IImplementations, IGovParams, networkConfig } from '../../../common/configuration' import { bn, fp } from '../../../common/numbers' +import { useEnv } from '#/utils/env' import { Implementation, IMPLEMENTATION, ORACLE_ERROR, PRICE_TIMEOUT } from '../../fixtures' import { Asset, @@ -39,8 +40,7 @@ interface RSRFixture { rsr: ERC20Mock } -async function rsrFixture(): Promise { - const chainId = await getChainId(hre) +async function rsrFixture(chainId: number): Promise { const rsr: ERC20Mock = ( await ethers.getContractAt('ERC20Mock', networkConfig[chainId].tokens.RSR || '') ) @@ -72,9 +72,10 @@ export interface DefaultFixture extends RSRAndModuleFixture { export const getDefaultFixture = async function (salt: string) { const defaultFixture: Fixture = async function (): Promise { - const { rsr } = await rsrFixture() + let chainId = await getChainId(hre) + if (useEnv('FORK_NETWORK').toLowerCase() == 'base') chainId = 8453 + const { rsr } = await rsrFixture(chainId) const { gnosis } = await gnosisFixture() - const chainId = await getChainId(hre) if (!networkConfig[chainId]) { throw new Error(`Missing network configuration for ${hre.network.name}`) } From d2281d7f2c20dc89a5a0cdab1225fd10aa42bc01 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Tue, 31 Oct 2023 11:28:15 -0400 Subject: [PATCH 8/8] add amount checks post-issue/redeem --- test/plugins/individual-collateral/collateralTests.ts | 9 ++++++++- .../individual-collateral/curve/collateralTests.ts | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index 540366bf7d..bf5336a5d0 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -794,11 +794,18 @@ export default function fn( }) it('issues', async () => { - // Issuance tested in beforeEach + // Issuance in beforeEach + expect(await rToken.totalSupply()).to.equal(supply) }) it('redeems', async () => { await rToken.connect(addr1).redeem(supply) + expect(await rToken.totalSupply()).to.equal(0) + const initialCollBal = toBNDecimals(fp('1'), await collateralERC20.decimals()) + expect(await collateralERC20.balanceOf(addr1.address)).to.be.closeTo( + initialCollBal, + initialCollBal.div(bn('1e5')) // 1-part-in-100k + ) }) it('rebalances out of the collateral', async () => { diff --git a/test/plugins/individual-collateral/curve/collateralTests.ts b/test/plugins/individual-collateral/curve/collateralTests.ts index cc607eaab3..99f0b9e463 100644 --- a/test/plugins/individual-collateral/curve/collateralTests.ts +++ b/test/plugins/individual-collateral/curve/collateralTests.ts @@ -952,11 +952,18 @@ export default function fn( }) it('issues', async () => { - // Issuance tested in beforeEach + // Issuance in beforeEach + expect(await rToken.totalSupply()).to.equal(supply) }) it('redeems', async () => { await rToken.connect(addr1).redeem(supply) + expect(await rToken.totalSupply()).to.equal(0) + const initialCollBal = toBNDecimals(fp('1'), await collateralERC20.decimals()) + expect(await collateralERC20.balanceOf(addr1.address)).to.be.closeTo( + initialCollBal, + initialCollBal.div(bn('1e5')) // 1-part-in-100k + ) }) it('rebalances out of the collateral', async () => {