From 251a0d4f6dfa11a638a8d416c7788f0b4368a097 Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Wed, 3 Dec 2025 17:47:44 +0900 Subject: [PATCH 01/15] wip: Debug accrue test --- snapshots/Hub.Operations.json | 4 ++-- tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/snapshots/Hub.Operations.json b/snapshots/Hub.Operations.json index 54b1b87c4..80f27df18 100644 --- a/snapshots/Hub.Operations.json +++ b/snapshots/Hub.Operations.json @@ -11,8 +11,8 @@ "remove: partial": "81640", "reportDeficit": "115225", "restore: full": "80471", - "restore: full - with transfer": "173377", + "restore: full - with transfer": "165877", "restore: partial": "89137", - "restore: partial - with transfer": "147400", + "restore: partial - with transfer": "144900", "transferShares": "71192" } \ No newline at end of file diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index 8934d64e2..d5916589e 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -56,7 +56,6 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { TestAmounts memory amounts, uint40 skipTime ) public { - vm.skip(true, 'pending rft'); amounts = _bound(amounts); skipTime = bound(skipTime, 0, MAX_SKIP_TIME / 2).toUint40(); @@ -589,18 +588,26 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { 'dai after second accrual' ); + rates.wethBaseBorrowRate = hub1.getAssetDrawnRate(wethAssetId).toUint96(); indices.wethIndex = _calculateExpectedDrawnIndex( indices.wethIndex, rates.wethBaseBorrowRate, startTime ); + /* + assertEq( + indices.wethIndex, + hub1.getAssetDrawnIndex(wethAssetId), + 'weth drawn index after second accrual' + );*/ bobPosition = spoke2.getUserPosition(_wethReserveId(spoke2), bob); assertEq( bobPosition.drawnShares, baseShares.weth, 'weth base drawn shares after second accrual' ); - drawnDebt = baseShares.weth.rayMulUp(indices.wethIndex); + //drawnDebt = baseShares.weth.rayMulUp(indices.wethIndex); + drawnDebt = baseShares.weth.rayMulUp(hub1.getAssetDrawnIndex(wethAssetId)); expectedPremiumDebt = _calculateExpectedPremiumDebt( amounts.wethBorrowAmount, drawnDebt, From f36e4b6d87c6684516dc9c476eef77bf5562c5fe Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Wed, 3 Dec 2025 22:04:56 +0900 Subject: [PATCH 02/15] wip: debugging indices --- tests/Base.t.sol | 11 +- .../Spoke/Spoke.AccrueInterest.Scenario.t.sol | 115 ++++++++++++------ 2 files changed, 86 insertions(+), 40 deletions(-) diff --git a/tests/Base.t.sol b/tests/Base.t.sol index 424bc3683..075b43448 100644 --- a/tests/Base.t.sol +++ b/tests/Base.t.sol @@ -11,7 +11,10 @@ import {console2 as console} from 'forge-std/console2.sol'; // dependencies import {AggregatorV3Interface} from 'src/dependencies/chainlink/AggregatorV3Interface.sol'; -import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; +import { + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy +} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; import {IERC20Metadata} from 'src/dependencies/openzeppelin/IERC20Metadata.sol'; import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; import {IERC20Errors} from 'src/dependencies/openzeppelin/IERC20Errors.sol'; @@ -44,7 +47,11 @@ import {AccessManagerEnumerable} from 'src/access/AccessManagerEnumerable.sol'; import {HubConfigurator, IHubConfigurator} from 'src/hub/HubConfigurator.sol'; import {Hub, IHub, IHubBase} from 'src/hub/Hub.sol'; import {SharesMath} from 'src/hub/libraries/SharesMath.sol'; -import {AssetInterestRateStrategy, IAssetInterestRateStrategy, IBasicInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol'; +import { + AssetInterestRateStrategy, + IAssetInterestRateStrategy, + IBasicInterestRateStrategy +} from 'src/hub/AssetInterestRateStrategy.sol'; // spoke import {Spoke, ISpoke, ISpokeBase} from 'src/spoke/Spoke.sol'; diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index d5916589e..0b7825f10 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -42,6 +42,13 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { uint256 wbtc; } + struct OriginalValues { + uint40 startTime; + uint256 wethIndex; + uint256 usdxIndex; + uint256 wbtcIndex; + } + function setUp() public override { super.setUp(); updateLiquidityFee(hub1, daiAssetId, 0); @@ -64,6 +71,11 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { TestAmounts memory originalAmounts = _copyAmounts(amounts); // deep copy original amounts uint40 startTime = vm.getBlockTimestamp().toUint40(); + OriginalValues memory originalValues; + originalValues.startTime = startTime; + originalValues.wethIndex = hub1.getAssetDrawnIndex(wethAssetId); + originalValues.usdxIndex = hub1.getAssetDrawnIndex(usdxAssetId); + originalValues.wbtcIndex = hub1.getAssetDrawnIndex(wbtcAssetId); // Bob supply dai on spoke 2 if (amounts.daiSupplyAmount > 0) { @@ -472,11 +484,23 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { rates.usdxBaseBorrowRate = hub1.getAssetDrawnRate(usdxAssetId).toUint96(); rates.wbtcBaseBorrowRate = hub1.getAssetDrawnRate(wbtcAssetId).toUint96(); + // Refresh indexes + Indices memory indices; + indices.daiIndex = hub1.getAssetDrawnIndex(daiAssetId); + indices.wethIndex = hub1.getAssetDrawnIndex(wethAssetId); + indices.usdxIndex = hub1.getAssetDrawnIndex(usdxAssetId); + indices.wbtcIndex = hub1.getAssetDrawnIndex(wbtcAssetId); + BaseShares memory baseShares; // Check debt values before accrual bobPosition = spoke2.getUserPosition(_daiReserveId(spoke2), bob); - expectedPremiumDebt = _calculatePremiumDebtRay(spoke2, _daiReserveId(spoke2), bob); + expectedPremiumDebt = _calculatePremiumDebt( + hub1, + daiAssetId, + bobPosition.premiumShares, + bobPosition.premiumOffsetRay + ); _assertSingleUserProtocolDebt( spoke2, _daiReserveId(spoke2), @@ -488,7 +512,12 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { baseShares.dai = bobPosition.drawnShares; bobPosition = spoke2.getUserPosition(_wethReserveId(spoke2), bob); - expectedPremiumDebt = _calculatePremiumDebtRay(spoke2, _wethReserveId(spoke2), bob); + expectedPremiumDebt = _calculatePremiumDebt( + hub1, + wethAssetId, + bobPosition.premiumShares, + bobPosition.premiumOffsetRay + ); _assertSingleUserProtocolDebt( spoke2, _wethReserveId(spoke2), @@ -500,7 +529,12 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { baseShares.weth = bobPosition.drawnShares; bobPosition = spoke2.getUserPosition(_usdxReserveId(spoke2), bob); - expectedPremiumDebt = _calculatePremiumDebtRay(spoke2, _usdxReserveId(spoke2), bob); + expectedPremiumDebt = _calculatePremiumDebt( + hub1, + usdxAssetId, + bobPosition.premiumShares, + bobPosition.premiumOffsetRay + ); _assertSingleUserProtocolDebt( spoke2, _usdxReserveId(spoke2), @@ -512,7 +546,12 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { baseShares.usdx = bobPosition.drawnShares; bobPosition = spoke2.getUserPosition(_wbtcReserveId(spoke2), bob); - expectedPremiumDebt = _calculatePremiumDebtRay(spoke2, _wbtcReserveId(spoke2), bob); + expectedPremiumDebt = _calculatePremiumDebt( + hub1, + wbtcAssetId, + bobPosition.premiumShares, + bobPosition.premiumOffsetRay + ); _assertSingleUserProtocolDebt( spoke2, _wbtcReserveId(spoke2), @@ -523,13 +562,6 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); baseShares.wbtc = bobPosition.drawnShares; - // Store index before accrual, and use this for calculating expected drawn debt - Indices memory indices; - indices.daiIndex = hub1.getAssetDrawnIndex(daiAssetId); - indices.wethIndex = hub1.getAssetDrawnIndex(wethAssetId); - indices.usdxIndex = hub1.getAssetDrawnIndex(usdxAssetId); - indices.wbtcIndex = hub1.getAssetDrawnIndex(wbtcAssetId); - // Store timestamp before next skip time startTime = vm.getBlockTimestamp().toUint40(); skipTime = randomizer(0, MAX_SKIP_TIME / 2).toUint40(); @@ -541,12 +573,18 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { rates.daiBaseBorrowRate, startTime ); + assertEq( + indices.daiIndex, + hub1.getAssetDrawnIndex(daiAssetId), + 'dai drawn index after second accrual' + ); bobPosition = spoke2.getUserPosition(_daiReserveId(spoke2), bob); drawnDebt = baseShares.dai.rayMulUp(indices.daiIndex); - expectedPremiumDebt = _calculateExpectedPremiumDebt( - amounts.daiBorrowAmount, - drawnDebt, - bobRp + expectedPremiumDebt = _calculatePremiumDebt( + hub1, + daiAssetId, + bobPosition.premiumShares, + bobPosition.premiumOffsetRay ); interest = (drawnDebt + expectedPremiumDebt) - @@ -588,30 +626,29 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { 'dai after second accrual' ); - rates.wethBaseBorrowRate = hub1.getAssetDrawnRate(wethAssetId).toUint96(); indices.wethIndex = _calculateExpectedDrawnIndex( - indices.wethIndex, + originalValues.wethIndex, rates.wethBaseBorrowRate, - startTime + originalValues.startTime ); - /* + assertEq( indices.wethIndex, hub1.getAssetDrawnIndex(wethAssetId), 'weth drawn index after second accrual' - );*/ + ); bobPosition = spoke2.getUserPosition(_wethReserveId(spoke2), bob); assertEq( bobPosition.drawnShares, baseShares.weth, 'weth base drawn shares after second accrual' ); - //drawnDebt = baseShares.weth.rayMulUp(indices.wethIndex); - drawnDebt = baseShares.weth.rayMulUp(hub1.getAssetDrawnIndex(wethAssetId)); - expectedPremiumDebt = _calculateExpectedPremiumDebt( - amounts.wethBorrowAmount, - drawnDebt, - bobRp + drawnDebt = baseShares.weth.rayMulUp(indices.wethIndex); + expectedPremiumDebt = _calculatePremiumDebt( + hub1, + wethAssetId, + bobPosition.premiumShares, + bobPosition.premiumOffsetRay ); interest = (drawnDebt + expectedPremiumDebt) - @@ -654,16 +691,17 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); indices.usdxIndex = _calculateExpectedDrawnIndex( - indices.usdxIndex, + originalValues.usdxIndex, rates.usdxBaseBorrowRate, - startTime + originalValues.startTime ); bobPosition = spoke2.getUserPosition(_usdxReserveId(spoke2), bob); drawnDebt = baseShares.usdx.rayMulUp(indices.usdxIndex); - expectedPremiumDebt = _calculateExpectedPremiumDebt( - amounts.usdxBorrowAmount, - drawnDebt, - bobRp + expectedPremiumDebt = _calculatePremiumDebt( + hub1, + usdxAssetId, + bobPosition.premiumShares, + bobPosition.premiumOffsetRay ); interest = (drawnDebt + expectedPremiumDebt) - @@ -706,16 +744,17 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); indices.wbtcIndex = _calculateExpectedDrawnIndex( - indices.wbtcIndex, + originalValues.wbtcIndex, rates.wbtcBaseBorrowRate, - startTime + originalValues.startTime ); bobPosition = spoke2.getUserPosition(_wbtcReserveId(spoke2), bob); drawnDebt = baseShares.wbtc.rayMulUp(indices.wbtcIndex); - expectedPremiumDebt = _calculateExpectedPremiumDebt( - amounts.wbtcBorrowAmount, - drawnDebt, - bobRp + expectedPremiumDebt = _calculatePremiumDebt( + hub1, + wbtcAssetId, + bobPosition.premiumShares, + bobPosition.premiumOffsetRay ); interest = (drawnDebt + expectedPremiumDebt) - From 32cfa1abb22724eb27d74afd00410541d22d0699 Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Mon, 8 Dec 2025 16:52:04 +0900 Subject: [PATCH 03/15] wip: More debugging --- .../Spoke/Spoke.AccrueInterest.Scenario.t.sol | 62 ++++++++++++++----- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index 0b7825f10..3eafc05f6 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -8,7 +8,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { using SharesMath for uint256; using WadRayMath for *; using PercentageMath for uint256; - using SafeCast for uint256; + using SafeCast for *; struct TestAmounts { uint256 daiSupplyAmount; @@ -49,6 +49,13 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { uint256 wbtcIndex; } + struct UpdatedTimestamps { + uint40 dai; + uint40 weth; + uint40 usdx; + uint40 wbtc; + } + function setUp() public override { super.setUp(); updateLiquidityFee(hub1, daiAssetId, 0); @@ -64,7 +71,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { uint40 skipTime ) public { amounts = _bound(amounts); - skipTime = bound(skipTime, 0, MAX_SKIP_TIME / 2).toUint40(); + skipTime = bound(skipTime, 0, 2000 days).toUint40(); // Ensure bob does not draw more than half his normalized supply value amounts = _ensureSufficientCollateral(spoke2, amounts); @@ -486,10 +493,17 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { // Refresh indexes Indices memory indices; - indices.daiIndex = hub1.getAssetDrawnIndex(daiAssetId); - indices.wethIndex = hub1.getAssetDrawnIndex(wethAssetId); - indices.usdxIndex = hub1.getAssetDrawnIndex(usdxAssetId); - indices.wbtcIndex = hub1.getAssetDrawnIndex(wbtcAssetId); + indices.daiIndex = hub1.getAssetDrawnIndex(daiAssetId).toUint120(); + indices.wethIndex = hub1.getAssetDrawnIndex(wethAssetId).toUint120(); + indices.usdxIndex = hub1.getAssetDrawnIndex(usdxAssetId).toUint120(); + indices.wbtcIndex = hub1.getAssetDrawnIndex(wbtcAssetId).toUint120(); + + // Refresh timestamps - DAI may or may not have been borrowed above, potentially triggering accruals + UpdatedTimestamps memory timestamps; + timestamps.dai = hub1.getAsset(daiAssetId).lastUpdateTimestamp; + timestamps.weth = hub1.getAsset(wethAssetId).lastUpdateTimestamp; + timestamps.usdx = hub1.getAsset(usdxAssetId).lastUpdateTimestamp; + timestamps.wbtc = hub1.getAsset(wbtcAssetId).lastUpdateTimestamp; BaseShares memory baseShares; @@ -562,16 +576,22 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); baseShares.wbtc = bobPosition.drawnShares; + assertEq( + hub1.getAsset(wethAssetId).lastUpdateTimestamp, + timestamps.weth, + 'weth last update time before second accrual' + ); + // Store timestamp before next skip time startTime = vm.getBlockTimestamp().toUint40(); - skipTime = randomizer(0, MAX_SKIP_TIME / 2).toUint40(); + skipTime = randomizer(0, 1000 days).toUint40(); skip(skipTime); // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level indices.daiIndex = _calculateExpectedDrawnIndex( indices.daiIndex, rates.daiBaseBorrowRate, - startTime + timestamps.dai ); assertEq( indices.daiIndex, @@ -626,14 +646,26 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { 'dai after second accrual' ); - indices.wethIndex = _calculateExpectedDrawnIndex( - originalValues.wethIndex, + assertEq( rates.wethBaseBorrowRate, - originalValues.startTime + hub1.getAssetDrawnRate(wethAssetId), + 'weth drawn rate after second accrual' ); assertEq( + hub1.getAsset(wethAssetId).lastUpdateTimestamp, + timestamps.weth, + 'weth last update time before second accrual' + ); + + indices.wethIndex = _calculateExpectedDrawnIndex( indices.wethIndex, + rates.wethBaseBorrowRate, + timestamps.weth + ); + + assertEq( + indices.wethIndex.toUint120(), hub1.getAssetDrawnIndex(wethAssetId), 'weth drawn index after second accrual' ); @@ -691,9 +723,9 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); indices.usdxIndex = _calculateExpectedDrawnIndex( - originalValues.usdxIndex, + indices.usdxIndex, rates.usdxBaseBorrowRate, - originalValues.startTime + timestamps.usdx ); bobPosition = spoke2.getUserPosition(_usdxReserveId(spoke2), bob); drawnDebt = baseShares.usdx.rayMulUp(indices.usdxIndex); @@ -744,9 +776,9 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); indices.wbtcIndex = _calculateExpectedDrawnIndex( - originalValues.wbtcIndex, + indices.wbtcIndex, rates.wbtcBaseBorrowRate, - originalValues.startTime + timestamps.wbtc ); bobPosition = spoke2.getUserPosition(_wbtcReserveId(spoke2), bob); drawnDebt = baseShares.wbtc.rayMulUp(indices.wbtcIndex); From 5dc2b50a9ec18649e2ffbc222040dc2ab41c4163 Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Mon, 8 Dec 2025 19:36:32 +0900 Subject: [PATCH 04/15] fix: accrue scenario test --- .../Spoke/Spoke.AccrueInterest.Scenario.t.sol | 490 +++++++++--------- 1 file changed, 242 insertions(+), 248 deletions(-) diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index 3eafc05f6..59a812afb 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -19,41 +19,29 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { uint256 wethBorrowAmount; uint256 usdxBorrowAmount; uint256 wbtcBorrowAmount; + uint40 startTime; + uint256 wethIndex; + uint256 usdxIndex; + uint256 wbtcIndex; } - struct Rates { + struct LocalInfo { uint96 daiBaseBorrowRate; uint96 wethBaseBorrowRate; uint96 usdxBaseBorrowRate; uint96 wbtcBaseBorrowRate; - } - - struct Indices { uint256 daiIndex; uint256 wethIndex; uint256 usdxIndex; uint256 wbtcIndex; - } - - struct BaseShares { - uint256 dai; - uint256 weth; - uint256 usdx; - uint256 wbtc; - } - - struct OriginalValues { - uint40 startTime; - uint256 wethIndex; - uint256 usdxIndex; - uint256 wbtcIndex; - } - - struct UpdatedTimestamps { - uint40 dai; - uint40 weth; - uint40 usdx; - uint40 wbtc; + uint256 daiBaseShares; + uint256 wethBaseShares; + uint256 usdxBaseShares; + uint256 wbtcBaseShares; + uint40 daiTimestamp; + uint40 wethTimestamp; + uint40 usdxTimestamp; + uint40 wbtcTimestamp; } function setUp() public override { @@ -75,10 +63,10 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { // Ensure bob does not draw more than half his normalized supply value amounts = _ensureSufficientCollateral(spoke2, amounts); - TestAmounts memory originalAmounts = _copyAmounts(amounts); // deep copy original amounts + TestAmounts memory originalValues = _copyAmounts(amounts); // deep copy original amounts + LocalInfo memory values; uint40 startTime = vm.getBlockTimestamp().toUint40(); - OriginalValues memory originalValues; originalValues.startTime = startTime; originalValues.wethIndex = hub1.getAssetDrawnIndex(wethAssetId); originalValues.usdxIndex = hub1.getAssetDrawnIndex(usdxAssetId); @@ -159,16 +147,15 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { assertEq(bobRp, _calculateExpectedUserRP(spoke2, bob), 'user risk premium Before'); // Store base borrow rates - Rates memory rates; - rates.daiBaseBorrowRate = hub1.getAssetDrawnRate(daiAssetId).toUint96(); - rates.wethBaseBorrowRate = hub1.getAssetDrawnRate(wethAssetId).toUint96(); - rates.usdxBaseBorrowRate = hub1.getAssetDrawnRate(usdxAssetId).toUint96(); - rates.wbtcBaseBorrowRate = hub1.getAssetDrawnRate(wbtcAssetId).toUint96(); + values.daiBaseBorrowRate = hub1.getAssetDrawnRate(daiAssetId).toUint96(); + values.wethBaseBorrowRate = hub1.getAssetDrawnRate(wethAssetId).toUint96(); + values.usdxBaseBorrowRate = hub1.getAssetDrawnRate(usdxAssetId).toUint96(); + values.wbtcBaseBorrowRate = hub1.getAssetDrawnRate(wbtcAssetId).toUint96(); // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level uint256 drawnDebt = _calculateExpectedDrawnDebt( amounts.daiBorrowAmount, - rates.daiBaseBorrowRate, + values.daiBaseBorrowRate, startTime ); _assertSingleUserProtocolDebt( @@ -192,7 +179,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { drawnDebt = _calculateExpectedDrawnDebt( amounts.wethBorrowAmount, - rates.wethBaseBorrowRate, + values.wethBaseBorrowRate, startTime ); _assertSingleUserProtocolDebt( @@ -216,7 +203,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { drawnDebt = _calculateExpectedDrawnDebt( amounts.usdxBorrowAmount, - rates.usdxBaseBorrowRate, + values.usdxBaseBorrowRate, startTime ); _assertSingleUserProtocolDebt( @@ -240,7 +227,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { drawnDebt = _calculateExpectedDrawnDebt( amounts.wbtcBorrowAmount, - rates.wbtcBaseBorrowRate, + values.wbtcBaseBorrowRate, startTime ); _assertSingleUserProtocolDebt( @@ -269,7 +256,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ISpoke.UserPosition memory bobPosition = spoke2.getUserPosition(_daiReserveId(spoke2), bob); drawnDebt = _calculateExpectedDrawnDebt( amounts.daiBorrowAmount, - rates.daiBaseBorrowRate, + values.daiBaseBorrowRate, startTime ); uint256 expectedPremiumDebt = _calculateExpectedPremiumDebt( @@ -317,7 +304,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { bobPosition = spoke2.getUserPosition(_wethReserveId(spoke2), bob); drawnDebt = _calculateExpectedDrawnDebt( amounts.wethBorrowAmount, - rates.wethBaseBorrowRate, + values.wethBaseBorrowRate, startTime ); expectedPremiumDebt = _calculateExpectedPremiumDebt(amounts.wethBorrowAmount, drawnDebt, bobRp); @@ -362,7 +349,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { bobPosition = spoke2.getUserPosition(_usdxReserveId(spoke2), bob); drawnDebt = _calculateExpectedDrawnDebt( amounts.usdxBorrowAmount, - rates.usdxBaseBorrowRate, + values.usdxBaseBorrowRate, startTime ); expectedPremiumDebt = _calculateExpectedPremiumDebt(amounts.usdxBorrowAmount, drawnDebt, bobRp); @@ -407,7 +394,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { bobPosition = spoke2.getUserPosition(_wbtcReserveId(spoke2), bob); drawnDebt = _calculateExpectedDrawnDebt( amounts.wbtcBorrowAmount, - rates.wbtcBaseBorrowRate, + values.wbtcBaseBorrowRate, startTime ); expectedPremiumDebt = _calculateExpectedPremiumDebt(amounts.wbtcBorrowAmount, drawnDebt, bobRp); @@ -466,6 +453,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { bob ); } + // Workaround for precision loss with RP calc: https://github.com/aave/aave-v4/issues/421 // Construct mock call so we can see the same user rp calc as within the borrow function vm.mockCall( @@ -486,26 +474,22 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { (amounts.wbtcBorrowAmount, ) = spoke2.getUserDebt(_wbtcReserveId(spoke2), bob); // Refresh base borrow rates - rates.daiBaseBorrowRate = hub1.getAssetDrawnRate(daiAssetId).toUint96(); - rates.wethBaseBorrowRate = hub1.getAssetDrawnRate(wethAssetId).toUint96(); - rates.usdxBaseBorrowRate = hub1.getAssetDrawnRate(usdxAssetId).toUint96(); - rates.wbtcBaseBorrowRate = hub1.getAssetDrawnRate(wbtcAssetId).toUint96(); + values.daiBaseBorrowRate = hub1.getAssetDrawnRate(daiAssetId).toUint96(); + values.wethBaseBorrowRate = hub1.getAssetDrawnRate(wethAssetId).toUint96(); + values.usdxBaseBorrowRate = hub1.getAssetDrawnRate(usdxAssetId).toUint96(); + values.wbtcBaseBorrowRate = hub1.getAssetDrawnRate(wbtcAssetId).toUint96(); // Refresh indexes - Indices memory indices; - indices.daiIndex = hub1.getAssetDrawnIndex(daiAssetId).toUint120(); - indices.wethIndex = hub1.getAssetDrawnIndex(wethAssetId).toUint120(); - indices.usdxIndex = hub1.getAssetDrawnIndex(usdxAssetId).toUint120(); - indices.wbtcIndex = hub1.getAssetDrawnIndex(wbtcAssetId).toUint120(); + values.daiIndex = hub1.getAssetDrawnIndex(daiAssetId).toUint120(); + values.wethIndex = hub1.getAssetDrawnIndex(wethAssetId).toUint120(); + values.usdxIndex = hub1.getAssetDrawnIndex(usdxAssetId).toUint120(); + values.wbtcIndex = hub1.getAssetDrawnIndex(wbtcAssetId).toUint120(); // Refresh timestamps - DAI may or may not have been borrowed above, potentially triggering accruals - UpdatedTimestamps memory timestamps; - timestamps.dai = hub1.getAsset(daiAssetId).lastUpdateTimestamp; - timestamps.weth = hub1.getAsset(wethAssetId).lastUpdateTimestamp; - timestamps.usdx = hub1.getAsset(usdxAssetId).lastUpdateTimestamp; - timestamps.wbtc = hub1.getAsset(wbtcAssetId).lastUpdateTimestamp; - - BaseShares memory baseShares; + values.daiTimestamp = hub1.getAsset(daiAssetId).lastUpdateTimestamp; + values.wethTimestamp = hub1.getAsset(wethAssetId).lastUpdateTimestamp; + values.usdxTimestamp = hub1.getAsset(usdxAssetId).lastUpdateTimestamp; + values.wbtcTimestamp = hub1.getAsset(wbtcAssetId).lastUpdateTimestamp; // Check debt values before accrual bobPosition = spoke2.getUserPosition(_daiReserveId(spoke2), bob); @@ -523,7 +507,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { expectedPremiumDebt, 'dai before second accrual' ); - baseShares.dai = bobPosition.drawnShares; + values.daiBaseShares = bobPosition.drawnShares; bobPosition = spoke2.getUserPosition(_wethReserveId(spoke2), bob); expectedPremiumDebt = _calculatePremiumDebt( @@ -540,7 +524,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { expectedPremiumDebt, 'weth before second accrual' ); - baseShares.weth = bobPosition.drawnShares; + values.wethBaseShares = bobPosition.drawnShares; bobPosition = spoke2.getUserPosition(_usdxReserveId(spoke2), bob); expectedPremiumDebt = _calculatePremiumDebt( @@ -557,7 +541,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { expectedPremiumDebt, 'usdx before second accrual' ); - baseShares.usdx = bobPosition.drawnShares; + values.usdxBaseShares = bobPosition.drawnShares; bobPosition = spoke2.getUserPosition(_wbtcReserveId(spoke2), bob); expectedPremiumDebt = _calculatePremiumDebt( @@ -574,11 +558,11 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { expectedPremiumDebt, 'wbtc before second accrual' ); - baseShares.wbtc = bobPosition.drawnShares; + values.wbtcBaseShares = bobPosition.drawnShares; assertEq( hub1.getAsset(wethAssetId).lastUpdateTimestamp, - timestamps.weth, + values.wethTimestamp, 'weth last update time before second accrual' ); @@ -588,18 +572,18 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { skip(skipTime); // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level - indices.daiIndex = _calculateExpectedDrawnIndex( - indices.daiIndex, - rates.daiBaseBorrowRate, - timestamps.dai + values.daiIndex = _calculateExpectedDrawnIndex( + values.daiIndex, + values.daiBaseBorrowRate, + values.daiTimestamp ); assertEq( - indices.daiIndex, + values.daiIndex, hub1.getAssetDrawnIndex(daiAssetId), 'dai drawn index after second accrual' ); bobPosition = spoke2.getUserPosition(_daiReserveId(spoke2), bob); - drawnDebt = baseShares.dai.rayMulUp(indices.daiIndex); + drawnDebt = values.daiBaseShares.rayMulUp(values.daiIndex); expectedPremiumDebt = _calculatePremiumDebt( hub1, daiAssetId, @@ -608,7 +592,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); interest = (drawnDebt + expectedPremiumDebt) - - (originalAmounts.daiBorrowAmount + 1e18) - + (originalValues.daiBorrowAmount + 1e18) - _calculateBurntInterest(hub1, daiAssetId); // subtract out the extra amount we borrowed _assertSingleUserProtocolDebt( spoke2, @@ -622,8 +606,8 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { spoke2, _daiReserveId(spoke2), bob, - originalAmounts.daiSupplyAmount + - (interest * originalAmounts.daiSupplyAmount) / + originalValues.daiSupplyAmount + + (interest * originalValues.daiSupplyAmount) / MAX_SUPPLY_AMOUNT, 'dai after second accrual' ); @@ -646,187 +630,193 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { 'dai after second accrual' ); - assertEq( - rates.wethBaseBorrowRate, - hub1.getAssetDrawnRate(wethAssetId), - 'weth drawn rate after second accrual' - ); + if (originalValues.wethBorrowAmount > 0) { + assertEq( + values.wethBaseBorrowRate, + hub1.getAssetDrawnRate(wethAssetId), + 'weth drawn rate after second accrual' + ); - assertEq( - hub1.getAsset(wethAssetId).lastUpdateTimestamp, - timestamps.weth, - 'weth last update time before second accrual' - ); + assertEq( + hub1.getAsset(wethAssetId).lastUpdateTimestamp, + values.wethTimestamp, + 'weth last update time before second accrual' + ); - indices.wethIndex = _calculateExpectedDrawnIndex( - indices.wethIndex, - rates.wethBaseBorrowRate, - timestamps.weth - ); + values.wethIndex = _calculateExpectedDrawnIndex( + values.wethTimestamp == 1 ? originalValues.wethIndex : values.wethIndex, // If weth never updated, use original index + values.wethBaseBorrowRate, + values.wethTimestamp + ); - assertEq( - indices.wethIndex.toUint120(), - hub1.getAssetDrawnIndex(wethAssetId), - 'weth drawn index after second accrual' - ); - bobPosition = spoke2.getUserPosition(_wethReserveId(spoke2), bob); - assertEq( - bobPosition.drawnShares, - baseShares.weth, - 'weth base drawn shares after second accrual' - ); - drawnDebt = baseShares.weth.rayMulUp(indices.wethIndex); - expectedPremiumDebt = _calculatePremiumDebt( - hub1, - wethAssetId, - bobPosition.premiumShares, - bobPosition.premiumOffsetRay - ); - interest = - (drawnDebt + expectedPremiumDebt) - - originalAmounts.wethBorrowAmount - - _calculateBurntInterest(hub1, wethAssetId); - _assertSingleUserProtocolDebt( - spoke2, - _wethReserveId(spoke2), - bob, - drawnDebt, - expectedPremiumDebt, - 'weth after second accrual' - ); - _assertUserSupply( - spoke2, - _wethReserveId(spoke2), - bob, - originalAmounts.wethSupplyAmount + - (interest * originalAmounts.wethSupplyAmount) / - MAX_SUPPLY_AMOUNT, - 'weth after second accrual' - ); - _assertReserveSupply( - spoke2, - _wethReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'weth after second accrual' - ); - _assertSpokeSupply( - spoke2, - _wethReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'weth after second accrual' - ); - _assertAssetSupply( - spoke2, - _wethReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'weth after second accrual' - ); + assertEq( + values.wethIndex.toUint120(), + hub1.getAssetDrawnIndex(wethAssetId), + 'weth drawn index after second accrual' + ); + bobPosition = spoke2.getUserPosition(_wethReserveId(spoke2), bob); + assertEq( + bobPosition.drawnShares, + values.wethBaseShares, + 'weth base drawn shares after second accrual' + ); + drawnDebt = values.wethBaseShares.rayMulUp(values.wethIndex); + expectedPremiumDebt = _calculatePremiumDebt( + hub1, + wethAssetId, + bobPosition.premiumShares, + bobPosition.premiumOffsetRay + ); + interest = + (drawnDebt + expectedPremiumDebt) - + originalValues.wethBorrowAmount - + _calculateBurntInterest(hub1, wethAssetId); + _assertSingleUserProtocolDebt( + spoke2, + _wethReserveId(spoke2), + bob, + drawnDebt, + expectedPremiumDebt, + 'weth after second accrual' + ); + _assertUserSupply( + spoke2, + _wethReserveId(spoke2), + bob, + originalValues.wethSupplyAmount + + (interest * originalValues.wethSupplyAmount) / + MAX_SUPPLY_AMOUNT, + 'weth after second accrual' + ); + _assertReserveSupply( + spoke2, + _wethReserveId(spoke2), + MAX_SUPPLY_AMOUNT + interest, + 'weth after second accrual' + ); + _assertSpokeSupply( + spoke2, + _wethReserveId(spoke2), + MAX_SUPPLY_AMOUNT + interest, + 'weth after second accrual' + ); + _assertAssetSupply( + spoke2, + _wethReserveId(spoke2), + MAX_SUPPLY_AMOUNT + interest, + 'weth after second accrual' + ); + } - indices.usdxIndex = _calculateExpectedDrawnIndex( - indices.usdxIndex, - rates.usdxBaseBorrowRate, - timestamps.usdx - ); - bobPosition = spoke2.getUserPosition(_usdxReserveId(spoke2), bob); - drawnDebt = baseShares.usdx.rayMulUp(indices.usdxIndex); - expectedPremiumDebt = _calculatePremiumDebt( - hub1, - usdxAssetId, - bobPosition.premiumShares, - bobPosition.premiumOffsetRay - ); - interest = - (drawnDebt + expectedPremiumDebt) - - originalAmounts.usdxBorrowAmount - - _calculateBurntInterest(hub1, usdxAssetId); - _assertSingleUserProtocolDebt( - spoke2, - _usdxReserveId(spoke2), - bob, - drawnDebt, - expectedPremiumDebt, - 'usdx after second accrual' - ); - _assertUserSupply( - spoke2, - _usdxReserveId(spoke2), - bob, - originalAmounts.usdxSupplyAmount + - (interest * originalAmounts.usdxSupplyAmount) / - MAX_SUPPLY_AMOUNT, - 'usdx after second accrual' - ); - _assertReserveSupply( - spoke2, - _usdxReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'usdx after second accrual' - ); - _assertSpokeSupply( - spoke2, - _usdxReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'usdx after second accrual' - ); - _assertAssetSupply( - spoke2, - _usdxReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'usdx after second accrual' - ); + if (originalValues.usdxBorrowAmount > 0) { + values.usdxIndex = _calculateExpectedDrawnIndex( + values.usdxTimestamp == 1 ? originalValues.usdxIndex : values.usdxIndex, // If usdx never updated, use original index + values.usdxBaseBorrowRate, + values.usdxTimestamp + ); + bobPosition = spoke2.getUserPosition(_usdxReserveId(spoke2), bob); + drawnDebt = values.usdxBaseShares.rayMulUp(values.usdxIndex); + expectedPremiumDebt = _calculatePremiumDebt( + hub1, + usdxAssetId, + bobPosition.premiumShares, + bobPosition.premiumOffsetRay + ); + interest = + (drawnDebt + expectedPremiumDebt) - + originalValues.usdxBorrowAmount - + _calculateBurntInterest(hub1, usdxAssetId); + _assertSingleUserProtocolDebt( + spoke2, + _usdxReserveId(spoke2), + bob, + drawnDebt, + expectedPremiumDebt, + 'usdx after second accrual' + ); + _assertUserSupply( + spoke2, + _usdxReserveId(spoke2), + bob, + originalValues.usdxSupplyAmount + + (interest * originalValues.usdxSupplyAmount) / + MAX_SUPPLY_AMOUNT, + 'usdx after second accrual' + ); + _assertReserveSupply( + spoke2, + _usdxReserveId(spoke2), + MAX_SUPPLY_AMOUNT + interest, + 'usdx after second accrual' + ); + _assertSpokeSupply( + spoke2, + _usdxReserveId(spoke2), + MAX_SUPPLY_AMOUNT + interest, + 'usdx after second accrual' + ); + _assertAssetSupply( + spoke2, + _usdxReserveId(spoke2), + MAX_SUPPLY_AMOUNT + interest, + 'usdx after second accrual' + ); + } - indices.wbtcIndex = _calculateExpectedDrawnIndex( - indices.wbtcIndex, - rates.wbtcBaseBorrowRate, - timestamps.wbtc - ); - bobPosition = spoke2.getUserPosition(_wbtcReserveId(spoke2), bob); - drawnDebt = baseShares.wbtc.rayMulUp(indices.wbtcIndex); - expectedPremiumDebt = _calculatePremiumDebt( - hub1, - wbtcAssetId, - bobPosition.premiumShares, - bobPosition.premiumOffsetRay - ); - interest = - (drawnDebt + expectedPremiumDebt) - - originalAmounts.wbtcBorrowAmount - - _calculateBurntInterest(hub1, wbtcAssetId); - _assertSingleUserProtocolDebt( - spoke2, - _wbtcReserveId(spoke2), - bob, - drawnDebt, - expectedPremiumDebt, - 'wbtc after second accrual' - ); - _assertUserSupply( - spoke2, - _wbtcReserveId(spoke2), - bob, - originalAmounts.wbtcSupplyAmount + - (interest * originalAmounts.wbtcSupplyAmount) / - MAX_SUPPLY_AMOUNT, - 'wbtc after second accrual' - ); - _assertReserveSupply( - spoke2, - _wbtcReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'wbtc after second accrual' - ); - _assertSpokeSupply( - spoke2, - _wbtcReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'wbtc after second accrual' - ); - _assertAssetSupply( - spoke2, - _wbtcReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'wbtc after second accrual' - ); + if (originalValues.wbtcBorrowAmount > 0) { + values.wbtcIndex = _calculateExpectedDrawnIndex( + values.wbtcTimestamp == 1 ? originalValues.wbtcIndex : values.wbtcIndex, // If wbtc never updated, use original index + values.wbtcBaseBorrowRate, + values.wbtcTimestamp + ); + bobPosition = spoke2.getUserPosition(_wbtcReserveId(spoke2), bob); + drawnDebt = values.wbtcBaseShares.rayMulUp(values.wbtcIndex); + expectedPremiumDebt = _calculatePremiumDebt( + hub1, + wbtcAssetId, + bobPosition.premiumShares, + bobPosition.premiumOffsetRay + ); + interest = + (drawnDebt + expectedPremiumDebt) - + originalValues.wbtcBorrowAmount - + _calculateBurntInterest(hub1, wbtcAssetId); + _assertSingleUserProtocolDebt( + spoke2, + _wbtcReserveId(spoke2), + bob, + drawnDebt, + expectedPremiumDebt, + 'wbtc after second accrual' + ); + _assertUserSupply( + spoke2, + _wbtcReserveId(spoke2), + bob, + originalValues.wbtcSupplyAmount + + (interest * originalValues.wbtcSupplyAmount) / + MAX_SUPPLY_AMOUNT, + 'wbtc after second accrual' + ); + _assertReserveSupply( + spoke2, + _wbtcReserveId(spoke2), + MAX_SUPPLY_AMOUNT + interest, + 'wbtc after second accrual' + ); + _assertSpokeSupply( + spoke2, + _wbtcReserveId(spoke2), + MAX_SUPPLY_AMOUNT + interest, + 'wbtc after second accrual' + ); + _assertAssetSupply( + spoke2, + _wbtcReserveId(spoke2), + MAX_SUPPLY_AMOUNT + interest, + 'wbtc after second accrual' + ); + } } } @@ -843,7 +833,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { return amounts; } - function _bound(Rates memory rates) internal view returns (Rates memory) { + function _bound(LocalInfo memory rates) internal view returns (LocalInfo memory) { rates.daiBaseBorrowRate = _bpsToRay( bound(rates.daiBaseBorrowRate, 1, irStrategy.MAX_BORROW_RATE()) ).toUint96(); @@ -930,7 +920,11 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { daiBorrowAmount: amounts.daiBorrowAmount, wethBorrowAmount: amounts.wethBorrowAmount, usdxBorrowAmount: amounts.usdxBorrowAmount, - wbtcBorrowAmount: amounts.wbtcBorrowAmount + wbtcBorrowAmount: amounts.wbtcBorrowAmount, + startTime: 0, + wethIndex: 0, + usdxIndex: 0, + wbtcIndex: 0 }); } } From b6b90af1cd2ec7754b7ddc25739d5984808a331b Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Mon, 8 Dec 2025 20:25:26 +0900 Subject: [PATCH 05/15] fix: Remove debugging code --- .../Spoke/Spoke.AccrueInterest.Scenario.t.sol | 41 ++----------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index 59a812afb..64ccadf61 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -8,7 +8,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { using SharesMath for uint256; using WadRayMath for *; using PercentageMath for uint256; - using SafeCast for *; + using SafeCast for uint256; struct TestAmounts { uint256 daiSupplyAmount; @@ -59,7 +59,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { uint40 skipTime ) public { amounts = _bound(amounts); - skipTime = bound(skipTime, 0, 2000 days).toUint40(); + skipTime = bound(skipTime, 0, MAX_SKIP_TIME / 2).toUint40(); // Ensure bob does not draw more than half his normalized supply value amounts = _ensureSufficientCollateral(spoke2, amounts); @@ -453,7 +453,6 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { bob ); } - // Workaround for precision loss with RP calc: https://github.com/aave/aave-v4/issues/421 // Construct mock call so we can see the same user rp calc as within the borrow function vm.mockCall( @@ -560,15 +559,9 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); values.wbtcBaseShares = bobPosition.drawnShares; - assertEq( - hub1.getAsset(wethAssetId).lastUpdateTimestamp, - values.wethTimestamp, - 'weth last update time before second accrual' - ); - // Store timestamp before next skip time startTime = vm.getBlockTimestamp().toUint40(); - skipTime = randomizer(0, 1000 days).toUint40(); + skipTime = randomizer(0, MAX_SKIP_TIME / 2).toUint40(); skip(skipTime); // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level @@ -577,11 +570,6 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { values.daiBaseBorrowRate, values.daiTimestamp ); - assertEq( - values.daiIndex, - hub1.getAssetDrawnIndex(daiAssetId), - 'dai drawn index after second accrual' - ); bobPosition = spoke2.getUserPosition(_daiReserveId(spoke2), bob); drawnDebt = values.daiBaseShares.rayMulUp(values.daiIndex); expectedPremiumDebt = _calculatePremiumDebt( @@ -631,35 +619,12 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); if (originalValues.wethBorrowAmount > 0) { - assertEq( - values.wethBaseBorrowRate, - hub1.getAssetDrawnRate(wethAssetId), - 'weth drawn rate after second accrual' - ); - - assertEq( - hub1.getAsset(wethAssetId).lastUpdateTimestamp, - values.wethTimestamp, - 'weth last update time before second accrual' - ); - values.wethIndex = _calculateExpectedDrawnIndex( values.wethTimestamp == 1 ? originalValues.wethIndex : values.wethIndex, // If weth never updated, use original index values.wethBaseBorrowRate, values.wethTimestamp ); - - assertEq( - values.wethIndex.toUint120(), - hub1.getAssetDrawnIndex(wethAssetId), - 'weth drawn index after second accrual' - ); bobPosition = spoke2.getUserPosition(_wethReserveId(spoke2), bob); - assertEq( - bobPosition.drawnShares, - values.wethBaseShares, - 'weth base drawn shares after second accrual' - ); drawnDebt = values.wethBaseShares.rayMulUp(values.wethIndex); expectedPremiumDebt = _calculatePremiumDebt( hub1, From 1cef0051ab770c3da31e287c1f6653921fa7790b Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Mon, 8 Dec 2025 20:31:51 +0900 Subject: [PATCH 06/15] fix: Remove old workaround for rp imprecision --- tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index 64ccadf61..7954dc514 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -453,19 +453,12 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { bob ); } - // Workaround for precision loss with RP calc: https://github.com/aave/aave-v4/issues/421 - // Construct mock call so we can see the same user rp calc as within the borrow function - vm.mockCall( - address(spoke2), - abi.encodeCall(Spoke.getUserTotalDebt, (_daiReserveId(spoke2), bob)), - abi.encode(spoke2.getUserTotalDebt(_daiReserveId(spoke2), bob) + 1e18) // Debt amount seen in the borrow function when calculating user rp - ); - bobRp = _calculateExpectedUserRP(spoke2, bob); - vm.clearMockedCalls(); // Bob borrows more dai to trigger accrual Utils.borrow(spoke2, _daiReserveId(spoke2), bob, 1e18, bob); + bobRp = _calculateExpectedUserRP(spoke2, bob); + // Refresh debt values (amounts.daiBorrowAmount, ) = spoke2.getUserDebt(_daiReserveId(spoke2), bob); (amounts.wethBorrowAmount, ) = spoke2.getUserDebt(_wethReserveId(spoke2), bob); From eb73921ca6ba6e6c1bca1a262f431c9c32888201 Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Mon, 8 Dec 2025 20:39:04 +0900 Subject: [PATCH 07/15] style: Variable placement --- tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index 7954dc514..3b9f754da 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -64,7 +64,6 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { // Ensure bob does not draw more than half his normalized supply value amounts = _ensureSufficientCollateral(spoke2, amounts); TestAmounts memory originalValues = _copyAmounts(amounts); // deep copy original amounts - LocalInfo memory values; uint40 startTime = vm.getBlockTimestamp().toUint40(); originalValues.startTime = startTime; @@ -147,6 +146,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { assertEq(bobRp, _calculateExpectedUserRP(spoke2, bob), 'user risk premium Before'); // Store base borrow rates + LocalInfo memory values; values.daiBaseBorrowRate = hub1.getAssetDrawnRate(daiAssetId).toUint96(); values.wethBaseBorrowRate = hub1.getAssetDrawnRate(wethAssetId).toUint96(); values.usdxBaseBorrowRate = hub1.getAssetDrawnRate(usdxAssetId).toUint96(); From ce719fb7d8515ff6d90950f7b687743311df144e Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Tue, 9 Dec 2025 18:01:40 +0900 Subject: [PATCH 08/15] fix: snapshots --- snapshots/Hub.Operations.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snapshots/Hub.Operations.json b/snapshots/Hub.Operations.json index 80f27df18..54b1b87c4 100644 --- a/snapshots/Hub.Operations.json +++ b/snapshots/Hub.Operations.json @@ -11,8 +11,8 @@ "remove: partial": "81640", "reportDeficit": "115225", "restore: full": "80471", - "restore: full - with transfer": "165877", + "restore: full - with transfer": "173377", "restore: partial": "89137", - "restore: partial - with transfer": "144900", + "restore: partial - with transfer": "147400", "transferShares": "71192" } \ No newline at end of file From 5985de605dd6337bd2607d3598cd99776912334f Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Tue, 9 Dec 2025 18:07:11 +0900 Subject: [PATCH 09/15] fix: Change variable name --- .../Spoke/Spoke.AccrueInterest.Scenario.t.sol | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index 3b9f754da..cd5ebcf8e 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -63,13 +63,13 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { // Ensure bob does not draw more than half his normalized supply value amounts = _ensureSufficientCollateral(spoke2, amounts); - TestAmounts memory originalValues = _copyAmounts(amounts); // deep copy original amounts + TestAmounts memory originalAmounts = _copyAmounts(amounts); // deep copy original amounts uint40 startTime = vm.getBlockTimestamp().toUint40(); - originalValues.startTime = startTime; - originalValues.wethIndex = hub1.getAssetDrawnIndex(wethAssetId); - originalValues.usdxIndex = hub1.getAssetDrawnIndex(usdxAssetId); - originalValues.wbtcIndex = hub1.getAssetDrawnIndex(wbtcAssetId); + originalAmounts.startTime = startTime; + originalAmounts.wethIndex = hub1.getAssetDrawnIndex(wethAssetId); + originalAmounts.usdxIndex = hub1.getAssetDrawnIndex(usdxAssetId); + originalAmounts.wbtcIndex = hub1.getAssetDrawnIndex(wbtcAssetId); // Bob supply dai on spoke 2 if (amounts.daiSupplyAmount > 0) { @@ -573,7 +573,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); interest = (drawnDebt + expectedPremiumDebt) - - (originalValues.daiBorrowAmount + 1e18) - + (originalAmounts.daiBorrowAmount + 1e18) - _calculateBurntInterest(hub1, daiAssetId); // subtract out the extra amount we borrowed _assertSingleUserProtocolDebt( spoke2, @@ -587,8 +587,8 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { spoke2, _daiReserveId(spoke2), bob, - originalValues.daiSupplyAmount + - (interest * originalValues.daiSupplyAmount) / + originalAmounts.daiSupplyAmount + + (interest * originalAmounts.daiSupplyAmount) / MAX_SUPPLY_AMOUNT, 'dai after second accrual' ); @@ -611,9 +611,9 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { 'dai after second accrual' ); - if (originalValues.wethBorrowAmount > 0) { + if (originalAmounts.wethBorrowAmount > 0) { values.wethIndex = _calculateExpectedDrawnIndex( - values.wethTimestamp == 1 ? originalValues.wethIndex : values.wethIndex, // If weth never updated, use original index + values.wethTimestamp == 1 ? originalAmounts.wethIndex : values.wethIndex, // If weth never updated, use original index values.wethBaseBorrowRate, values.wethTimestamp ); @@ -627,7 +627,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); interest = (drawnDebt + expectedPremiumDebt) - - originalValues.wethBorrowAmount - + originalAmounts.wethBorrowAmount - _calculateBurntInterest(hub1, wethAssetId); _assertSingleUserProtocolDebt( spoke2, @@ -641,8 +641,8 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { spoke2, _wethReserveId(spoke2), bob, - originalValues.wethSupplyAmount + - (interest * originalValues.wethSupplyAmount) / + originalAmounts.wethSupplyAmount + + (interest * originalAmounts.wethSupplyAmount) / MAX_SUPPLY_AMOUNT, 'weth after second accrual' ); @@ -666,9 +666,9 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); } - if (originalValues.usdxBorrowAmount > 0) { + if (originalAmounts.usdxBorrowAmount > 0) { values.usdxIndex = _calculateExpectedDrawnIndex( - values.usdxTimestamp == 1 ? originalValues.usdxIndex : values.usdxIndex, // If usdx never updated, use original index + values.usdxTimestamp == 1 ? originalAmounts.usdxIndex : values.usdxIndex, // If usdx never updated, use original index values.usdxBaseBorrowRate, values.usdxTimestamp ); @@ -682,7 +682,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); interest = (drawnDebt + expectedPremiumDebt) - - originalValues.usdxBorrowAmount - + originalAmounts.usdxBorrowAmount - _calculateBurntInterest(hub1, usdxAssetId); _assertSingleUserProtocolDebt( spoke2, @@ -696,8 +696,8 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { spoke2, _usdxReserveId(spoke2), bob, - originalValues.usdxSupplyAmount + - (interest * originalValues.usdxSupplyAmount) / + originalAmounts.usdxSupplyAmount + + (interest * originalAmounts.usdxSupplyAmount) / MAX_SUPPLY_AMOUNT, 'usdx after second accrual' ); @@ -721,9 +721,9 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); } - if (originalValues.wbtcBorrowAmount > 0) { + if (originalAmounts.wbtcBorrowAmount > 0) { values.wbtcIndex = _calculateExpectedDrawnIndex( - values.wbtcTimestamp == 1 ? originalValues.wbtcIndex : values.wbtcIndex, // If wbtc never updated, use original index + values.wbtcTimestamp == 1 ? originalAmounts.wbtcIndex : values.wbtcIndex, // If wbtc never updated, use original index values.wbtcBaseBorrowRate, values.wbtcTimestamp ); @@ -737,7 +737,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { ); interest = (drawnDebt + expectedPremiumDebt) - - originalValues.wbtcBorrowAmount - + originalAmounts.wbtcBorrowAmount - _calculateBurntInterest(hub1, wbtcAssetId); _assertSingleUserProtocolDebt( spoke2, @@ -751,8 +751,8 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { spoke2, _wbtcReserveId(spoke2), bob, - originalValues.wbtcSupplyAmount + - (interest * originalValues.wbtcSupplyAmount) / + originalAmounts.wbtcSupplyAmount + + (interest * originalAmounts.wbtcSupplyAmount) / MAX_SUPPLY_AMOUNT, 'wbtc after second accrual' ); From cb3fb9328c933b7cc630cf7877153cadc2788295 Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Wed, 17 Dec 2025 00:26:19 +0900 Subject: [PATCH 10/15] chore: Cleanup test using loops --- .../Spoke/Spoke.AccrueInterest.Scenario.t.sol | 883 +++++------------- 1 file changed, 224 insertions(+), 659 deletions(-) diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index cd5ebcf8e..aec5c5640 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -25,23 +25,23 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { uint256 wbtcIndex; } - struct LocalInfo { - uint96 daiBaseBorrowRate; - uint96 wethBaseBorrowRate; - uint96 usdxBaseBorrowRate; - uint96 wbtcBaseBorrowRate; - uint256 daiIndex; - uint256 wethIndex; - uint256 usdxIndex; - uint256 wbtcIndex; - uint256 daiBaseShares; - uint256 wethBaseShares; - uint256 usdxBaseShares; - uint256 wbtcBaseShares; - uint40 daiTimestamp; - uint40 wethTimestamp; - uint40 usdxTimestamp; - uint40 wbtcTimestamp; + struct TestAmount { + uint256 supplyAmount; + uint256 borrowAmount; + uint256 originalSupplyAmount; + uint256 originalBorrowAmount; + uint256 index; + uint256 originalIndex; + uint256 reserveId; + uint256 assetId; + string name; + } + + struct TestInfo { + uint96 baseBorrowRate; + uint256 index; + uint256 baseShares; + uint40 timestamp; } function setUp() public override { @@ -63,82 +63,89 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { // Ensure bob does not draw more than half his normalized supply value amounts = _ensureSufficientCollateral(spoke2, amounts); - TestAmounts memory originalAmounts = _copyAmounts(amounts); // deep copy original amounts - - uint40 startTime = vm.getBlockTimestamp().toUint40(); - originalAmounts.startTime = startTime; - originalAmounts.wethIndex = hub1.getAssetDrawnIndex(wethAssetId); - originalAmounts.usdxIndex = hub1.getAssetDrawnIndex(usdxAssetId); - originalAmounts.wbtcIndex = hub1.getAssetDrawnIndex(wbtcAssetId); - - // Bob supply dai on spoke 2 - if (amounts.daiSupplyAmount > 0) { - Utils.supplyCollateral(spoke2, _daiReserveId(spoke2), bob, amounts.daiSupplyAmount, bob); - } - - // Bob supply weth on spoke 2 - if (amounts.wethSupplyAmount > 0) { - Utils.supplyCollateral(spoke2, _wethReserveId(spoke2), bob, amounts.wethSupplyAmount, bob); - } - - // Bob supply usdx on spoke 2 - if (amounts.usdxSupplyAmount > 0) { - Utils.supplyCollateral(spoke2, _usdxReserveId(spoke2), bob, amounts.usdxSupplyAmount, bob); - } - - // Bob supply wbtc on spoke 2 - if (amounts.wbtcSupplyAmount > 0) { - Utils.supplyCollateral(spoke2, _wbtcReserveId(spoke2), bob, amounts.wbtcSupplyAmount, bob); - } - - // Deploy remainder of liquidity - if (amounts.daiSupplyAmount < MAX_SUPPLY_AMOUNT) { - _openSupplyPosition( - spoke2, - _daiReserveId(spoke2), - MAX_SUPPLY_AMOUNT - amounts.daiSupplyAmount - ); - } - if (amounts.wethSupplyAmount < MAX_SUPPLY_AMOUNT) { - _openSupplyPosition( - spoke2, - _wethReserveId(spoke2), - MAX_SUPPLY_AMOUNT - amounts.wethSupplyAmount - ); - } - if (amounts.usdxSupplyAmount < MAX_SUPPLY_AMOUNT) { - _openSupplyPosition( - spoke2, - _usdxReserveId(spoke2), - MAX_SUPPLY_AMOUNT - amounts.usdxSupplyAmount - ); - } - if (amounts.wbtcSupplyAmount < MAX_SUPPLY_AMOUNT) { - _openSupplyPosition( - spoke2, - _wbtcReserveId(spoke2), - MAX_SUPPLY_AMOUNT - amounts.wbtcSupplyAmount - ); - } - // Bob borrows dai from spoke 2 - if (amounts.daiBorrowAmount > 0) { - Utils.borrow(spoke2, _daiReserveId(spoke2), bob, amounts.daiBorrowAmount, bob); + // 1 -> DAI, 2 -> WETH, 3 -> USDx, 4 -> WBTC + TestAmount[] memory testAmounts = new TestAmount[](4); + for (uint256 i = 0; i < 4; ++i) { + if (i == 0) { + testAmounts[i] = TestAmount({ + supplyAmount: amounts.daiSupplyAmount, + borrowAmount: amounts.daiBorrowAmount, + originalSupplyAmount: amounts.daiSupplyAmount, + originalBorrowAmount: amounts.daiBorrowAmount, + index: hub1.getAssetDrawnIndex(daiAssetId), + originalIndex: hub1.getAssetDrawnIndex(daiAssetId), + reserveId: _daiReserveId(spoke2), + assetId: daiAssetId, + name: 'DAI' + }); + } else if (i == 1) { + testAmounts[i] = TestAmount({ + supplyAmount: amounts.wethSupplyAmount, + borrowAmount: amounts.wethBorrowAmount, + originalSupplyAmount: amounts.wethSupplyAmount, + originalBorrowAmount: amounts.wethBorrowAmount, + index: hub1.getAssetDrawnIndex(wethAssetId), + originalIndex: hub1.getAssetDrawnIndex(wethAssetId), + reserveId: _wethReserveId(spoke2), + assetId: wethAssetId, + name: 'WETH' + }); + } else if (i == 2) { + testAmounts[i] = TestAmount({ + supplyAmount: amounts.usdxSupplyAmount, + borrowAmount: amounts.usdxBorrowAmount, + originalSupplyAmount: amounts.usdxSupplyAmount, + originalBorrowAmount: amounts.usdxBorrowAmount, + index: hub1.getAssetDrawnIndex(usdxAssetId), + originalIndex: hub1.getAssetDrawnIndex(usdxAssetId), + reserveId: _usdxReserveId(spoke2), + assetId: usdxAssetId, + name: 'USDX' + }); + } else { + testAmounts[i] = TestAmount({ + supplyAmount: amounts.wbtcSupplyAmount, + borrowAmount: amounts.wbtcBorrowAmount, + originalSupplyAmount: amounts.wbtcSupplyAmount, + originalBorrowAmount: amounts.wbtcBorrowAmount, + index: hub1.getAssetDrawnIndex(wbtcAssetId), + originalIndex: hub1.getAssetDrawnIndex(wbtcAssetId), + reserveId: _wbtcReserveId(spoke2), + assetId: wbtcAssetId, + name: 'WBTC' + }); + } } - // Bob borrows weth from spoke 2 - if (amounts.wethBorrowAmount > 0) { - Utils.borrow(spoke2, _wethReserveId(spoke2), bob, amounts.wethBorrowAmount, bob); - } + uint40 startTime = vm.getBlockTimestamp().toUint40(); - // Bob borrows usdx from spoke 2 - if (amounts.usdxBorrowAmount > 0) { - Utils.borrow(spoke2, _usdxReserveId(spoke2), bob, amounts.usdxBorrowAmount, bob); + // Bob supplies amounts on spoke 2, then we deploy remainder of liquidity + for (uint256 i = 0; i < 4; ++i) { + if (testAmounts[i].supplyAmount > 0) { + Utils.supplyCollateral( + spoke2, + testAmounts[i].reserveId, + bob, + testAmounts[i].supplyAmount, + bob + ); + } + // Deploy remainder of liquidity for each asset + if (testAmounts[i].supplyAmount < MAX_SUPPLY_AMOUNT) { + _openSupplyPosition( + spoke2, + testAmounts[i].reserveId, + MAX_SUPPLY_AMOUNT - testAmounts[i].supplyAmount + ); + } } - // Bob borrows wbtc from spoke 2 - if (amounts.wbtcBorrowAmount > 0) { - Utils.borrow(spoke2, _wbtcReserveId(spoke2), bob, amounts.wbtcBorrowAmount, bob); + // Bob borrows amounts from spoke 2 + for (uint256 i = 0; i < 4; ++i) { + if (testAmounts[i].borrowAmount > 0) { + Utils.borrow(spoke2, testAmounts[i].reserveId, bob, testAmounts[i].borrowAmount, bob); + } } // Check Bob's risk premium @@ -146,633 +153,227 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { assertEq(bobRp, _calculateExpectedUserRP(spoke2, bob), 'user risk premium Before'); // Store base borrow rates - LocalInfo memory values; - values.daiBaseBorrowRate = hub1.getAssetDrawnRate(daiAssetId).toUint96(); - values.wethBaseBorrowRate = hub1.getAssetDrawnRate(wethAssetId).toUint96(); - values.usdxBaseBorrowRate = hub1.getAssetDrawnRate(usdxAssetId).toUint96(); - values.wbtcBaseBorrowRate = hub1.getAssetDrawnRate(wbtcAssetId).toUint96(); - - // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level - uint256 drawnDebt = _calculateExpectedDrawnDebt( - amounts.daiBorrowAmount, - values.daiBaseBorrowRate, - startTime - ); - _assertSingleUserProtocolDebt( - spoke2, - _daiReserveId(spoke2), - bob, - drawnDebt, - 0, - 'dai before accrual' - ); - _assertUserSupply( - spoke2, - _daiReserveId(spoke2), - bob, - amounts.daiSupplyAmount, - 'dai before accrual' - ); - _assertReserveSupply(spoke2, _daiReserveId(spoke2), MAX_SUPPLY_AMOUNT, 'dai before accrual'); - _assertSpokeSupply(spoke2, _daiReserveId(spoke2), MAX_SUPPLY_AMOUNT, 'dai before accrual'); - _assertAssetSupply(spoke2, _daiReserveId(spoke2), MAX_SUPPLY_AMOUNT, 'dai before accrual'); - - drawnDebt = _calculateExpectedDrawnDebt( - amounts.wethBorrowAmount, - values.wethBaseBorrowRate, - startTime - ); - _assertSingleUserProtocolDebt( - spoke2, - _wethReserveId(spoke2), - bob, - drawnDebt, - 0, - 'weth before accrual' - ); - _assertUserSupply( - spoke2, - _wethReserveId(spoke2), - bob, - amounts.wethSupplyAmount, - 'weth before accrual' - ); - _assertReserveSupply(spoke2, _wethReserveId(spoke2), MAX_SUPPLY_AMOUNT, 'weth before accrual'); - _assertSpokeSupply(spoke2, _wethReserveId(spoke2), MAX_SUPPLY_AMOUNT, 'weth before accrual'); - _assertAssetSupply(spoke2, _wethReserveId(spoke2), MAX_SUPPLY_AMOUNT, 'weth before accrual'); - - drawnDebt = _calculateExpectedDrawnDebt( - amounts.usdxBorrowAmount, - values.usdxBaseBorrowRate, - startTime - ); - _assertSingleUserProtocolDebt( - spoke2, - _usdxReserveId(spoke2), - bob, - drawnDebt, - 0, - 'usdx before accrual' - ); - _assertUserSupply( - spoke2, - _usdxReserveId(spoke2), - bob, - amounts.usdxSupplyAmount, - 'usdx before accrual' - ); - _assertReserveSupply(spoke2, _usdxReserveId(spoke2), MAX_SUPPLY_AMOUNT, 'usdx before accrual'); - _assertSpokeSupply(spoke2, _usdxReserveId(spoke2), MAX_SUPPLY_AMOUNT, 'usdx before accrual'); - _assertAssetSupply(spoke2, _usdxReserveId(spoke2), MAX_SUPPLY_AMOUNT, 'usdx before accrual'); - - drawnDebt = _calculateExpectedDrawnDebt( - amounts.wbtcBorrowAmount, - values.wbtcBaseBorrowRate, - startTime - ); - _assertSingleUserProtocolDebt( - spoke2, - _wbtcReserveId(spoke2), - bob, - drawnDebt, - 0, - 'wbtc before accrual' - ); - _assertUserSupply( - spoke2, - _wbtcReserveId(spoke2), - bob, - amounts.wbtcSupplyAmount, - 'wbtc before accrual' - ); - _assertReserveSupply(spoke2, _wbtcReserveId(spoke2), MAX_SUPPLY_AMOUNT, 'wbtc before accrual'); - _assertSpokeSupply(spoke2, _wbtcReserveId(spoke2), MAX_SUPPLY_AMOUNT, 'wbtc before accrual'); - _assertAssetSupply(spoke2, _wbtcReserveId(spoke2), MAX_SUPPLY_AMOUNT, 'wbtc before accrual'); - - // Skip time to accrue interest - skip(skipTime); + TestInfo[] memory values = new TestInfo[](4); + for (uint256 i = 0; i < 4; ++i) { + values[i].baseBorrowRate = hub1.getAssetDrawnRate(testAmounts[i].assetId).toUint96(); + } // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level - ISpoke.UserPosition memory bobPosition = spoke2.getUserPosition(_daiReserveId(spoke2), bob); - drawnDebt = _calculateExpectedDrawnDebt( - amounts.daiBorrowAmount, - values.daiBaseBorrowRate, - startTime - ); - uint256 expectedPremiumDebt = _calculateExpectedPremiumDebt( - amounts.daiBorrowAmount, - drawnDebt, - bobRp - ); - uint256 interest = (drawnDebt + expectedPremiumDebt) - - amounts.daiBorrowAmount - - _calculateBurntInterest(hub1, daiAssetId); - _assertSingleUserProtocolDebt( - spoke2, - _daiReserveId(spoke2), - bob, - drawnDebt, - expectedPremiumDebt, - 'dai after accrual' - ); - _assertUserSupply( - spoke2, - _daiReserveId(spoke2), - bob, - amounts.daiSupplyAmount + (interest * amounts.daiSupplyAmount) / MAX_SUPPLY_AMOUNT, - 'dai after accrual' - ); - _assertReserveSupply( - spoke2, - _daiReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'dai after accrual' - ); - _assertSpokeSupply( - spoke2, - _daiReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'dai after accrual' - ); - _assertAssetSupply( - spoke2, - _daiReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'dai after accrual' - ); - - bobPosition = spoke2.getUserPosition(_wethReserveId(spoke2), bob); - drawnDebt = _calculateExpectedDrawnDebt( - amounts.wethBorrowAmount, - values.wethBaseBorrowRate, - startTime - ); - expectedPremiumDebt = _calculateExpectedPremiumDebt(amounts.wethBorrowAmount, drawnDebt, bobRp); - interest = - (drawnDebt + expectedPremiumDebt) - - amounts.wethBorrowAmount - - _calculateBurntInterest(hub1, wethAssetId); - _assertSingleUserProtocolDebt( - spoke2, - _wethReserveId(spoke2), - bob, - drawnDebt, - expectedPremiumDebt, - 'weth after accrual' - ); - _assertUserSupply( - spoke2, - _wethReserveId(spoke2), - bob, - amounts.wethSupplyAmount + (interest * amounts.wethSupplyAmount) / MAX_SUPPLY_AMOUNT, - 'weth after accrual' - ); - _assertReserveSupply( - spoke2, - _wethReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'weth after accrual' - ); - _assertSpokeSupply( - spoke2, - _wethReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'weth after accrual' - ); - _assertAssetSupply( - spoke2, - _wethReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'weth after accrual' - ); - - bobPosition = spoke2.getUserPosition(_usdxReserveId(spoke2), bob); - drawnDebt = _calculateExpectedDrawnDebt( - amounts.usdxBorrowAmount, - values.usdxBaseBorrowRate, - startTime - ); - expectedPremiumDebt = _calculateExpectedPremiumDebt(amounts.usdxBorrowAmount, drawnDebt, bobRp); - interest = - (drawnDebt + expectedPremiumDebt) - - amounts.usdxBorrowAmount - - _calculateBurntInterest(hub1, usdxAssetId); - _assertSingleUserProtocolDebt( - spoke2, - _usdxReserveId(spoke2), - bob, - drawnDebt, - expectedPremiumDebt, - 'usdx after accrual' - ); - _assertUserSupply( - spoke2, - _usdxReserveId(spoke2), - bob, - amounts.usdxSupplyAmount + (interest * amounts.usdxSupplyAmount) / MAX_SUPPLY_AMOUNT, - 'usdx after accrual' - ); - _assertReserveSupply( - spoke2, - _usdxReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'usdx after accrual' - ); - _assertSpokeSupply( - spoke2, - _usdxReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'usdx after accrual' - ); - _assertAssetSupply( - spoke2, - _usdxReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'usdx after accrual' - ); - - bobPosition = spoke2.getUserPosition(_wbtcReserveId(spoke2), bob); - drawnDebt = _calculateExpectedDrawnDebt( - amounts.wbtcBorrowAmount, - values.wbtcBaseBorrowRate, - startTime - ); - expectedPremiumDebt = _calculateExpectedPremiumDebt(amounts.wbtcBorrowAmount, drawnDebt, bobRp); - interest = - (drawnDebt + expectedPremiumDebt) - - amounts.wbtcBorrowAmount - - _calculateBurntInterest(hub1, wbtcAssetId); - _assertSingleUserProtocolDebt( - spoke2, - _wbtcReserveId(spoke2), - bob, - drawnDebt, - expectedPremiumDebt, - 'wbtc after accrual' - ); - _assertUserSupply( - spoke2, - _wbtcReserveId(spoke2), - bob, - amounts.wbtcSupplyAmount + (interest * amounts.wbtcSupplyAmount) / MAX_SUPPLY_AMOUNT, - 'wbtc after accrual' - ); - _assertReserveSupply( - spoke2, - _wbtcReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'wbtc after accrual' - ); - _assertSpokeSupply( - spoke2, - _wbtcReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'wbtc after accrual' - ); - _assertAssetSupply( - spoke2, - _wbtcReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'wbtc after accrual' - ); - - // Only proceed with test if position is healthy - if (_getUserHealthFactor(spoke2, bob) >= HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { - // Supply more collateral to ensure bob can borrow more dai to trigger accrual - deal(address(tokenList.dai), bob, MAX_SUPPLY_AMOUNT); - Utils.supplyCollateral(spoke2, _usdzReserveId(spoke2), bob, MAX_SUPPLY_AMOUNT, bob); - - // Handle case that bob isn't already borrowing dai by borrowing 1 share - bobPosition = spoke2.getUserPosition(_daiReserveId(spoke2), bob); - if (bobPosition.drawnShares == 0) { - Utils.borrow( - spoke2, - _daiReserveId(spoke2), - bob, - hub1.previewRestoreByShares(daiAssetId, 1), - bob - ); - } - - // Bob borrows more dai to trigger accrual - Utils.borrow(spoke2, _daiReserveId(spoke2), bob, 1e18, bob); - - bobRp = _calculateExpectedUserRP(spoke2, bob); - - // Refresh debt values - (amounts.daiBorrowAmount, ) = spoke2.getUserDebt(_daiReserveId(spoke2), bob); - (amounts.wethBorrowAmount, ) = spoke2.getUserDebt(_wethReserveId(spoke2), bob); - (amounts.usdxBorrowAmount, ) = spoke2.getUserDebt(_usdxReserveId(spoke2), bob); - (amounts.wbtcBorrowAmount, ) = spoke2.getUserDebt(_wbtcReserveId(spoke2), bob); - - // Refresh base borrow rates - values.daiBaseBorrowRate = hub1.getAssetDrawnRate(daiAssetId).toUint96(); - values.wethBaseBorrowRate = hub1.getAssetDrawnRate(wethAssetId).toUint96(); - values.usdxBaseBorrowRate = hub1.getAssetDrawnRate(usdxAssetId).toUint96(); - values.wbtcBaseBorrowRate = hub1.getAssetDrawnRate(wbtcAssetId).toUint96(); - - // Refresh indexes - values.daiIndex = hub1.getAssetDrawnIndex(daiAssetId).toUint120(); - values.wethIndex = hub1.getAssetDrawnIndex(wethAssetId).toUint120(); - values.usdxIndex = hub1.getAssetDrawnIndex(usdxAssetId).toUint120(); - values.wbtcIndex = hub1.getAssetDrawnIndex(wbtcAssetId).toUint120(); - - // Refresh timestamps - DAI may or may not have been borrowed above, potentially triggering accruals - values.daiTimestamp = hub1.getAsset(daiAssetId).lastUpdateTimestamp; - values.wethTimestamp = hub1.getAsset(wethAssetId).lastUpdateTimestamp; - values.usdxTimestamp = hub1.getAsset(usdxAssetId).lastUpdateTimestamp; - values.wbtcTimestamp = hub1.getAsset(wbtcAssetId).lastUpdateTimestamp; - - // Check debt values before accrual - bobPosition = spoke2.getUserPosition(_daiReserveId(spoke2), bob); - expectedPremiumDebt = _calculatePremiumDebt( - hub1, - daiAssetId, - bobPosition.premiumShares, - bobPosition.premiumOffsetRay + uint256 drawnDebt; + for (uint256 i = 0; i < 4; ++i) { + drawnDebt = _calculateExpectedDrawnDebt( + testAmounts[i].borrowAmount, + values[i].baseBorrowRate, + startTime ); _assertSingleUserProtocolDebt( spoke2, - _daiReserveId(spoke2), + testAmounts[i].reserveId, bob, - amounts.daiBorrowAmount, - expectedPremiumDebt, - 'dai before second accrual' - ); - values.daiBaseShares = bobPosition.drawnShares; - - bobPosition = spoke2.getUserPosition(_wethReserveId(spoke2), bob); - expectedPremiumDebt = _calculatePremiumDebt( - hub1, - wethAssetId, - bobPosition.premiumShares, - bobPosition.premiumOffsetRay + drawnDebt, + 0, + string.concat(testAmounts[i].name, ' before accrual') ); - _assertSingleUserProtocolDebt( + _assertUserSupply( spoke2, - _wethReserveId(spoke2), + testAmounts[i].reserveId, bob, - amounts.wethBorrowAmount, - expectedPremiumDebt, - 'weth before second accrual' + testAmounts[i].supplyAmount, + string.concat(testAmounts[i].name, ' before accrual') ); - values.wethBaseShares = bobPosition.drawnShares; - - bobPosition = spoke2.getUserPosition(_usdxReserveId(spoke2), bob); - expectedPremiumDebt = _calculatePremiumDebt( - hub1, - usdxAssetId, - bobPosition.premiumShares, - bobPosition.premiumOffsetRay - ); - _assertSingleUserProtocolDebt( + _assertReserveSupply( spoke2, - _usdxReserveId(spoke2), - bob, - amounts.usdxBorrowAmount, - expectedPremiumDebt, - 'usdx before second accrual' + testAmounts[i].reserveId, + MAX_SUPPLY_AMOUNT, + string.concat(testAmounts[i].name, ' before accrual') ); - values.usdxBaseShares = bobPosition.drawnShares; - - bobPosition = spoke2.getUserPosition(_wbtcReserveId(spoke2), bob); - expectedPremiumDebt = _calculatePremiumDebt( - hub1, - wbtcAssetId, - bobPosition.premiumShares, - bobPosition.premiumOffsetRay + _assertSpokeSupply( + spoke2, + testAmounts[i].reserveId, + MAX_SUPPLY_AMOUNT, + string.concat(testAmounts[i].name, ' before accrual') ); - _assertSingleUserProtocolDebt( + _assertAssetSupply( spoke2, - _wbtcReserveId(spoke2), - bob, - amounts.wbtcBorrowAmount, - expectedPremiumDebt, - 'wbtc before second accrual' + testAmounts[i].reserveId, + MAX_SUPPLY_AMOUNT, + string.concat(testAmounts[i].name, ' before accrual') ); - values.wbtcBaseShares = bobPosition.drawnShares; + } - // Store timestamp before next skip time - startTime = vm.getBlockTimestamp().toUint40(); - skipTime = randomizer(0, MAX_SKIP_TIME / 2).toUint40(); - skip(skipTime); + // Skip time to accrue interest + skip(skipTime); - // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level - values.daiIndex = _calculateExpectedDrawnIndex( - values.daiIndex, - values.daiBaseBorrowRate, - values.daiTimestamp + // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level + ISpoke.UserPosition memory bobPosition; + uint256 expectedPremiumDebt; + uint256 interest; + for (uint256 i = 0; i < 4; ++i) { + bobPosition = spoke2.getUserPosition(testAmounts[i].reserveId, bob); + drawnDebt = _calculateExpectedDrawnDebt( + testAmounts[i].borrowAmount, + values[i].baseBorrowRate, + startTime ); - bobPosition = spoke2.getUserPosition(_daiReserveId(spoke2), bob); - drawnDebt = values.daiBaseShares.rayMulUp(values.daiIndex); - expectedPremiumDebt = _calculatePremiumDebt( - hub1, - daiAssetId, - bobPosition.premiumShares, - bobPosition.premiumOffsetRay + expectedPremiumDebt = _calculateExpectedPremiumDebt( + testAmounts[i].borrowAmount, + drawnDebt, + bobRp ); interest = (drawnDebt + expectedPremiumDebt) - - (originalAmounts.daiBorrowAmount + 1e18) - - _calculateBurntInterest(hub1, daiAssetId); // subtract out the extra amount we borrowed + testAmounts[i].borrowAmount - + _calculateBurntInterest(hub1, testAmounts[i].assetId); _assertSingleUserProtocolDebt( spoke2, - _daiReserveId(spoke2), + testAmounts[i].reserveId, bob, drawnDebt, expectedPremiumDebt, - 'dai after second accrual' + string.concat(testAmounts[i].name, ' after accrual') ); _assertUserSupply( spoke2, - _daiReserveId(spoke2), + testAmounts[i].reserveId, bob, - originalAmounts.daiSupplyAmount + - (interest * originalAmounts.daiSupplyAmount) / - MAX_SUPPLY_AMOUNT, - 'dai after second accrual' + testAmounts[i].supplyAmount + (interest * testAmounts[i].supplyAmount) / MAX_SUPPLY_AMOUNT, + string.concat(testAmounts[i].name, ' after accrual') ); _assertReserveSupply( spoke2, - _daiReserveId(spoke2), + testAmounts[i].reserveId, MAX_SUPPLY_AMOUNT + interest, - 'dai after second accrual' + string.concat(testAmounts[i].name, ' after accrual') ); _assertSpokeSupply( spoke2, - _daiReserveId(spoke2), + testAmounts[i].reserveId, MAX_SUPPLY_AMOUNT + interest, - 'dai after second accrual' + string.concat(testAmounts[i].name, ' after accrual') ); _assertAssetSupply( spoke2, - _daiReserveId(spoke2), + testAmounts[i].reserveId, MAX_SUPPLY_AMOUNT + interest, - 'dai after second accrual' + string.concat(testAmounts[i].name, ' after accrual') ); + } - if (originalAmounts.wethBorrowAmount > 0) { - values.wethIndex = _calculateExpectedDrawnIndex( - values.wethTimestamp == 1 ? originalAmounts.wethIndex : values.wethIndex, // If weth never updated, use original index - values.wethBaseBorrowRate, - values.wethTimestamp - ); - bobPosition = spoke2.getUserPosition(_wethReserveId(spoke2), bob); - drawnDebt = values.wethBaseShares.rayMulUp(values.wethIndex); - expectedPremiumDebt = _calculatePremiumDebt( - hub1, - wethAssetId, - bobPosition.premiumShares, - bobPosition.premiumOffsetRay - ); - interest = - (drawnDebt + expectedPremiumDebt) - - originalAmounts.wethBorrowAmount - - _calculateBurntInterest(hub1, wethAssetId); - _assertSingleUserProtocolDebt( - spoke2, - _wethReserveId(spoke2), - bob, - drawnDebt, - expectedPremiumDebt, - 'weth after second accrual' - ); - _assertUserSupply( + // Only proceed with test if position is healthy + if (_getUserHealthFactor(spoke2, bob) >= HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { + // Supply more collateral to ensure bob can borrow more dai to trigger accrual + deal(address(tokenList.dai), bob, MAX_SUPPLY_AMOUNT); + Utils.supplyCollateral(spoke2, _usdzReserveId(spoke2), bob, MAX_SUPPLY_AMOUNT, bob); + + // Handle case that bob isn't already borrowing dai by borrowing 1 share + bobPosition = spoke2.getUserPosition(_daiReserveId(spoke2), bob); + if (bobPosition.drawnShares == 0) { + Utils.borrow( spoke2, - _wethReserveId(spoke2), + _daiReserveId(spoke2), bob, - originalAmounts.wethSupplyAmount + - (interest * originalAmounts.wethSupplyAmount) / - MAX_SUPPLY_AMOUNT, - 'weth after second accrual' - ); - _assertReserveSupply( - spoke2, - _wethReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'weth after second accrual' - ); - _assertSpokeSupply( - spoke2, - _wethReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'weth after second accrual' - ); - _assertAssetSupply( - spoke2, - _wethReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'weth after second accrual' + hub1.previewRestoreByShares(daiAssetId, 1), + bob ); } - if (originalAmounts.usdxBorrowAmount > 0) { - values.usdxIndex = _calculateExpectedDrawnIndex( - values.usdxTimestamp == 1 ? originalAmounts.usdxIndex : values.usdxIndex, // If usdx never updated, use original index - values.usdxBaseBorrowRate, - values.usdxTimestamp - ); - bobPosition = spoke2.getUserPosition(_usdxReserveId(spoke2), bob); - drawnDebt = values.usdxBaseShares.rayMulUp(values.usdxIndex); + // Bob borrows more dai to trigger accrual + Utils.borrow(spoke2, _daiReserveId(spoke2), bob, 1e18, bob); + + bobRp = _calculateExpectedUserRP(spoke2, bob); + + // Update amounts for second accrual checks + for (uint256 i = 0; i < 4; ++i) { + (testAmounts[i].borrowAmount, ) = spoke2.getUserDebt(testAmounts[i].reserveId, bob); + values[i].baseBorrowRate = hub1.getAssetDrawnRate(testAmounts[i].assetId).toUint96(); + values[i].index = hub1.getAssetDrawnIndex(testAmounts[i].assetId).toUint120(); + values[i].timestamp = hub1.getAsset(testAmounts[i].assetId).lastUpdateTimestamp; + } + + // Check debt values before accrual + for (uint256 i = 0; i < 4; ++i) { + bobPosition = spoke2.getUserPosition(testAmounts[i].reserveId, bob); expectedPremiumDebt = _calculatePremiumDebt( hub1, - usdxAssetId, + testAmounts[i].assetId, bobPosition.premiumShares, bobPosition.premiumOffsetRay ); - interest = - (drawnDebt + expectedPremiumDebt) - - originalAmounts.usdxBorrowAmount - - _calculateBurntInterest(hub1, usdxAssetId); _assertSingleUserProtocolDebt( spoke2, - _usdxReserveId(spoke2), + testAmounts[i].reserveId, bob, - drawnDebt, + testAmounts[i].borrowAmount, expectedPremiumDebt, - 'usdx after second accrual' - ); - _assertUserSupply( - spoke2, - _usdxReserveId(spoke2), - bob, - originalAmounts.usdxSupplyAmount + - (interest * originalAmounts.usdxSupplyAmount) / - MAX_SUPPLY_AMOUNT, - 'usdx after second accrual' - ); - _assertReserveSupply( - spoke2, - _usdxReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'usdx after second accrual' - ); - _assertSpokeSupply( - spoke2, - _usdxReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'usdx after second accrual' - ); - _assertAssetSupply( - spoke2, - _usdxReserveId(spoke2), - MAX_SUPPLY_AMOUNT + interest, - 'usdx after second accrual' + string.concat(testAmounts[i].name, ' before second accrual') ); + values[i].baseShares = bobPosition.drawnShares; } - if (originalAmounts.wbtcBorrowAmount > 0) { - values.wbtcIndex = _calculateExpectedDrawnIndex( - values.wbtcTimestamp == 1 ? originalAmounts.wbtcIndex : values.wbtcIndex, // If wbtc never updated, use original index - values.wbtcBaseBorrowRate, - values.wbtcTimestamp + // Store timestamp before next skip time + startTime = vm.getBlockTimestamp().toUint40(); + skipTime = randomizer(0, MAX_SKIP_TIME / 2).toUint40(); + skip(skipTime); + + // Account for the dai we just borrowed + testAmounts[0].originalBorrowAmount += 1e18; + + // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level + for (uint256 i = 0; i < 4; ++i) { + if (testAmounts[i].originalBorrowAmount == 0) { + continue; + } + values[i].index = _calculateExpectedDrawnIndex( + values[i].timestamp == 1 ? testAmounts[i].originalIndex : values[i].index, // If reserve never updated, use original index + values[i].baseBorrowRate, + values[i].timestamp ); - bobPosition = spoke2.getUserPosition(_wbtcReserveId(spoke2), bob); - drawnDebt = values.wbtcBaseShares.rayMulUp(values.wbtcIndex); + bobPosition = spoke2.getUserPosition(testAmounts[i].reserveId, bob); + drawnDebt = values[i].baseShares.rayMulUp(values[i].index); expectedPremiumDebt = _calculatePremiumDebt( hub1, - wbtcAssetId, + testAmounts[i].assetId, bobPosition.premiumShares, bobPosition.premiumOffsetRay ); interest = (drawnDebt + expectedPremiumDebt) - - originalAmounts.wbtcBorrowAmount - - _calculateBurntInterest(hub1, wbtcAssetId); + testAmounts[i].originalBorrowAmount - + _calculateBurntInterest(hub1, testAmounts[i].assetId); _assertSingleUserProtocolDebt( spoke2, - _wbtcReserveId(spoke2), + testAmounts[i].reserveId, bob, drawnDebt, expectedPremiumDebt, - 'wbtc after second accrual' + string.concat(testAmounts[i].name, ' after second accrual') ); _assertUserSupply( spoke2, - _wbtcReserveId(spoke2), + testAmounts[i].reserveId, bob, - originalAmounts.wbtcSupplyAmount + - (interest * originalAmounts.wbtcSupplyAmount) / + testAmounts[i].originalSupplyAmount + + (interest * testAmounts[i].originalSupplyAmount) / MAX_SUPPLY_AMOUNT, - 'wbtc after second accrual' + string.concat(testAmounts[i].name, ' after second accrual') ); _assertReserveSupply( spoke2, - _wbtcReserveId(spoke2), + testAmounts[i].reserveId, MAX_SUPPLY_AMOUNT + interest, - 'wbtc after second accrual' + string.concat(testAmounts[i].name, ' after second accrual') ); _assertSpokeSupply( spoke2, - _wbtcReserveId(spoke2), + testAmounts[i].reserveId, MAX_SUPPLY_AMOUNT + interest, - 'wbtc after second accrual' + string.concat(testAmounts[i].name, ' after second accrual') ); _assertAssetSupply( spoke2, - _wbtcReserveId(spoke2), + testAmounts[i].reserveId, MAX_SUPPLY_AMOUNT + interest, - 'wbtc after second accrual' + string.concat(testAmounts[i].name, ' after second accrual') ); } } @@ -791,23 +392,6 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { return amounts; } - function _bound(LocalInfo memory rates) internal view returns (LocalInfo memory) { - rates.daiBaseBorrowRate = _bpsToRay( - bound(rates.daiBaseBorrowRate, 1, irStrategy.MAX_BORROW_RATE()) - ).toUint96(); - rates.wethBaseBorrowRate = _bpsToRay( - bound(rates.wethBaseBorrowRate, 1, irStrategy.MAX_BORROW_RATE()) - ).toUint96(); - rates.usdxBaseBorrowRate = _bpsToRay( - bound(rates.usdxBaseBorrowRate, 1, irStrategy.MAX_BORROW_RATE()) - ).toUint96(); - rates.wbtcBaseBorrowRate = _bpsToRay( - bound(rates.wbtcBaseBorrowRate, 1, irStrategy.MAX_BORROW_RATE()) - ).toUint96(); - - return rates; - } - function _ensureSufficientCollateral( ISpoke spoke, TestAmounts memory amounts @@ -866,23 +450,4 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { return amounts; } - - /// @dev Helper to deep copy TestAmounts struct - function _copyAmounts(TestAmounts memory amounts) internal pure returns (TestAmounts memory) { - return - TestAmounts({ - daiSupplyAmount: amounts.daiSupplyAmount, - wethSupplyAmount: amounts.wethSupplyAmount, - usdxSupplyAmount: amounts.usdxSupplyAmount, - wbtcSupplyAmount: amounts.wbtcSupplyAmount, - daiBorrowAmount: amounts.daiBorrowAmount, - wethBorrowAmount: amounts.wethBorrowAmount, - usdxBorrowAmount: amounts.usdxBorrowAmount, - wbtcBorrowAmount: amounts.wbtcBorrowAmount, - startTime: 0, - wethIndex: 0, - usdxIndex: 0, - wbtcIndex: 0 - }); - } } From 4cf794280858cb776e45cd1e073ec76f11134338 Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Wed, 17 Dec 2025 16:41:13 +0900 Subject: [PATCH 11/15] fix: Helper for parsing inputs --- .../Spoke/Spoke.AccrueInterest.Scenario.t.sol | 119 +++++++++--------- 1 file changed, 61 insertions(+), 58 deletions(-) diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index aec5c5640..ba8eedb83 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -53,74 +53,21 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { updateLiquidityFee(hub1, usdzAssetId, 0); } - /// Second accrual after an action - which should update the user rp + /// @dev Check protocol supply and debt values after two separate interest accruals with multiple assets supplied and borrowed + /// @dev Ensures interest accrues correctly after each accrual, in accordance with the user's expected risk premium function test_accrueInterest_fuzz_RPBorrowAndSkipTime_twoActions( TestAmounts memory amounts, uint40 skipTime ) public { amounts = _bound(amounts); skipTime = bound(skipTime, 0, MAX_SKIP_TIME / 2).toUint40(); + uint40 startTime = vm.getBlockTimestamp().toUint40(); // Ensure bob does not draw more than half his normalized supply value amounts = _ensureSufficientCollateral(spoke2, amounts); + TestAmount[] memory testAmounts = _parseTestInputs(amounts); - // 1 -> DAI, 2 -> WETH, 3 -> USDx, 4 -> WBTC - TestAmount[] memory testAmounts = new TestAmount[](4); - for (uint256 i = 0; i < 4; ++i) { - if (i == 0) { - testAmounts[i] = TestAmount({ - supplyAmount: amounts.daiSupplyAmount, - borrowAmount: amounts.daiBorrowAmount, - originalSupplyAmount: amounts.daiSupplyAmount, - originalBorrowAmount: amounts.daiBorrowAmount, - index: hub1.getAssetDrawnIndex(daiAssetId), - originalIndex: hub1.getAssetDrawnIndex(daiAssetId), - reserveId: _daiReserveId(spoke2), - assetId: daiAssetId, - name: 'DAI' - }); - } else if (i == 1) { - testAmounts[i] = TestAmount({ - supplyAmount: amounts.wethSupplyAmount, - borrowAmount: amounts.wethBorrowAmount, - originalSupplyAmount: amounts.wethSupplyAmount, - originalBorrowAmount: amounts.wethBorrowAmount, - index: hub1.getAssetDrawnIndex(wethAssetId), - originalIndex: hub1.getAssetDrawnIndex(wethAssetId), - reserveId: _wethReserveId(spoke2), - assetId: wethAssetId, - name: 'WETH' - }); - } else if (i == 2) { - testAmounts[i] = TestAmount({ - supplyAmount: amounts.usdxSupplyAmount, - borrowAmount: amounts.usdxBorrowAmount, - originalSupplyAmount: amounts.usdxSupplyAmount, - originalBorrowAmount: amounts.usdxBorrowAmount, - index: hub1.getAssetDrawnIndex(usdxAssetId), - originalIndex: hub1.getAssetDrawnIndex(usdxAssetId), - reserveId: _usdxReserveId(spoke2), - assetId: usdxAssetId, - name: 'USDX' - }); - } else { - testAmounts[i] = TestAmount({ - supplyAmount: amounts.wbtcSupplyAmount, - borrowAmount: amounts.wbtcBorrowAmount, - originalSupplyAmount: amounts.wbtcSupplyAmount, - originalBorrowAmount: amounts.wbtcBorrowAmount, - index: hub1.getAssetDrawnIndex(wbtcAssetId), - originalIndex: hub1.getAssetDrawnIndex(wbtcAssetId), - reserveId: _wbtcReserveId(spoke2), - assetId: wbtcAssetId, - name: 'WBTC' - }); - } - } - - uint40 startTime = vm.getBlockTimestamp().toUint40(); - - // Bob supplies amounts on spoke 2, then we deploy remainder of liquidity + // Bob supplies amounts on spoke 2, then we deploy remainder of liquidity up to respective supply caps for (uint256 i = 0; i < 4; ++i) { if (testAmounts[i].supplyAmount > 0) { Utils.supplyCollateral( @@ -392,6 +339,62 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { return amounts; } + function _parseTestInputs( + TestAmounts memory amounts + ) internal view returns (TestAmount[] memory) { + TestAmount[] memory testAmounts = new TestAmount[](4); + + testAmounts[0] = TestAmount({ + supplyAmount: amounts.daiSupplyAmount, + borrowAmount: amounts.daiBorrowAmount, + originalSupplyAmount: amounts.daiSupplyAmount, + originalBorrowAmount: amounts.daiBorrowAmount, + index: hub1.getAssetDrawnIndex(daiAssetId), + originalIndex: hub1.getAssetDrawnIndex(daiAssetId), + reserveId: _daiReserveId(spoke2), + assetId: daiAssetId, + name: 'DAI' + }); + + testAmounts[1] = TestAmount({ + supplyAmount: amounts.wethSupplyAmount, + borrowAmount: amounts.wethBorrowAmount, + originalSupplyAmount: amounts.wethSupplyAmount, + originalBorrowAmount: amounts.wethBorrowAmount, + index: hub1.getAssetDrawnIndex(wethAssetId), + originalIndex: hub1.getAssetDrawnIndex(wethAssetId), + reserveId: _wethReserveId(spoke2), + assetId: wethAssetId, + name: 'WETH' + }); + + testAmounts[2] = TestAmount({ + supplyAmount: amounts.usdxSupplyAmount, + borrowAmount: amounts.usdxBorrowAmount, + originalSupplyAmount: amounts.usdxSupplyAmount, + originalBorrowAmount: amounts.usdxBorrowAmount, + index: hub1.getAssetDrawnIndex(usdxAssetId), + originalIndex: hub1.getAssetDrawnIndex(usdxAssetId), + reserveId: _usdxReserveId(spoke2), + assetId: usdxAssetId, + name: 'USDX' + }); + + testAmounts[3] = TestAmount({ + supplyAmount: amounts.wbtcSupplyAmount, + borrowAmount: amounts.wbtcBorrowAmount, + originalSupplyAmount: amounts.wbtcSupplyAmount, + originalBorrowAmount: amounts.wbtcBorrowAmount, + index: hub1.getAssetDrawnIndex(wbtcAssetId), + originalIndex: hub1.getAssetDrawnIndex(wbtcAssetId), + reserveId: _wbtcReserveId(spoke2), + assetId: wbtcAssetId, + name: 'WBTC' + }); + + return testAmounts; + } + function _ensureSufficientCollateral( ISpoke spoke, TestAmounts memory amounts From 7ae17d1fa1ec6ca09c2c5decda6956e32b2117d0 Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Wed, 17 Dec 2025 16:44:44 +0900 Subject: [PATCH 12/15] fix: Rename structs --- .../Spoke/Spoke.AccrueInterest.Scenario.t.sol | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index ba8eedb83..bd6d327b2 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -10,7 +10,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { using PercentageMath for uint256; using SafeCast for uint256; - struct TestAmounts { + struct TestInputs { uint256 daiSupplyAmount; uint256 wethSupplyAmount; uint256 usdxSupplyAmount; @@ -37,7 +37,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { string name; } - struct TestInfo { + struct TestValues { uint96 baseBorrowRate; uint256 index; uint256 baseShares; @@ -56,7 +56,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { /// @dev Check protocol supply and debt values after two separate interest accruals with multiple assets supplied and borrowed /// @dev Ensures interest accrues correctly after each accrual, in accordance with the user's expected risk premium function test_accrueInterest_fuzz_RPBorrowAndSkipTime_twoActions( - TestAmounts memory amounts, + TestInputs memory amounts, uint40 skipTime ) public { amounts = _bound(amounts); @@ -100,7 +100,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { assertEq(bobRp, _calculateExpectedUserRP(spoke2, bob), 'user risk premium Before'); // Store base borrow rates - TestInfo[] memory values = new TestInfo[](4); + TestValues[] memory values = new TestValues[](4); for (uint256 i = 0; i < 4; ++i) { values[i].baseBorrowRate = hub1.getAssetDrawnRate(testAmounts[i].assetId).toUint96(); } @@ -326,7 +326,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { } } - function _bound(TestAmounts memory amounts) internal pure returns (TestAmounts memory) { + function _bound(TestInputs memory amounts) internal pure returns (TestInputs memory) { amounts.daiSupplyAmount = bound(amounts.daiSupplyAmount, 0, MAX_SUPPLY_AMOUNT); amounts.wethSupplyAmount = bound(amounts.wethSupplyAmount, 0, MAX_SUPPLY_AMOUNT); amounts.usdxSupplyAmount = bound(amounts.usdxSupplyAmount, 0, MAX_SUPPLY_AMOUNT); @@ -339,9 +339,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { return amounts; } - function _parseTestInputs( - TestAmounts memory amounts - ) internal view returns (TestAmount[] memory) { + function _parseTestInputs(TestInputs memory amounts) internal view returns (TestAmount[] memory) { TestAmount[] memory testAmounts = new TestAmount[](4); testAmounts[0] = TestAmount({ @@ -397,8 +395,8 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { function _ensureSufficientCollateral( ISpoke spoke, - TestAmounts memory amounts - ) internal view returns (TestAmounts memory) { + TestInputs memory amounts + ) internal view returns (TestInputs memory) { uint256 remainingCollateralValue = _getValue( spoke, _daiReserveId(spoke), From e680df864c143e7da72de315b5cc6c7b2119da7f Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Wed, 17 Dec 2025 18:12:54 +0900 Subject: [PATCH 13/15] fix: Helper for checking protocol supply and debt --- .../Spoke/Spoke.AccrueInterest.Scenario.t.sol | 165 +++++++----------- 1 file changed, 63 insertions(+), 102 deletions(-) diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index bd6d327b2..f1e489389 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -106,45 +106,20 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { } // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level - uint256 drawnDebt; for (uint256 i = 0; i < 4; ++i) { - drawnDebt = _calculateExpectedDrawnDebt( + uint256 drawnDebt = _calculateExpectedDrawnDebt( testAmounts[i].borrowAmount, values[i].baseBorrowRate, startTime ); - _assertSingleUserProtocolDebt( - spoke2, + _assertProtocolSupplyAndDebt( testAmounts[i].reserveId, - bob, + testAmounts[i].name, drawnDebt, 0, - string.concat(testAmounts[i].name, ' before accrual') - ); - _assertUserSupply( - spoke2, - testAmounts[i].reserveId, - bob, testAmounts[i].supplyAmount, - string.concat(testAmounts[i].name, ' before accrual') - ); - _assertReserveSupply( - spoke2, - testAmounts[i].reserveId, - MAX_SUPPLY_AMOUNT, - string.concat(testAmounts[i].name, ' before accrual') - ); - _assertSpokeSupply( - spoke2, - testAmounts[i].reserveId, - MAX_SUPPLY_AMOUNT, - string.concat(testAmounts[i].name, ' before accrual') - ); - _assertAssetSupply( - spoke2, - testAmounts[i].reserveId, MAX_SUPPLY_AMOUNT, - string.concat(testAmounts[i].name, ' before accrual') + ' before first accrual' ); } @@ -152,57 +127,32 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { skip(skipTime); // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level - ISpoke.UserPosition memory bobPosition; - uint256 expectedPremiumDebt; - uint256 interest; for (uint256 i = 0; i < 4; ++i) { - bobPosition = spoke2.getUserPosition(testAmounts[i].reserveId, bob); - drawnDebt = _calculateExpectedDrawnDebt( + uint256 drawnDebt = _calculateExpectedDrawnDebt( testAmounts[i].borrowAmount, values[i].baseBorrowRate, startTime ); - expectedPremiumDebt = _calculateExpectedPremiumDebt( + uint256 expectedPremiumDebt = _calculateExpectedPremiumDebt( testAmounts[i].borrowAmount, drawnDebt, bobRp ); - interest = - (drawnDebt + expectedPremiumDebt) - + uint256 interest = (drawnDebt + expectedPremiumDebt) - testAmounts[i].borrowAmount - _calculateBurntInterest(hub1, testAmounts[i].assetId); - _assertSingleUserProtocolDebt( - spoke2, + uint256 expectedUserSupply = testAmounts[i].supplyAmount + + (interest * testAmounts[i].supplyAmount) / + MAX_SUPPLY_AMOUNT; + + _assertProtocolSupplyAndDebt( testAmounts[i].reserveId, - bob, + testAmounts[i].name, drawnDebt, expectedPremiumDebt, - string.concat(testAmounts[i].name, ' after accrual') - ); - _assertUserSupply( - spoke2, - testAmounts[i].reserveId, - bob, - testAmounts[i].supplyAmount + (interest * testAmounts[i].supplyAmount) / MAX_SUPPLY_AMOUNT, - string.concat(testAmounts[i].name, ' after accrual') - ); - _assertReserveSupply( - spoke2, - testAmounts[i].reserveId, - MAX_SUPPLY_AMOUNT + interest, - string.concat(testAmounts[i].name, ' after accrual') - ); - _assertSpokeSupply( - spoke2, - testAmounts[i].reserveId, + expectedUserSupply, MAX_SUPPLY_AMOUNT + interest, - string.concat(testAmounts[i].name, ' after accrual') - ); - _assertAssetSupply( - spoke2, - testAmounts[i].reserveId, - MAX_SUPPLY_AMOUNT + interest, - string.concat(testAmounts[i].name, ' after accrual') + ' after first accrual' ); } @@ -213,7 +163,7 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { Utils.supplyCollateral(spoke2, _usdzReserveId(spoke2), bob, MAX_SUPPLY_AMOUNT, bob); // Handle case that bob isn't already borrowing dai by borrowing 1 share - bobPosition = spoke2.getUserPosition(_daiReserveId(spoke2), bob); + ISpoke.UserPosition memory bobPosition = spoke2.getUserPosition(_daiReserveId(spoke2), bob); if (bobPosition.drawnShares == 0) { Utils.borrow( spoke2, @@ -235,12 +185,13 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { values[i].baseBorrowRate = hub1.getAssetDrawnRate(testAmounts[i].assetId).toUint96(); values[i].index = hub1.getAssetDrawnIndex(testAmounts[i].assetId).toUint120(); values[i].timestamp = hub1.getAsset(testAmounts[i].assetId).lastUpdateTimestamp; + values[i].baseShares = spoke2.getUserPosition(testAmounts[i].reserveId, bob).drawnShares; } // Check debt values before accrual for (uint256 i = 0; i < 4; ++i) { bobPosition = spoke2.getUserPosition(testAmounts[i].reserveId, bob); - expectedPremiumDebt = _calculatePremiumDebt( + uint256 expectedPremiumDebt = _calculatePremiumDebt( hub1, testAmounts[i].assetId, bobPosition.premiumShares, @@ -254,7 +205,6 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { expectedPremiumDebt, string.concat(testAmounts[i].name, ' before second accrual') ); - values[i].baseShares = bobPosition.drawnShares; } // Store timestamp before next skip time @@ -276,51 +226,28 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { values[i].timestamp ); bobPosition = spoke2.getUserPosition(testAmounts[i].reserveId, bob); - drawnDebt = values[i].baseShares.rayMulUp(values[i].index); - expectedPremiumDebt = _calculatePremiumDebt( + uint256 drawnDebt = values[i].baseShares.rayMulUp(values[i].index); + uint256 expectedPremiumDebt = _calculatePremiumDebt( hub1, testAmounts[i].assetId, bobPosition.premiumShares, bobPosition.premiumOffsetRay ); - interest = - (drawnDebt + expectedPremiumDebt) - + uint256 interest = (drawnDebt + expectedPremiumDebt) - testAmounts[i].originalBorrowAmount - _calculateBurntInterest(hub1, testAmounts[i].assetId); - _assertSingleUserProtocolDebt( - spoke2, + uint256 expectedUserSupply = testAmounts[i].originalSupplyAmount + + (interest * testAmounts[i].originalSupplyAmount) / + MAX_SUPPLY_AMOUNT; + + _assertProtocolSupplyAndDebt( testAmounts[i].reserveId, - bob, + testAmounts[i].name, drawnDebt, expectedPremiumDebt, - string.concat(testAmounts[i].name, ' after second accrual') - ); - _assertUserSupply( - spoke2, - testAmounts[i].reserveId, - bob, - testAmounts[i].originalSupplyAmount + - (interest * testAmounts[i].originalSupplyAmount) / - MAX_SUPPLY_AMOUNT, - string.concat(testAmounts[i].name, ' after second accrual') - ); - _assertReserveSupply( - spoke2, - testAmounts[i].reserveId, - MAX_SUPPLY_AMOUNT + interest, - string.concat(testAmounts[i].name, ' after second accrual') - ); - _assertSpokeSupply( - spoke2, - testAmounts[i].reserveId, - MAX_SUPPLY_AMOUNT + interest, - string.concat(testAmounts[i].name, ' after second accrual') - ); - _assertAssetSupply( - spoke2, - testAmounts[i].reserveId, + expectedUserSupply, MAX_SUPPLY_AMOUNT + interest, - string.concat(testAmounts[i].name, ' after second accrual') + ' after second accrual' ); } } @@ -451,4 +378,38 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { return amounts; } + + function _assertProtocolSupplyAndDebt( + uint256 reserveId, + string memory reserveName, + uint256 expectedDrawnDebt, + uint256 expectedPremiumDebt, + uint256 expectedUserSupply, + uint256 expectedReserveSupply, + string memory label + ) internal view { + _assertSingleUserProtocolDebt( + spoke2, + reserveId, + bob, + expectedDrawnDebt, + expectedPremiumDebt, + string.concat(reserveName, label) + ); + _assertUserSupply( + spoke2, + reserveId, + bob, + expectedUserSupply, + string.concat(reserveName, label) + ); + _assertReserveSupply( + spoke2, + reserveId, + expectedReserveSupply, + string.concat(reserveName, label) + ); + _assertSpokeSupply(spoke2, reserveId, expectedReserveSupply, string.concat(reserveName, label)); + _assertAssetSupply(spoke2, reserveId, expectedReserveSupply, string.concat(reserveName, label)); + } } From 62770277e58fc9b89cbdc9b33276f632e7d963a2 Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Wed, 17 Dec 2025 18:33:24 +0900 Subject: [PATCH 14/15] chore: Cleanup --- .../Spoke/Spoke.AccrueInterest.Scenario.t.sol | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index f1e489389..16b8d504e 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -176,6 +176,8 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { // Bob borrows more dai to trigger accrual Utils.borrow(spoke2, _daiReserveId(spoke2), bob, 1e18, bob); + // Account for the dai we just borrowed + testAmounts[0].originalBorrowAmount += 1e18; bobRp = _calculateExpectedUserRP(spoke2, bob); @@ -188,22 +190,31 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { values[i].baseShares = spoke2.getUserPosition(testAmounts[i].reserveId, bob).drawnShares; } - // Check debt values before accrual + // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level for (uint256 i = 0; i < 4; ++i) { bobPosition = spoke2.getUserPosition(testAmounts[i].reserveId, bob); + uint256 drawnDebt = testAmounts[i].borrowAmount; uint256 expectedPremiumDebt = _calculatePremiumDebt( hub1, testAmounts[i].assetId, bobPosition.premiumShares, bobPosition.premiumOffsetRay ); - _assertSingleUserProtocolDebt( - spoke2, + uint256 interest = (drawnDebt + expectedPremiumDebt) - + testAmounts[i].originalBorrowAmount - + _calculateBurntInterest(hub1, testAmounts[i].assetId); + uint256 expectedUserSupply = testAmounts[i].originalSupplyAmount + + (interest * testAmounts[i].originalSupplyAmount) / + MAX_SUPPLY_AMOUNT; + + _assertProtocolSupplyAndDebt( testAmounts[i].reserveId, - bob, - testAmounts[i].borrowAmount, + testAmounts[i].name, + drawnDebt, expectedPremiumDebt, - string.concat(testAmounts[i].name, ' before second accrual') + expectedUserSupply, + MAX_SUPPLY_AMOUNT + interest, + ' before second accrual' ); } @@ -212,9 +223,6 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { skipTime = randomizer(0, MAX_SKIP_TIME / 2).toUint40(); skip(skipTime); - // Account for the dai we just borrowed - testAmounts[0].originalBorrowAmount += 1e18; - // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level for (uint256 i = 0; i < 4; ++i) { if (testAmounts[i].originalBorrowAmount == 0) { From b3358f4b9a82a6ae35b02e2cf8d5e7ace8350797 Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Wed, 17 Dec 2025 18:39:06 +0900 Subject: [PATCH 15/15] fix: Add checks for completeness --- tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol index 16b8d504e..486590315 100644 --- a/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueInterest.Scenario.t.sol @@ -226,6 +226,15 @@ contract SpokeAccrueInterestScenarioTest is SpokeBase { // Check bob's drawn debt, premium debt, and supplied amounts for all assets at user, reserve, spoke, and asset level for (uint256 i = 0; i < 4; ++i) { if (testAmounts[i].originalBorrowAmount == 0) { + _assertProtocolSupplyAndDebt( + testAmounts[i].reserveId, + testAmounts[i].name, + 0, + 0, + testAmounts[i].originalSupplyAmount, + MAX_SUPPLY_AMOUNT, + ' after second accrual' + ); continue; } values[i].index = _calculateExpectedDrawnIndex(