From 8842f3ab95c12f9cfd5cb74b7e113f00c2ca7eed Mon Sep 17 00:00:00 2001 From: Julian R Date: Fri, 6 Jun 2025 10:22:54 -0300 Subject: [PATCH 1/2] implement optimization --- .../CurveAppreciatingRTokenFiatCollateral.sol | 7 ++-- .../CurveStableRTokenMetapoolCollateral.sol | 7 ++-- .../CrvStableRTokenMetapoolTestSuite.test.ts | 6 +-- ...xAppreciatingRTokenSelfReferential.test.ts | 38 +++++++++++++++---- .../CvxStableRTokenMetapoolTestSuite.test.ts | 37 +++++++++++++++--- 5 files changed, 71 insertions(+), 24 deletions(-) diff --git a/contracts/plugins/assets/curve/CurveAppreciatingRTokenFiatCollateral.sol b/contracts/plugins/assets/curve/CurveAppreciatingRTokenFiatCollateral.sol index 887371ee0c..7dd774ccbb 100644 --- a/contracts/plugins/assets/curve/CurveAppreciatingRTokenFiatCollateral.sol +++ b/contracts/plugins/assets/curve/CurveAppreciatingRTokenFiatCollateral.sol @@ -47,10 +47,9 @@ contract CurveAppreciatingRTokenFiatCollateral is CurveStableCollateral { /// Refresh exchange rates and update default status. /// Have to override to add custom default checks function refresh() public virtual override { - // solhint-disable-next-line no-empty-blocks - try pairedAssetRegistry.refresh() {} catch { - // must allow failure since cannot brick refresh() - } + // Note: Intentionally not refreshing AssetRegistry as an acceptable trade-off + // Issuance throttle becomes worst-case outcome in case of issuance frontruns in + // response to picking up bad collateral CollateralStatus oldStatus = status(); diff --git a/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol b/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol index 63c5024b52..a4be74d1b0 100644 --- a/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol +++ b/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol @@ -49,10 +49,9 @@ contract CurveStableRTokenMetapoolCollateral is CurveStableMetapoolCollateral { /// Refresh exchange rates and update default status. /// Have to override to add custom default checks function refresh() public virtual override { - // solhint-disable-next-line no-empty-blocks - try pairedAssetRegistry.refresh() {} catch { - // must allow failure since cannot brick refresh() - } + // Note: Intentionally not refreshing AssetRegistry as an acceptable trade-off + // Issuance throttle becomes worst-case outcome in case of issuance frontruns in + // response to picking up bad collateral CollateralStatus oldStatus = status(); diff --git a/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts b/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts index 79a7f7b030..820192dd9d 100644 --- a/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts +++ b/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts @@ -245,7 +245,7 @@ const collateralSpecificStatusTests = () => { await collateral.refresh() }) - it('Regression test -- refreshes inner RTokenAsset on refresh()', async () => { + it('Regression test -- does not refresh inner RTokenAsset on refresh()', async () => { const [collateral] = await deployCollateral({}) const initialPrice = await collateral.price() expect(initialPrice[0]).to.be.gt(0) @@ -276,8 +276,8 @@ const collateralSpecificStatusTests = () => { // Refresh CurveStableRTokenMetapoolCollateral await collateral.refresh() - // Stale should be false again - expect(await mockRTokenAsset.stale()).to.be.false + // Stale remains true + expect(await mockRTokenAsset.stale()).to.be.true }) } diff --git a/test/plugins/individual-collateral/curve/cvx/CvxAppreciatingRTokenSelfReferential.test.ts b/test/plugins/individual-collateral/curve/cvx/CvxAppreciatingRTokenSelfReferential.test.ts index 809da90715..ec37c0c35b 100644 --- a/test/plugins/individual-collateral/curve/cvx/CvxAppreciatingRTokenSelfReferential.test.ts +++ b/test/plugins/individual-collateral/curve/cvx/CvxAppreciatingRTokenSelfReferential.test.ts @@ -227,7 +227,7 @@ const collateralSpecificStatusTests = () => { await collateral.refresh() }) - it('Regression test -- refreshes inner RTokenAsset on refresh()', async () => { + it('Regression test -- does not refresh inner RTokenAsset on refresh()', async () => { const [collateral] = await deployCollateral({}) const initialPrice = await collateral.price() expect(initialPrice[0]).to.be.gt(0) @@ -258,11 +258,11 @@ const collateralSpecificStatusTests = () => { // Refresh CurveAppreciatingRTokenSelfReferentialCollateral await collateral.refresh() - // Stale should be false again - expect(await mockRTokenAsset.stale()).to.be.false + // Stale remains true + expect(await mockRTokenAsset.stale()).to.be.true }) - it('Regression test -- stays IFFY throughout inner RToken default + rebalancing', async () => { + it.only('Regression test -- stays IFFY throughout inner RToken default + rebalancing', async () => { const [collateral, opts] = await deployCollateral({}) const ethplusAssetRegistry = await ethers.getContractAt( 'IAssetRegistry', @@ -297,9 +297,28 @@ const collateralSpecificStatusTests = () => { const uoaPerRefOracle = await overrideOracle(uoaPerRefFeed) await uoaPerRefOracle.updateAnswer(await uoaPerRefOracle.latestAnswer()) - // wstETHCollateral + CurveAppreciatingRTokenSelfReferentialCollateral should - // become IFFY through the top-level refresh + // wstETHCollateral + CurveAppreciatingRTokenSelfReferentialCollateral will NOT + // become IFFY through the top-level refresh (optimization) await expectEvents(collateral.refresh(), [ + { + contract: ethplusBasketHandler, + name: 'BasketStatusChanged', + emitted: false, + }, + { + contract: wstETHCollateral, + name: 'CollateralStatusChanged', + emitted: false, + }, + { + contract: collateral, + name: 'CollateralStatusChanged', + emitted: false, + }, + ]) + + // Refresh inner RToken to set IFFY + await expectEvents(ethplusAssetRegistry.refresh(), [ { contract: ethplusBasketHandler, name: 'BasketStatusChanged', @@ -312,6 +331,11 @@ const collateralSpecificStatusTests = () => { args: [0, 1], emitted: true, }, + ]) + + // Now we can refresh CurveAppreciatingRTokenSelfReferentialCollateral + // and it should become IFFY + await expectEvents(collateral.refresh(), [ { contract: collateral, name: 'CollateralStatusChanged', @@ -319,6 +343,7 @@ const collateralSpecificStatusTests = () => { emitted: true, }, ]) + expect(await wstETHCollateral.status()).to.equal(1) expect(await collateral.status()).to.equal(1) expect(await ethplusBasketHandler.status()).to.equal(1) @@ -380,7 +405,6 @@ const collateralSpecificStatusTests = () => { // refPerTok should finally fall after a 50% haircut const basketsNeeded = await ethplus.basketsNeeded() await whileImpersonating(ETHPLUS_BACKING_MANAGER, async (bm) => { - console.log('whale', whaleBal, basketsNeeded, await weth.balanceOf(bm.address)) await weth.connect(bm).transfer(whale, whaleBal.sub(basketsNeeded.mul(26).div(100))) // leave >25% WETH backing expect(await ethplusBasketHandler.fullyCollateralized()).to.equal(false) await ethplus.connect(bm).setBasketsNeeded(basketsNeeded.div(2)) // 50% haircut = WETH backing is sufficient diff --git a/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts b/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts index 0e48f9758c..1eb438921d 100644 --- a/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts +++ b/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts @@ -254,7 +254,7 @@ const collateralSpecificStatusTests = () => { await collateral.refresh() }) - it('Regression test -- refreshes inner RTokenAsset on refresh()', async () => { + it('Regression test -- does not refresh inner RTokenAsset on refresh()', async () => { const [collateral] = await deployCollateral({}) const initialPrice = await collateral.price() expect(initialPrice[0]).to.be.gt(0) @@ -285,11 +285,11 @@ const collateralSpecificStatusTests = () => { // Refresh CurveStableRTokenMetapoolCollateral await collateral.refresh() - // Stale should be false again - expect(await mockRTokenAsset.stale()).to.be.false + // Stale remains true + expect(await mockRTokenAsset.stale()).to.be.true }) - it('Regression test -- stays IFFY throughout inner RToken default + rebalancing', async () => { + it.only('Regression test -- stays IFFY throughout inner RToken default + rebalancing', async () => { const [collateral, opts] = await deployCollateral({}) const eusdAssetRegistry = await ethers.getContractAt('IAssetRegistry', EUSD_ASSET_REGISTRY) const eusdBasketHandler = await ethers.getContractAt('TestIBasketHandler', EUSD_BASKET_HANDLER) @@ -308,9 +308,28 @@ const collateralSpecificStatusTests = () => { const latestAnswer = await oracle.latestAnswer() await oracle.updateAnswer(latestAnswer.mul(4).div(5)) - // CTokenFiatCollateral + CurveStableRTokenMetapoolCollateral should - // become IFFY through the top-level refresh + // CTokenFiatCollateral + CurveStableRTokenMetapoolCollateral will NOT + // become IFFY through the top-level refresh (optimization) await expectEvents(collateral.refresh(), [ + { + contract: eusdBasketHandler, + name: 'BasketStatusChanged', + emitted: false, + }, + { + contract: cUSDTCollateral, + name: 'CollateralStatusChanged', + emitted: false, + }, + { + contract: collateral, + name: 'CollateralStatusChanged', + emitted: false, + }, + ]) + + // Refresh inner RToken to set IFFY + await expectEvents(eusdAssetRegistry.refresh(), [ { contract: eusdBasketHandler, name: 'BasketStatusChanged', @@ -323,6 +342,11 @@ const collateralSpecificStatusTests = () => { args: [0, 1], emitted: true, }, + ]) + + // Now we can refresh CurveStableRTokenMetapoolCollateral + // and it should become IFFY + await expectEvents(collateral.refresh(), [ { contract: collateral, name: 'CollateralStatusChanged', @@ -330,6 +354,7 @@ const collateralSpecificStatusTests = () => { emitted: true, }, ]) + expect(await cUSDTCollateral.status()).to.equal(1) expect(await collateral.status()).to.equal(1) expect(await eusdBasketHandler.status()).to.equal(1) From 82db17f31882cad408c0c1fc30749847c1d51dd7 Mon Sep 17 00:00:00 2001 From: Julian R Date: Fri, 6 Jun 2025 10:24:39 -0300 Subject: [PATCH 2/2] remove only --- .../curve/cvx/CvxAppreciatingRTokenSelfReferential.test.ts | 2 +- .../curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/plugins/individual-collateral/curve/cvx/CvxAppreciatingRTokenSelfReferential.test.ts b/test/plugins/individual-collateral/curve/cvx/CvxAppreciatingRTokenSelfReferential.test.ts index ec37c0c35b..376825bc97 100644 --- a/test/plugins/individual-collateral/curve/cvx/CvxAppreciatingRTokenSelfReferential.test.ts +++ b/test/plugins/individual-collateral/curve/cvx/CvxAppreciatingRTokenSelfReferential.test.ts @@ -262,7 +262,7 @@ const collateralSpecificStatusTests = () => { expect(await mockRTokenAsset.stale()).to.be.true }) - it.only('Regression test -- stays IFFY throughout inner RToken default + rebalancing', async () => { + it('Regression test -- stays IFFY throughout inner RToken default + rebalancing', async () => { const [collateral, opts] = await deployCollateral({}) const ethplusAssetRegistry = await ethers.getContractAt( 'IAssetRegistry', diff --git a/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts b/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts index 1eb438921d..ae8cc63c67 100644 --- a/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts +++ b/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts @@ -289,7 +289,7 @@ const collateralSpecificStatusTests = () => { expect(await mockRTokenAsset.stale()).to.be.true }) - it.only('Regression test -- stays IFFY throughout inner RToken default + rebalancing', async () => { + it('Regression test -- stays IFFY throughout inner RToken default + rebalancing', async () => { const [collateral, opts] = await deployCollateral({}) const eusdAssetRegistry = await ethers.getContractAt('IAssetRegistry', EUSD_ASSET_REGISTRY) const eusdBasketHandler = await ethers.getContractAt('TestIBasketHandler', EUSD_BASKET_HANDLER)