From 630143dfab6e21a607b7b23fbd9c36abe4352859 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Wed, 2 Sep 2026 19:00:02 +0000 Subject: [PATCH 1/7] Delay automatic rebalancing after defaults --- CHANGELOG.md | 5 ++ contracts/interfaces/IBasketHandler.sol | 3 ++ contracts/interfaces/IDeployer.sol | 2 +- contracts/mixins/Versioned.sol | 2 +- contracts/p0/BackingManager.sol | 22 ++++---- contracts/p0/BasketHandler.sol | 10 +++- contracts/p1/BackingManager.sol | 10 ++-- contracts/p1/BasketHandler.sol | 10 +++- contracts/plugins/assets/VersionedAsset.sol | 2 +- test/RToken.test.ts | 57 +++++++++++++++++++++ test/Recollateralization.test.ts | 43 ++++++++++++---- test/Revenues.test.ts | 45 +++++++++++----- test/fixtures.ts | 2 +- 13 files changed, 170 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c870e8289..ba1c3a782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +# 4.3.0 + +- Delay automatic rebalancing after a collateral default while allowing issuance against the backup basket. +- Allow governance-triggered basket changes to rebalance without waiting for `tradingDelay`. + # 4.2.0 Bump solidity version to 0.8.28 diff --git a/contracts/interfaces/IBasketHandler.sol b/contracts/interfaces/IBasketHandler.sol index d5f21969f..261a3034d 100644 --- a/contracts/interfaces/IBasketHandler.sol +++ b/contracts/interfaces/IBasketHandler.sol @@ -116,6 +116,9 @@ interface IBasketHandler is IComponent { /// @return If the basket is ready to issue and trade function isReady() external view returns (bool); + /// @return Whether the BackingManager's trading delay is bypassed for this basket + function tradingDelayBypassed() external view returns (bool); + /// Returns basket quantity rounded up, wihout any issuance premium /// @param erc20 The ERC20 token contract for the asset /// @return {tok/BU} The redemption quantity of token in the reference basket, rounded up diff --git a/contracts/interfaces/IDeployer.sol b/contracts/interfaces/IDeployer.sol index ede062536..38716c11d 100644 --- a/contracts/interfaces/IDeployer.sol +++ b/contracts/interfaces/IDeployer.sol @@ -47,7 +47,7 @@ struct DeploymentParams { bool enableIssuancePremium; // whether to enable the issuance premium // // === BackingManager === - uint48 tradingDelay; // {s} how long to wait until starting auctions after switching basket + uint48 tradingDelay; // {s} delay before automatic trading after a default-triggered basket switch uint48 batchAuctionLength; // {s} the length of a Gnosis EasyAuction uint48 dutchAuctionLength; // {s} the length of a falling-price dutch auction uint192 backingBuffer; // {1} how much extra backing collateral to keep diff --git a/contracts/mixins/Versioned.sol b/contracts/mixins/Versioned.sol index 94d275cb8..511ef4ac4 100644 --- a/contracts/mixins/Versioned.sol +++ b/contracts/mixins/Versioned.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.28; import "../interfaces/IVersioned.sol"; // This value should be updated on each release -string constant VERSION = "4.2.0"; +string constant VERSION = "4.3.0"; /** * @title Versioned diff --git a/contracts/p0/BackingManager.sol b/contracts/p0/BackingManager.sol index 2efc92896..f0e10d484 100644 --- a/contracts/p0/BackingManager.sol +++ b/contracts/p0/BackingManager.sol @@ -24,7 +24,7 @@ contract BackingManagerP0 is TradingP0, IBackingManager { uint48 public constant MAX_TRADING_DELAY = 60 * 60 * 24 * 365; // {s} 1 year uint192 public constant MAX_BACKING_BUFFER = 1e18; // {%} - uint48 public tradingDelay; // {s} how long to wait until resuming trading after switching + uint48 public tradingDelay; // {s} delay before automatic trading after a default-triggered basket switch uint192 public backingBuffer; // {%} how much extra backing collateral to keep mapping(TradeKind => uint48) private tradeEnd; // {s} last endTime() of an auction per kind @@ -94,10 +94,12 @@ contract BackingManagerP0 is TradingP0, IBackingManager { require(tradesOpen == 0, "trade open"); require(main.basketHandler().isReady(), "basket not ready"); - require( - block.timestamp >= main.basketHandler().timestamp() + tradingDelay, - "trading delayed" - ); + if (!main.basketHandler().tradingDelayBypassed()) { + require( + block.timestamp >= main.basketHandler().timestamp() + tradingDelay, + "trading delayed" + ); + } require(!main.basketHandler().fullyCollateralized(), "already collateralized"); // First dissolve any held RToken balance @@ -151,10 +153,12 @@ contract BackingManagerP0 is TradingP0, IBackingManager { require(tradesOpen == 0, "trade open"); require(main.basketHandler().isReady(), "basket not ready"); - require( - block.timestamp >= main.basketHandler().timestamp() + tradingDelay, - "trading delayed" - ); + if (!main.basketHandler().tradingDelayBypassed()) { + require( + block.timestamp >= main.basketHandler().timestamp() + tradingDelay, + "trading delayed" + ); + } require(main.basketHandler().fullyCollateralized(), "undercollateralized"); BasketRange memory basketsHeld = main.basketHandler().basketsHeldBy(address(this)); diff --git a/contracts/p0/BasketHandler.sol b/contracts/p0/BasketHandler.sol index aca8db918..bf6aa78b3 100644 --- a/contracts/p0/BasketHandler.sol +++ b/contracts/p0/BasketHandler.sol @@ -156,6 +156,11 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { bool public enableIssuancePremium; + // === 4.3.0 === + + /// Whether the trading delay is bypassed for the current basket + bool public tradingDelayBypassed; + // ==== Invariants ==== // basket is a valid Basket: // basket.erc20s is a valid collateral array and basket.erc20s == keys(basket.refAmts) @@ -214,11 +219,12 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { function refreshBasket() external { main.assetRegistry().refresh(); + bool isOwner = main.hasRole(OWNER, _msgSender()); require( - main.hasRole(OWNER, _msgSender()) || - (lastStatus == CollateralStatus.DISABLED && !main.tradingPausedOrFrozen()), + isOwner || (lastStatus == CollateralStatus.DISABLED && !main.tradingPausedOrFrozen()), "basket unrefreshable" ); + tradingDelayBypassed = isOwner || disabled; _switchBasket(); trackStatus(); diff --git a/contracts/p1/BackingManager.sol b/contracts/p1/BackingManager.sol index 1bdca04b0..220ce691f 100644 --- a/contracts/p1/BackingManager.sol +++ b/contracts/p1/BackingManager.sol @@ -33,7 +33,7 @@ contract BackingManagerP1 is TradingP1, IBackingManager { uint48 public constant MAX_TRADING_DELAY = 60 * 60 * 24 * 365; // {s} 1 year uint192 public constant MAX_BACKING_BUFFER = FIX_ONE; // {1} 100% - uint48 public tradingDelay; // {s} how long to wait until resuming trading after switching + uint48 public tradingDelay; // {s} delay before automatic trading after a default-triggered basket switch uint192 public backingBuffer; // {1} how much extra backing collateral to keep // === 3.0.0 === @@ -120,7 +120,9 @@ contract BackingManagerP1 is TradingP1, IBackingManager { require(tradesOpen == 0, "trade open"); require(basketHandler.isReady(), "basket not ready"); - require(block.timestamp >= basketHandler.timestamp() + tradingDelay, "trading delayed"); + if (!basketHandler.tradingDelayBypassed()) { + require(block.timestamp >= basketHandler.timestamp() + tradingDelay, "trading delayed"); + } BasketRange memory basketsHeld = basketHandler.basketsHeldBy(address(this)); require(basketsHeld.bottom < rToken.basketsNeeded(), "already collateralized"); @@ -185,7 +187,9 @@ contract BackingManagerP1 is TradingP1, IBackingManager { require(tradesOpen == 0, "trade open"); require(basketHandler.isReady(), "basket not ready"); - require(block.timestamp >= basketHandler.timestamp() + tradingDelay, "trading delayed"); + if (!basketHandler.tradingDelayBypassed()) { + require(block.timestamp >= basketHandler.timestamp() + tradingDelay, "trading delayed"); + } require(basketsHeld.bottom >= rToken.basketsNeeded(), "undercollateralized"); // require(basketHandler.fullyCollateralized()) diff --git a/contracts/p1/BasketHandler.sol b/contracts/p1/BasketHandler.sol index 97fab9835..f4c3020fb 100644 --- a/contracts/p1/BasketHandler.sol +++ b/contracts/p1/BasketHandler.sol @@ -96,6 +96,11 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { bool public enableIssuancePremium; + // === 4.3.0 === + + /// Whether the trading delay is bypassed for the current basket + bool public tradingDelayBypassed; + // ==== Invariants ==== // basket is a valid Basket: // basket.erc20s is a valid collateral array and basket.erc20s == keys(basket.refAmts) @@ -159,11 +164,12 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { function refreshBasket() external { assetRegistry.refresh(); + bool isOwner = main.hasRole(OWNER, _msgSender()); require( - main.hasRole(OWNER, _msgSender()) || - (lastStatus == CollateralStatus.DISABLED && !main.tradingPausedOrFrozen()), + isOwner || (lastStatus == CollateralStatus.DISABLED && !main.tradingPausedOrFrozen()), "basket unrefreshable" ); + tradingDelayBypassed = isOwner || disabled; _switchBasket(); trackStatus(); diff --git a/contracts/plugins/assets/VersionedAsset.sol b/contracts/plugins/assets/VersionedAsset.sol index 7cde39f95..b9189f46a 100644 --- a/contracts/plugins/assets/VersionedAsset.sol +++ b/contracts/plugins/assets/VersionedAsset.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.28; import "../../interfaces/IVersioned.sol"; // This value should be updated on each release -string constant ASSET_VERSION = "4.2.0"; +string constant ASSET_VERSION = "4.3.0"; /** * @title VersionedAsset diff --git a/test/RToken.test.ts b/test/RToken.test.ts index a48cb9019..cb958db48 100644 --- a/test/RToken.test.ts +++ b/test/RToken.test.ts @@ -67,6 +67,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { let collateral0: Collateral let collateral1: Collateral + let collateral: Collateral[] let collateral2: ATokenFiatCollateral let collateral3: CTokenFiatCollateral let basket: Collateral[] @@ -98,6 +99,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { assetRegistry, backingManager, basket, + collateral, basketHandler, config, facadeTest, @@ -1671,6 +1673,61 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { ) }) + it('Should redeem across the pre-default and backup baskets', async function () { + const backupToken = ( + await ethers.getContractAt('ERC20Mock', await collateral[2].erc20()) + ) + await assetRegistry.connect(owner).register(collateral[2].address) + await basketHandler + .connect(owner) + .setBackupConfig(ethers.utils.formatBytes32String('USD'), 1, [backupToken.address]) + await backupToken.connect(owner).mint(addr1.address, initialBal) + + await setOraclePrice(collateral1.address, bn('0.5e8')) + await collateral1.refresh() + await advanceTime((await collateral1.delayUntilDefault()).toString()) + await collateral1.refresh() + await basketHandler.connect(addr2).refreshBasket() + + await advanceTime(Number(config.warmupPeriod) + 1) + await backupToken.connect(addr1).approve(rToken.address, initialBal) + await rToken.connect(addr1).issue(issueAmount) + + await rToken.connect(owner).setRedemptionThrottleParams({ + amtRate: fp('1e9'), + pctRate: fp('1'), + }) + + const redeemAmount = issueAmount.mul(2) + const basketNonces = [1, 2] + const portions = [fp('0.5'), fp('0.5')] + const quote = await basketHandler.quoteCustomRedemption( + basketNonces, + portions, + redeemAmount + ) + + await rToken + .connect(addr1) + .redeemCustom( + addr1.address, + redeemAmount, + basketNonces, + portions, + quote.erc20s, + quote.quantities + ) + + expect(await rToken.totalSupply()).to.equal(0) + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.equal(0) + await Promise.all( + tokens.map(async (token) => { + expect(await token.balanceOf(addr1.address)).to.equal(initialBal) + }) + ) + expect(await backupToken.balanceOf(addr1.address)).to.equal(initialBal) + }) + it('Should redeem to a different account', async function () { // Provide approvals await Promise.all(tokens.map((t) => t.connect(addr1).approve(rToken.address, initialBal))) diff --git a/test/Recollateralization.test.ts b/test/Recollateralization.test.ts index 962185af3..0292acf19 100644 --- a/test/Recollateralization.test.ts +++ b/test/Recollateralization.test.ts @@ -964,7 +964,7 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { }) }) - it('Should skip start recollateralization after tradingDelay', async () => { + it('Should skip tradingDelay after governance basket switch', async () => { // Set trading delay const newDelay = 3600 await backingManager.connect(owner).setTradingDelay(newDelay) // 1 hour @@ -977,19 +977,13 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { .to.emit(basketHandler, 'BasketSet') .withArgs(3, [token1.address], [fp('1')], false) + expect(await basketHandler.tradingDelayBypassed()).to.equal(true) + // Trigger recollateralization const sellAmt: BigNumber = await token0.balanceOf(backingManager.address) const minBuyAmt: BigNumber = await toMinBuyAmt(sellAmt, fp('1'), fp('1')) - // Attempt to trigger before trading delay - Should revert - await expect(backingManager.rebalance(TradeKind.BATCH_AUCTION)).to.be.revertedWith( - 'trading delayed' - ) - - // Advance time post trading delay - await advanceTime(newDelay + 1) - - // Auction can be run now + // Auction can be run immediately await expect(facadeTest.runAuctionsForAllTraders(rToken.address)) .to.emit(backingManager, 'TradeStarted') .withArgs( @@ -1012,6 +1006,35 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { }) }) + it('Should delay automatic rebalance after default', async () => { + const newDelay = 3600 + await backingManager.connect(owner).setTradingDelay(newDelay) + + await assetRegistry.connect(owner).register(backupCollateral1.address) + await basketHandler + .connect(owner) + .setBackupConfig(ethers.utils.formatBytes32String('USD'), bn(1), [backupToken1.address]) + + await setOraclePrice(collateral0.address, bn('0.5e8')) + await collateral0.refresh() + await advanceTime((await collateral0.delayUntilDefault()).toString()) + await collateral0.refresh() + + await basketHandler.connect(addr1).refreshBasket() + expect(await basketHandler.tradingDelayBypassed()).to.equal(false) + + await advanceTime(config.warmupPeriod.add(1).toString()) + await expect(backingManager.rebalance(TradeKind.BATCH_AUCTION)).to.be.revertedWith( + 'trading delayed' + ) + + await advanceTime(newDelay + 1) + await expect(backingManager.rebalance(TradeKind.BATCH_AUCTION)).to.emit( + backingManager, + 'TradeStarted' + ) + }) + it('Should not recollateralize when switching basket if all assets are UNPRICED', async () => { // Setup prime basket await basketHandler.connect(owner).setPrimeBasket([token1.address], [fp('1')]) diff --git a/test/Revenues.test.ts b/test/Revenues.test.ts index 95ece509c..b2cc9651e 100644 --- a/test/Revenues.test.ts +++ b/test/Revenues.test.ts @@ -5123,7 +5123,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { expect(await rToken.basketsNeeded()).to.equal(mintAmt.mul(2)) }) - it('Should not forward revenue before trading delay', async () => { + it('Should forward revenue immediately after governance basket switch', async () => { // Set trading delay const newDelay = 3600 await backingManager.connect(owner).setTradingDelay(newDelay) // 1 hour @@ -5141,19 +5141,9 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { .to.emit(basketHandler, 'BasketSet') .withArgs(3, [token1.address], [fp('1')], false) - // Cannot forward revenue yet - await expect(backingManager.forwardRevenue([aaveToken.address])).to.be.revertedWith( - 'trading delayed' - ) - - expect(await aaveToken.balanceOf(backingManager.address)).to.equal(rewardAmt) - expect(await aaveToken.balanceOf(rsrTrader.address)).to.equal(0) - expect(await aaveToken.balanceOf(rTokenTrader.address)).to.equal(0) - - // Advance time post trading delay - await advanceTime(newDelay + 1) + expect(await basketHandler.tradingDelayBypassed()).to.equal(true) - // Now we can forward revenue successfully + // Governance basket changes do not wait for trading delay await expect(backingManager.forwardRevenue([aaveToken.address])).to.emit( aaveToken, 'Transfer' @@ -5166,6 +5156,35 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { rewardAmt.mul(4000).div(10000) ) }) + + it('Should delay revenue forwarding after an automatic default', async () => { + const newDelay = 3600 + await backingManager.connect(owner).setTradingDelay(newDelay) + + await assetRegistry.connect(owner).register(collateral[2].address) + await basketHandler + .connect(owner) + .setBackupConfig(ethers.utils.formatBytes32String('USD'), 1, [erc20s[2].address]) + + const rewardAmt = bn('100e18') + await token2.setRewards(backingManager.address, rewardAmt) + await backingManager.claimRewardsSingle(token2.address) + + await token2.setExchangeRate(fp('0.99')) + await assetRegistry.refresh() + await basketHandler.connect(addr1).refreshBasket() + expect(await basketHandler.tradingDelayBypassed()).to.equal(false) + + await advanceTime(Number(config.warmupPeriod) + 1) + await expect(backingManager.forwardRevenue([aaveToken.address])).to.be.revertedWith( + 'trading delayed' + ) + await advanceTime(newDelay + 1) + await expect(backingManager.forwardRevenue([aaveToken.address])).to.emit( + aaveToken, + 'Transfer' + ) + }) }) }) diff --git a/test/fixtures.ts b/test/fixtures.ts index 3f6e7d27a..7c5c6dbfe 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -95,7 +95,7 @@ export const ORACLE_ERROR = fp('0.01') // 1% oracle error export const REVENUE_HIDING = fp('0') // no revenue hiding by default; test individually // This will have to be updated on each release -export const VERSION = '4.2.0' +export const VERSION = '4.3.0' export type Collateral = | FiatCollateral From 1e4d36f1e5b2f2260cb8a18dc13a82df074fa2c9 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Wed, 2 Sep 2026 19:05:57 +0000 Subject: [PATCH 2/7] Fix lint line lengths --- contracts/interfaces/IDeployer.sol | 3 ++- contracts/p0/BackingManager.sol | 3 ++- contracts/p1/BackingManager.sol | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/interfaces/IDeployer.sol b/contracts/interfaces/IDeployer.sol index 38716c11d..caae6cb72 100644 --- a/contracts/interfaces/IDeployer.sol +++ b/contracts/interfaces/IDeployer.sol @@ -47,7 +47,8 @@ struct DeploymentParams { bool enableIssuancePremium; // whether to enable the issuance premium // // === BackingManager === - uint48 tradingDelay; // {s} delay before automatic trading after a default-triggered basket switch + // {s} delay before automatic trading after a default-triggered basket switch + uint48 tradingDelay; uint48 batchAuctionLength; // {s} the length of a Gnosis EasyAuction uint48 dutchAuctionLength; // {s} the length of a falling-price dutch auction uint192 backingBuffer; // {1} how much extra backing collateral to keep diff --git a/contracts/p0/BackingManager.sol b/contracts/p0/BackingManager.sol index f0e10d484..31e5e9c44 100644 --- a/contracts/p0/BackingManager.sol +++ b/contracts/p0/BackingManager.sol @@ -24,7 +24,8 @@ contract BackingManagerP0 is TradingP0, IBackingManager { uint48 public constant MAX_TRADING_DELAY = 60 * 60 * 24 * 365; // {s} 1 year uint192 public constant MAX_BACKING_BUFFER = 1e18; // {%} - uint48 public tradingDelay; // {s} delay before automatic trading after a default-triggered basket switch + // {s} delay before automatic trading after a default-triggered basket switch + uint48 public tradingDelay; uint192 public backingBuffer; // {%} how much extra backing collateral to keep mapping(TradeKind => uint48) private tradeEnd; // {s} last endTime() of an auction per kind diff --git a/contracts/p1/BackingManager.sol b/contracts/p1/BackingManager.sol index 220ce691f..76cf74ff0 100644 --- a/contracts/p1/BackingManager.sol +++ b/contracts/p1/BackingManager.sol @@ -33,7 +33,8 @@ contract BackingManagerP1 is TradingP1, IBackingManager { uint48 public constant MAX_TRADING_DELAY = 60 * 60 * 24 * 365; // {s} 1 year uint192 public constant MAX_BACKING_BUFFER = FIX_ONE; // {1} 100% - uint48 public tradingDelay; // {s} delay before automatic trading after a default-triggered basket switch + // {s} delay before automatic trading after a default-triggered basket switch + uint48 public tradingDelay; uint192 public backingBuffer; // {1} how much extra backing collateral to keep // === 3.0.0 === From b06d4cbd36940be294e51e4e26bf08ad830f2cbc Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Wed, 2 Sep 2026 19:21:07 +0000 Subject: [PATCH 3/7] Simplify trading delay guards --- contracts/interfaces/IDeployer.sol | 3 +-- contracts/p0/BackingManager.sol | 21 +++++++++------------ contracts/p0/BasketHandler.sol | 6 +++++- contracts/p1/BackingManager.sol | 19 +++++++++++-------- contracts/p1/BasketHandler.sol | 6 +++++- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/contracts/interfaces/IDeployer.sol b/contracts/interfaces/IDeployer.sol index caae6cb72..7f14b63a6 100644 --- a/contracts/interfaces/IDeployer.sol +++ b/contracts/interfaces/IDeployer.sol @@ -47,8 +47,7 @@ struct DeploymentParams { bool enableIssuancePremium; // whether to enable the issuance premium // // === BackingManager === - // {s} delay before automatic trading after a default-triggered basket switch - uint48 tradingDelay; + uint48 tradingDelay; // {s} delay for automatic trading after default uint48 batchAuctionLength; // {s} the length of a Gnosis EasyAuction uint48 dutchAuctionLength; // {s} the length of a falling-price dutch auction uint192 backingBuffer; // {1} how much extra backing collateral to keep diff --git a/contracts/p0/BackingManager.sol b/contracts/p0/BackingManager.sol index 31e5e9c44..06c42192d 100644 --- a/contracts/p0/BackingManager.sol +++ b/contracts/p0/BackingManager.sol @@ -24,8 +24,7 @@ contract BackingManagerP0 is TradingP0, IBackingManager { uint48 public constant MAX_TRADING_DELAY = 60 * 60 * 24 * 365; // {s} 1 year uint192 public constant MAX_BACKING_BUFFER = 1e18; // {%} - // {s} delay before automatic trading after a default-triggered basket switch - uint48 public tradingDelay; + uint48 public tradingDelay; // {s} delay for automatic trading after default uint192 public backingBuffer; // {%} how much extra backing collateral to keep mapping(TradeKind => uint48) private tradeEnd; // {s} last endTime() of an auction per kind @@ -95,12 +94,11 @@ contract BackingManagerP0 is TradingP0, IBackingManager { require(tradesOpen == 0, "trade open"); require(main.basketHandler().isReady(), "basket not ready"); - if (!main.basketHandler().tradingDelayBypassed()) { - require( + require( + main.basketHandler().tradingDelayBypassed() || block.timestamp >= main.basketHandler().timestamp() + tradingDelay, - "trading delayed" - ); - } + "trading delayed" + ); require(!main.basketHandler().fullyCollateralized(), "already collateralized"); // First dissolve any held RToken balance @@ -154,12 +152,11 @@ contract BackingManagerP0 is TradingP0, IBackingManager { require(tradesOpen == 0, "trade open"); require(main.basketHandler().isReady(), "basket not ready"); - if (!main.basketHandler().tradingDelayBypassed()) { - require( + require( + main.basketHandler().tradingDelayBypassed() || block.timestamp >= main.basketHandler().timestamp() + tradingDelay, - "trading delayed" - ); - } + "trading delayed" + ); require(main.basketHandler().fullyCollateralized(), "undercollateralized"); BasketRange memory basketsHeld = main.basketHandler().basketsHeldBy(address(this)); diff --git a/contracts/p0/BasketHandler.sol b/contracts/p0/BasketHandler.sol index bf6aa78b3..3a9eb2fda 100644 --- a/contracts/p0/BasketHandler.sol +++ b/contracts/p0/BasketHandler.sol @@ -224,7 +224,11 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { isOwner || (lastStatus == CollateralStatus.DISABLED && !main.tradingPausedOrFrozen()), "basket unrefreshable" ); - tradingDelayBypassed = isOwner || disabled; + if (isOwner) { + tradingDelayBypassed = true; + } else if (!disabled) { + tradingDelayBypassed = false; + } _switchBasket(); trackStatus(); diff --git a/contracts/p1/BackingManager.sol b/contracts/p1/BackingManager.sol index 76cf74ff0..09ea31c20 100644 --- a/contracts/p1/BackingManager.sol +++ b/contracts/p1/BackingManager.sol @@ -33,8 +33,7 @@ contract BackingManagerP1 is TradingP1, IBackingManager { uint48 public constant MAX_TRADING_DELAY = 60 * 60 * 24 * 365; // {s} 1 year uint192 public constant MAX_BACKING_BUFFER = FIX_ONE; // {1} 100% - // {s} delay before automatic trading after a default-triggered basket switch - uint48 public tradingDelay; + uint48 public tradingDelay; // {s} delay for automatic trading after default uint192 public backingBuffer; // {1} how much extra backing collateral to keep // === 3.0.0 === @@ -121,9 +120,11 @@ contract BackingManagerP1 is TradingP1, IBackingManager { require(tradesOpen == 0, "trade open"); require(basketHandler.isReady(), "basket not ready"); - if (!basketHandler.tradingDelayBypassed()) { - require(block.timestamp >= basketHandler.timestamp() + tradingDelay, "trading delayed"); - } + require( + basketHandler.tradingDelayBypassed() || + block.timestamp >= basketHandler.timestamp() + tradingDelay, + "trading delayed" + ); BasketRange memory basketsHeld = basketHandler.basketsHeldBy(address(this)); require(basketsHeld.bottom < rToken.basketsNeeded(), "already collateralized"); @@ -188,9 +189,11 @@ contract BackingManagerP1 is TradingP1, IBackingManager { require(tradesOpen == 0, "trade open"); require(basketHandler.isReady(), "basket not ready"); - if (!basketHandler.tradingDelayBypassed()) { - require(block.timestamp >= basketHandler.timestamp() + tradingDelay, "trading delayed"); - } + require( + basketHandler.tradingDelayBypassed() || + block.timestamp >= basketHandler.timestamp() + tradingDelay, + "trading delayed" + ); require(basketsHeld.bottom >= rToken.basketsNeeded(), "undercollateralized"); // require(basketHandler.fullyCollateralized()) diff --git a/contracts/p1/BasketHandler.sol b/contracts/p1/BasketHandler.sol index f4c3020fb..4a1be44ee 100644 --- a/contracts/p1/BasketHandler.sol +++ b/contracts/p1/BasketHandler.sol @@ -169,7 +169,11 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { isOwner || (lastStatus == CollateralStatus.DISABLED && !main.tradingPausedOrFrozen()), "basket unrefreshable" ); - tradingDelayBypassed = isOwner || disabled; + if (isOwner) { + tradingDelayBypassed = true; + } else if (!disabled) { + tradingDelayBypassed = false; + } _switchBasket(); trackStatus(); From 4b89ee4893dd5b178a00325626eddefe7183ad50 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Wed, 2 Sep 2026 19:22:08 +0000 Subject: [PATCH 4/7] Order trading delay guard conditions --- contracts/p0/BackingManager.sol | 8 ++++---- contracts/p1/BackingManager.sol | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/p0/BackingManager.sol b/contracts/p0/BackingManager.sol index 06c42192d..7a3bdce60 100644 --- a/contracts/p0/BackingManager.sol +++ b/contracts/p0/BackingManager.sol @@ -95,8 +95,8 @@ contract BackingManagerP0 is TradingP0, IBackingManager { require(tradesOpen == 0, "trade open"); require(main.basketHandler().isReady(), "basket not ready"); require( - main.basketHandler().tradingDelayBypassed() || - block.timestamp >= main.basketHandler().timestamp() + tradingDelay, + block.timestamp >= main.basketHandler().timestamp() + tradingDelay || + main.basketHandler().tradingDelayBypassed(), "trading delayed" ); require(!main.basketHandler().fullyCollateralized(), "already collateralized"); @@ -153,8 +153,8 @@ contract BackingManagerP0 is TradingP0, IBackingManager { require(tradesOpen == 0, "trade open"); require(main.basketHandler().isReady(), "basket not ready"); require( - main.basketHandler().tradingDelayBypassed() || - block.timestamp >= main.basketHandler().timestamp() + tradingDelay, + block.timestamp >= main.basketHandler().timestamp() + tradingDelay || + main.basketHandler().tradingDelayBypassed(), "trading delayed" ); require(main.basketHandler().fullyCollateralized(), "undercollateralized"); diff --git a/contracts/p1/BackingManager.sol b/contracts/p1/BackingManager.sol index 09ea31c20..d9e8b679b 100644 --- a/contracts/p1/BackingManager.sol +++ b/contracts/p1/BackingManager.sol @@ -121,8 +121,8 @@ contract BackingManagerP1 is TradingP1, IBackingManager { require(tradesOpen == 0, "trade open"); require(basketHandler.isReady(), "basket not ready"); require( - basketHandler.tradingDelayBypassed() || - block.timestamp >= basketHandler.timestamp() + tradingDelay, + block.timestamp >= basketHandler.timestamp() + tradingDelay || + basketHandler.tradingDelayBypassed(), "trading delayed" ); @@ -190,8 +190,8 @@ contract BackingManagerP1 is TradingP1, IBackingManager { require(tradesOpen == 0, "trade open"); require(basketHandler.isReady(), "basket not ready"); require( - basketHandler.tradingDelayBypassed() || - block.timestamp >= basketHandler.timestamp() + tradingDelay, + block.timestamp >= basketHandler.timestamp() + tradingDelay || + basketHandler.tradingDelayBypassed(), "trading delayed" ); require(basketsHeld.bottom >= rToken.basketsNeeded(), "undercollateralized"); From 8339c152ad0072b0684c234129ed7c06de4558ae Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Wed, 2 Sep 2026 19:32:15 +0000 Subject: [PATCH 5/7] Document bypass state preservation --- contracts/p0/BasketHandler.sol | 1 + contracts/p1/BasketHandler.sol | 1 + 2 files changed, 2 insertions(+) diff --git a/contracts/p0/BasketHandler.sol b/contracts/p0/BasketHandler.sol index 3a9eb2fda..aea8c38e9 100644 --- a/contracts/p0/BasketHandler.sol +++ b/contracts/p0/BasketHandler.sol @@ -224,6 +224,7 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { isOwner || (lastStatus == CollateralStatus.DISABLED && !main.tradingPausedOrFrozen()), "basket unrefreshable" ); + // A disabled basket preserves the prior bypass value. if (isOwner) { tradingDelayBypassed = true; } else if (!disabled) { diff --git a/contracts/p1/BasketHandler.sol b/contracts/p1/BasketHandler.sol index 4a1be44ee..35c06f815 100644 --- a/contracts/p1/BasketHandler.sol +++ b/contracts/p1/BasketHandler.sol @@ -169,6 +169,7 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { isOwner || (lastStatus == CollateralStatus.DISABLED && !main.tradingPausedOrFrozen()), "basket unrefreshable" ); + // A disabled basket preserves the prior bypass value. if (isOwner) { tradingDelayBypassed = true; } else if (!disabled) { From bf94759cd5a4519ea431b88e1947e5caff373293 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Wed, 2 Sep 2026 19:52:10 +0000 Subject: [PATCH 6/7] Exclude retired dgnETH from historical upgrades --- test/integration/UpgradeToR4.test.ts | 8 +------- test/integration/UpgradeToR4WithRegistries.test.ts | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/test/integration/UpgradeToR4.test.ts b/test/integration/UpgradeToR4.test.ts index a9b4b21e2..3d4ee9abc 100644 --- a/test/integration/UpgradeToR4.test.ts +++ b/test/integration/UpgradeToR4.test.ts @@ -18,13 +18,7 @@ interface RTokenParams { } // These RTokens must be on 3.4.0 as the target block -const rTokensToTest: RTokenParams[] = [ - { - name: 'dgnETH', - mainAddress: '0xC376168c8470C6e0F4854A7d450874C30A0973d7', - timelockAddress: '0x98D7C5230C46b671dB0CeBb25B17d1E183B23B97', - }, -] +const rTokensToTest: RTokenParams[] = [] // 4.2.0 const v4VersionHash = '0x99b189f6a35f2d8d52cd79b21cabb1eca4a12f69132e253d75b4ee7634d0fef8' diff --git a/test/integration/UpgradeToR4WithRegistries.test.ts b/test/integration/UpgradeToR4WithRegistries.test.ts index b2ca9756d..f12cfc19c 100644 --- a/test/integration/UpgradeToR4WithRegistries.test.ts +++ b/test/integration/UpgradeToR4WithRegistries.test.ts @@ -17,13 +17,7 @@ interface RTokenParams { } // These RTokens must be on 3.4.0 as the target block -const rTokensToTest: RTokenParams[] = [ - { - name: 'dgnETH', - mainAddress: '0xC376168c8470C6e0F4854A7d450874C30A0973d7', - timelockAddress: '0x98D7C5230C46b671dB0CeBb25B17d1E183B23B97', - }, -] +const rTokensToTest: RTokenParams[] = [] // 4.2.0 const v4VersionHash = '0x99b189f6a35f2d8d52cd79b21cabb1eca4a12f69132e253d75b4ee7634d0fef8' From 98667e9cf20606da5b3feefab7888d1ce2a1e8f4 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Wed, 2 Sep 2026 20:22:26 +0000 Subject: [PATCH 7/7] Restore dgnETH upgrade coverage for 4.3.0 --- test/integration/UpgradeToR4.test.ts | 20 ++++++++++------ .../UpgradeToR4WithRegistries.test.ts | 24 ++++++++++++------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/test/integration/UpgradeToR4.test.ts b/test/integration/UpgradeToR4.test.ts index 3d4ee9abc..70fe2e2e6 100644 --- a/test/integration/UpgradeToR4.test.ts +++ b/test/integration/UpgradeToR4.test.ts @@ -18,10 +18,16 @@ interface RTokenParams { } // These RTokens must be on 3.4.0 as the target block -const rTokensToTest: RTokenParams[] = [] +const rTokensToTest: RTokenParams[] = [ + { + name: 'dgnETH', + mainAddress: '0xC376168c8470C6e0F4854A7d450874C30A0973d7', + timelockAddress: '0x98D7C5230C46b671dB0CeBb25B17d1E183B23B97', + }, +] -// 4.2.0 -const v4VersionHash = '0x99b189f6a35f2d8d52cd79b21cabb1eca4a12f69132e253d75b4ee7634d0fef8' +// 4.3.0 +const v4VersionHash = '0xdef94dbdd8411ae515cefcb56b153df454453010a1f4c881b3daa7ed18db793a' async function _confirmVersion(address: string, target: string) { const versionedTarget = await ethers.getContractAt('Versioned', address) @@ -29,7 +35,7 @@ async function _confirmVersion(address: string, target: string) { } // NOTE: This is an explicit test! -describe('Upgrade from 3.4.0 to 4.2.0 (Mainnet Fork)', () => { +describe('Upgrade from 3.4.0 to 4.3.0 (Mainnet Fork)', () => { let implementations: IImplementations let deployer: DeployerP1 let versionRegistry: VersionRegistry @@ -132,7 +138,7 @@ describe('Upgrade from 3.4.0 to 4.2.0 (Mainnet Fork)', () => { ) await whileImpersonating(hre, TimelockController.address, async (signer) => { - // Upgrade Main to 4.2.0's Main + // Upgrade Main to 4.3.0's Main await RTokenMain.connect(signer).upgradeTo(implementations.main) // Set registries @@ -175,7 +181,7 @@ describe('Upgrade from 3.4.0 to 4.2.0 (Mainnet Fork)', () => { ] for (let j = 0; j < targetsToVerify.length; j++) { - await _confirmVersion(targetsToVerify[j], '4.2.0') + await _confirmVersion(targetsToVerify[j], '4.3.0') } const broker = await ethers.getContractAt('BrokerP1', await RTokenMain.broker()) @@ -187,7 +193,7 @@ describe('Upgrade from 3.4.0 to 4.2.0 (Mainnet Fork)', () => { // So, let's upgrade the RToken _again_ to verify the process flow works. await whileImpersonating(hre, TimelockController.address, async (signer) => { - // Upgrade Main to 4.2.0's Main + // Upgrade Main to 4.3.0's Main await RTokenMain.connect(signer).upgradeMainTo(v4VersionHash) // Upgrade RToken diff --git a/test/integration/UpgradeToR4WithRegistries.test.ts b/test/integration/UpgradeToR4WithRegistries.test.ts index f12cfc19c..849528864 100644 --- a/test/integration/UpgradeToR4WithRegistries.test.ts +++ b/test/integration/UpgradeToR4WithRegistries.test.ts @@ -17,10 +17,16 @@ interface RTokenParams { } // These RTokens must be on 3.4.0 as the target block -const rTokensToTest: RTokenParams[] = [] - -// 4.2.0 -const v4VersionHash = '0x99b189f6a35f2d8d52cd79b21cabb1eca4a12f69132e253d75b4ee7634d0fef8' +const rTokensToTest: RTokenParams[] = [ + { + name: 'dgnETH', + mainAddress: '0xC376168c8470C6e0F4854A7d450874C30A0973d7', + timelockAddress: '0x98D7C5230C46b671dB0CeBb25B17d1E183B23B97', + }, +] + +// 4.3.0 +const v4VersionHash = '0xdef94dbdd8411ae515cefcb56b153df454453010a1f4c881b3daa7ed18db793a' const v2VersionHash = '0xb4bcb154e38601c389396fa918314da42d4626f13ef6d0ceb07e5f5d26b2fbc3' async function _confirmVersion(address: string, target: string) { @@ -29,7 +35,7 @@ async function _confirmVersion(address: string, target: string) { } // NOTE: This is an explicit test! -describe('Upgrade from 4.2.0 to New Version with all Registries Enabled', () => { +describe('Upgrade from 4.3.0 to New Version with all Registries Enabled', () => { let versionRegistry: VersionRegistry let assetPluginRegistry: AssetPluginRegistry let daoFeeRegistry: DAOFeeRegistry @@ -194,7 +200,7 @@ describe('Upgrade from 4.2.0 to New Version with all Registries Enabled', () => ) await whileImpersonating(hre, TimelockController.address, async (signer) => { - // Upgrade Main to 4.2.0's Main + // Upgrade Main to 4.3.0's Main await RTokenMain.connect(signer).upgradeTo(implementationsR4.main) // Set registries @@ -237,7 +243,7 @@ describe('Upgrade from 4.2.0 to New Version with all Registries Enabled', () => ] for (let j = 0; j < targetsToVerify.length; j++) { - await _confirmVersion(targetsToVerify[j], '4.2.0') + await _confirmVersion(targetsToVerify[j], '4.3.0') } const currentAssetRegistry = await RTokenAssetRegistry.getRegistry() @@ -255,7 +261,7 @@ describe('Upgrade from 4.2.0 to New Version with all Registries Enabled', () => // So, let's upgrade the RToken to a new version now. await whileImpersonating(hre, TimelockController.address, async (signer) => { - // Upgrade Main to 4.2.0's Main + // Upgrade Main to 4.3.0's Main await RTokenMain.connect(signer).upgradeMainTo(v2VersionHash) // Registry does not have assets yet. @@ -278,7 +284,7 @@ describe('Upgrade from 4.2.0 to New Version with all Registries Enabled', () => // Finish upgrade, with asset validation await whileImpersonating(hre, TimelockController.address, async (signer) => { - // Upgrade Main to 4.2.0's Main + // Upgrade Main to 4.3.0's Main await RTokenMain.connect(signer).upgradeMainTo(v2VersionHash) // Upgrade RToken, without validating assets