From 600e613808cffbb70973f05753fbdcbd988e8e3e Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:29:47 -0500 Subject: [PATCH 01/29] feat: v4 e2e tests --- foundry.toml | 2 +- remappings.txt | 2 + src/ProtocolV4TestBase.sol | 794 +++++++++++++ src/dependencies/v4/AaveV4EthereumHelpers.sol | 78 ++ src/dependencies/v4/Actions.sol | 467 ++++++++ src/dependencies/v4/GatewayScenarios.sol | 1012 +++++++++++++++++ src/dependencies/v4/Helpers.sol | 393 +++++++ src/dependencies/v4/Scenarios.sol | 690 +++++++++++ src/dependencies/v4/SnapshotV4.sol | 218 ++++ src/dependencies/v4/TokenizationActions.sol | 568 +++++++++ src/dependencies/v4/TokenizationScenarios.sol | 330 ++++++ src/dependencies/v4/Types.sol | 122 ++ src/dependencies/v4/V4DiffWriter.sol | 598 ++++++++++ .../AaveV4PayloadEthereum.sol | 11 + tests/ProtocolV4TestBase.t.sol | 250 ++++ 15 files changed, 5534 insertions(+), 1 deletion(-) create mode 100644 src/ProtocolV4TestBase.sol create mode 100644 src/dependencies/v4/AaveV4EthereumHelpers.sol create mode 100644 src/dependencies/v4/Actions.sol create mode 100644 src/dependencies/v4/GatewayScenarios.sol create mode 100644 src/dependencies/v4/Helpers.sol create mode 100644 src/dependencies/v4/Scenarios.sol create mode 100644 src/dependencies/v4/SnapshotV4.sol create mode 100644 src/dependencies/v4/TokenizationActions.sol create mode 100644 src/dependencies/v4/TokenizationScenarios.sol create mode 100644 src/dependencies/v4/Types.sol create mode 100644 src/dependencies/v4/V4DiffWriter.sol create mode 100644 src/v4-config-engine/AaveV4PayloadEthereum.sol create mode 100644 tests/ProtocolV4TestBase.t.sol diff --git a/foundry.toml b/foundry.toml index 6d3c4ce4..ac22d79e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,7 +5,7 @@ script = 'scripts' out = 'out' libs = ['lib'] remappings = [] -fs_permissions = [{ access = "read-write", path = "./reports" }] +fs_permissions = [{ access = "read-write", path = "./reports" }, { access = "read-write", path = "./diffs" }] ffi = true evm_version = 'cancun' decode_external_storage = true diff --git a/remappings.txt b/remappings.txt index 628b256f..b120f383 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,6 @@ aave-address-book/=lib/aave-address-book/src/ +aave-v4/=lib/aave-address-book/lib/aave-v4/src/ +lib/aave-address-book/lib/aave-v4/:src=lib/aave-address-book/lib/aave-v4/src aave-v3-origin/=lib/aave-address-book/lib/aave-v3-origin/src/ aave-v3-origin-tests/=lib/aave-address-book/lib/aave-v3-origin/tests forge-std/=lib/forge-std/src/ diff --git a/src/ProtocolV4TestBase.sol b/src/ProtocolV4TestBase.sol new file mode 100644 index 00000000..e02f1212 --- /dev/null +++ b/src/ProtocolV4TestBase.sol @@ -0,0 +1,794 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; +import {Strings} from 'openzeppelin-contracts/contracts/utils/Strings.sol'; + +import {ISpoke, IHub, ITokenizationSpoke, INativeTokenGateway, ISignatureGateway, IGiverPositionManager, ITakerPositionManager, IConfigPositionManager} from 'aave-address-book/AaveV4.sol'; +import {AaveV4EthereumPositionManagers} from 'aave-address-book/AaveV4Ethereum.sol'; +import {AaveV4EthereumHubHelpers} from 'src/dependencies/v4/AaveV4EthereumHelpers.sol'; +import {IPayloadsControllerCore, PayloadsControllerUtils} from 'aave-address-book/GovernanceV3.sol'; +import {GovV3Helpers} from 'src/GovV3Helpers.sol'; +import {Types} from 'src/dependencies/v4/Types.sol'; +import {SnapshotV4} from 'src/dependencies/v4/SnapshotV4.sol'; +import {Scenarios} from 'src/dependencies/v4/Scenarios.sol'; +import {TokenizationScenarios} from 'src/dependencies/v4/TokenizationScenarios.sol'; +import {GatewayScenarios} from 'src/dependencies/v4/GatewayScenarios.sol'; + +/// @title ProtocolV4TestBase +/// @notice E2E test base for Aave V4 hub/spoke architecture. +/// Tests supply, withdraw, borrow, repay, and liquidation for each reserve on a spoke. +/// Tests deposit, mint, withdraw, redeem for each tokenization spoke. +/// Tests NativeTokenGateway and SignatureGateway for each spoke. +/// Loops over all good collaterals and uses randomized amounts. +contract ProtocolV4TestBase is SnapshotV4, Scenarios, TokenizationScenarios, GatewayScenarios { + using SafeERC20 for IERC20; + + /// @notice Run the full V4 test suite: snapshot before, execute payload, snapshot after, diff, then e2e. + function defaultTest( + string memory reportName, + ISpoke[] memory spokes, + address[] memory tokenizationSpokes, + address payload + ) public { + return + defaultTest({ + reportName: reportName, + spokes: spokes, + tokenizationSpokes: tokenizationSpokes, + payload: payload, + runE2E: true, + testPositionManagers: false + }); + } + + function defaultTest( + string memory reportName, + ISpoke[] memory spokes, + address[] memory tokenizationSpokes, + address payload, + bool runE2E, + bool testPositionManagers + ) public { + if (payload != address(0)) { + _snapshotDiffAndExecute(reportName, spokes, payload); + } + + if (runE2E) { + vm.pauseGasMetering(); + e2eTestAllSpokes({spokes: spokes, testPositionManagers: testPositionManagers}); + e2eTestAllTokenizationSpokes(tokenizationSpokes); + vm.resumeGasMetering(); + } + } + + function _snapshotDiffAndExecute( + string memory reportName, + ISpoke[] memory spokes, + address payload + ) internal virtual { + IHub[] memory hubs = AaveV4EthereumHubHelpers.getHubs(); + string memory beforeName = string.concat(reportName, '_before'); + string memory afterName = string.concat(reportName, '_after'); + + Types.V4Snapshot memory snapshotBefore = createV4Snapshot(spokes, hubs); + writeV4SnapshotJson(beforeName, snapshotBefore); + + (string memory rawDiff, string memory logsJson) = _executePayloadWithRecording(payload); + + // as executor does delegateCall to the payload, the executor should have no storage changes + { + IPayloadsControllerCore pc = GovV3Helpers.getPayloadsController(block.chainid); + _validateNoExecutorStorageChange( + rawDiff, + pc + .getExecutorSettingsByAccessControl(PayloadsControllerUtils.AccessControl.Level_1) + .executor + ); + } + + Types.V4Snapshot memory snapshotAfter = createV4Snapshot(spokes, hubs); + writeV4SnapshotJson(afterName, snapshotAfter); + + string memory afterPath = string.concat('./reports/', afterName, '.json'); + vm.writeJson(rawDiff, afterPath, '$.raw'); + vm.writeJson(logsJson, afterPath, '$.logs'); + + diffV4Snapshots(reportName, snapshotBefore, snapshotAfter); + } + + function _executePayloadWithRecording( + address payload + ) private returns (string memory rawDiff, string memory logsJson) { + uint256 startGas = gasleft(); + vm.startStateDiffRecording(); + vm.recordLogs(); + + GovV3Helpers.executePayload( + vm, + payload, + address(GovV3Helpers.getPayloadsController(block.chainid)) + ); + + uint256 gasUsed = startGas - gasleft(); + assertLt(gasUsed, (block.gaslimit * 95) / 100, 'BLOCK_GAS_LIMIT_EXCEEDED'); + + rawDiff = vm.getStateDiffJson(); + logsJson = vm.getRecordedLogsJson(); + } + + /// @notice Test all reserves on every spoke in the array. + function e2eTestAllSpokes(ISpoke[] memory spokes, bool testPositionManagers) public { + for (uint256 i; i < spokes.length; i++) { + console.log('--- E2E: Testing spoke %s ---', address(spokes[i])); + console.log('--------------------------------'); + e2eTestSpoke(spokes[i]); + if (testPositionManagers) { + e2eTestPositionManagers(spokes[i]); + } + } + } + + /// @notice Test all reserves on one spoke, looping over ALL good collaterals, then gateway tests. + function e2eTestSpoke(ISpoke spoke) public { + Types.ReserveInfo[] memory allReserves = _getReserveInfo(spoke); + Types.ReserveInfo[] memory goodCollaterals = _getAllUsableCollaterals(allReserves); + require(goodCollaterals.length > 0, 'No usable collateral found'); + + uint256 numCollateralsToTest = 5; + numCollateralsToTest = goodCollaterals.length < numCollateralsToTest + ? goodCollaterals.length + : numCollateralsToTest; + + for (uint256 collateralIndex; collateralIndex < numCollateralsToTest; collateralIndex++) { + console.log('--- E2E: Using collateral %s ---', goodCollaterals[collateralIndex].symbol); + + uint256 spokeSnapshot = vm.snapshotState(); + + for (uint256 assetIndex; assetIndex < allReserves.length; assetIndex++) { + if (allReserves[assetIndex].paused) { + e2eTestPausedAsset({spoke: spoke, pausedAsset: allReserves[assetIndex]}); + vm.revertToState(spokeSnapshot); + continue; + } + + if (allReserves[assetIndex].frozen) { + e2eTestFrozenAsset({spoke: spoke, frozenAsset: allReserves[assetIndex]}); + vm.revertToState(spokeSnapshot); + continue; + } + + e2eTestAsset({ + spoke: spoke, + goodCollaterals: goodCollaterals, + primaryCollateralIndex: collateralIndex, + testAssetInfo: allReserves[assetIndex] + }); + vm.revertToState(spokeSnapshot); + } + } + } + + /// @notice Test all position managers on a spoke. + function e2eTestPositionManagers(ISpoke spoke) public { + e2eTestGateways(spoke); + e2eTestRegularPositionManagers(spoke); + } + + /// @notice Test all gateways on a spoke. + function e2eTestGateways(ISpoke spoke) public { + // set caps to max to simplify user ops + _setCapsToMax(spoke); + + Types.ReserveInfo[] memory allReserves = _getReserveInfo(spoke); + Types.ReserveInfo[] memory goodCollaterals = _getAllUsableCollaterals(allReserves); + Types.ReserveInfo[] memory goodDebtReserves = _getAllUsableDebtReserves(allReserves); + + // NativeTokenGateway — only if spoke lists WETH + { + INativeTokenGateway nativeGateway = AaveV4EthereumPositionManagers.NATIVE_TOKEN_GATEWAY; + (bool hasWeth, Types.ReserveInfo memory wethInfo) = _findNativeTokenReserveInfo( + nativeGateway, + spoke + ); + if (hasWeth) { + uint256 gatewaySnapshot = vm.snapshotState(); + _testNativeGateway(nativeGateway, spoke, wethInfo); + vm.revertToState(gatewaySnapshot); + } + } + + // SignatureGateway — on first usable debt reserve + collateral + if (goodCollaterals.length > 0 && goodDebtReserves.length > 0) { + uint256 gatewaySnapshot = vm.snapshotState(); + _testSignatureGateway({ + gateway: AaveV4EthereumPositionManagers.SIGNATURE_GATEWAY, + spoke: spoke, + reserveInfo: goodDebtReserves[0], + collateralInfo: goodCollaterals[0] + }); + vm.revertToState(gatewaySnapshot); + } + } + + /// @notice Test that a frozen reserve correctly reverts on supply and borrow. + function e2eTestFrozenAsset(ISpoke spoke, Types.ReserveInfo memory frozenAsset) public { + console.log('E2E: Testing frozen reserve %s (should revert)', frozenAsset.symbol); + + address oracleAddr = spoke.ORACLE(); + address user = vm.randomAddress(); + uint256 amount = _getTokenAmountByDollarValue({ + oracleAddr: oracleAddr, + reserveInfo: frozenAsset, + dollarValue: 1_000 + }); + + deal2(frozenAsset.underlying, user, amount); + + // Supply should revert with ReserveFrozen + vm.startPrank(user); + IERC20(frozenAsset.underlying).forceApprove(address(spoke), amount); + vm.expectRevert(ISpoke.ReserveFrozen.selector); + spoke.supply({reserveId: frozenAsset.reserveId, amount: amount, onBehalfOf: user}); + vm.stopPrank(); + + // Borrow should revert with ReserveFrozen (if borrowable) + if (frozenAsset.borrowable) { + vm.prank(user); + vm.expectRevert(ISpoke.ReserveFrozen.selector); + spoke.borrow({reserveId: frozenAsset.reserveId, amount: amount, onBehalfOf: user}); + } + } + + /// @notice Test all regular position managers on a spoke. + function e2eTestRegularPositionManagers(ISpoke spoke) public { + _setCapsToMax(spoke); + + Types.ReserveInfo[] memory allReserves = _getReserveInfo(spoke); + Types.ReserveInfo[] memory goodCollaterals = _getAllUsableCollaterals(allReserves); + Types.ReserveInfo[] memory goodDebtReserves = _getAllUsableDebtReserves(allReserves); + + if (goodCollaterals.length == 0 || goodDebtReserves.length == 0) { + console.log('POSITION_MANAGERS: Skipping spoke (no collateral or debt reserves)'); + return; + } + + Types.ReserveInfo memory collateralInfo = goodCollaterals[0]; + Types.ReserveInfo memory debtReserveInfo = goodDebtReserves[0]; + + _testGiverPositionManager(spoke, debtReserveInfo, collateralInfo); + _testTakerPositionManager(spoke, debtReserveInfo, collateralInfo); + _testConfigPositionManager(spoke, collateralInfo); + } + + /// @notice Test GiverPositionManager: supplyOnBehalfOf and repayOnBehalfOf. + function _testGiverPositionManager( + ISpoke spoke, + Types.ReserveInfo memory debtReserveInfo, + Types.ReserveInfo memory collateralInfo + ) internal { + uint256 snapshot = vm.snapshotState(); + console.log('GIVER_PM: Testing supplyOnBehalfOf and repayOnBehalfOf'); + + IGiverPositionManager giverPositionManager = AaveV4EthereumPositionManagers + .GIVER_POSITION_MANAGER; + address oracleAddr = spoke.ORACLE(); + address owner = makeAddr('GIVER_OWNER'); + address supplier = makeAddr('GIVER_SUPPLIER'); + + // Owner approves GiverPositionManager + vm.prank(owner); + spoke.setUserPositionManager(address(giverPositionManager), true); + + // --- supplyOnBehalfOf --- + uint256 supplyAmount = _getTokenAmountByDollarValue({ + oracleAddr: oracleAddr, + reserveInfo: collateralInfo, + dollarValue: 10_000 + }); + + uint256 ownerSupplyBefore = spoke.getUserSuppliedAssets(collateralInfo.reserveId, owner); + + vm.startPrank(supplier); + deal2(collateralInfo.underlying, supplier, supplyAmount); + IERC20(collateralInfo.underlying).forceApprove(address(giverPositionManager), supplyAmount); + giverPositionManager.supplyOnBehalfOf({ + spoke: address(spoke), + reserveId: collateralInfo.reserveId, + amount: supplyAmount, + onBehalfOf: owner + }); + vm.stopPrank(); + + uint256 ownerSupplyAfter = spoke.getUserSuppliedAssets(collateralInfo.reserveId, owner); + assertApproxEqAbs( + ownerSupplyAfter, + ownerSupplyBefore + supplyAmount, + 1, + 'GIVER_PM: supplyOnBehalfOf owner balance mismatch' + ); + + // --- repayOnBehalfOf --- + // Setup: owner needs a borrow position first + vm.prank(owner); + spoke.setUsingAsCollateral({ + reserveId: collateralInfo.reserveId, + usingAsCollateral: true, + onBehalfOf: owner + }); + + uint256 borrowAmount = _getTokenAmountByDollarValue({ + oracleAddr: oracleAddr, + reserveInfo: debtReserveInfo, + dollarValue: 1_000 + }); + _ensureLiquidity({spoke: spoke, reserveInfo: debtReserveInfo, amount: borrowAmount}); + + vm.prank(owner); + spoke.borrow({reserveId: debtReserveInfo.reserveId, amount: borrowAmount, onBehalfOf: owner}); + + uint256 ownerDebtBefore = spoke.getUserTotalDebt(debtReserveInfo.reserveId, owner); + assertGt(ownerDebtBefore, 0, 'GIVER_PM: owner should have debt before repay'); + + uint256 repayAmount = borrowAmount / 2; + vm.startPrank(supplier); + deal2(debtReserveInfo.underlying, supplier, repayAmount); + IERC20(debtReserveInfo.underlying).forceApprove(address(giverPositionManager), repayAmount); + giverPositionManager.repayOnBehalfOf({ + spoke: address(spoke), + reserveId: debtReserveInfo.reserveId, + amount: repayAmount, + onBehalfOf: owner + }); + vm.stopPrank(); + + uint256 ownerDebtAfter = spoke.getUserTotalDebt(debtReserveInfo.reserveId, owner); + assertApproxEqAbs( + ownerDebtBefore - ownerDebtAfter, + repayAmount, + 2, + 'GIVER_PM: repayOnBehalfOf debt should decrease' + ); + vm.revertToState(snapshot); + } + + /// @notice Test TakerPositionManager: withdrawOnBehalfOf and borrowOnBehalfOf. + function _testTakerPositionManager( + ISpoke spoke, + Types.ReserveInfo memory debtReserveInfo, + Types.ReserveInfo memory collateralInfo + ) internal { + uint256 snapshot = vm.snapshotState(); + console.log('TAKER_PM: Testing withdrawOnBehalfOf and borrowOnBehalfOf'); + + ITakerPositionManager takerPositionManager = AaveV4EthereumPositionManagers + .TAKER_POSITION_MANAGER; + address owner = makeAddr('TAKER_OWNER'); + address taker = makeAddr('TAKER_DELEGATEE'); + + // Owner approves TakerPositionManager + vm.prank(owner); + spoke.setUserPositionManager(address(takerPositionManager), true); + + // Supply collateral for owner + uint256 supplyAmount = _getTokenAmountByDollarValue({ + oracleAddr: spoke.ORACLE(), + reserveInfo: collateralInfo, + dollarValue: 10_000 + }); + _supply({spoke: spoke, reserveInfo: collateralInfo, user: owner, amount: supplyAmount}); + + _testTakerWithdraw(spoke, takerPositionManager, collateralInfo, owner, taker, supplyAmount / 4); + _testTakerBorrow(spoke, takerPositionManager, debtReserveInfo, collateralInfo, owner, taker); + vm.revertToState(snapshot); + } + + function _testTakerWithdraw( + ISpoke spoke, + ITakerPositionManager takerPositionManager, + Types.ReserveInfo memory collateralInfo, + address owner, + address taker, + uint256 withdrawAmount + ) internal { + vm.prank(owner); + takerPositionManager.approveWithdraw({ + spoke: address(spoke), + reserveId: collateralInfo.reserveId, + spender: taker, + amount: withdrawAmount + }); + + uint256 ownerSupplyBefore = spoke.getUserSuppliedAssets(collateralInfo.reserveId, owner); + uint256 takerBalanceBefore = IERC20(collateralInfo.underlying).balanceOf(taker); + + vm.prank(taker); + takerPositionManager.withdrawOnBehalfOf({ + spoke: address(spoke), + reserveId: collateralInfo.reserveId, + amount: withdrawAmount, + onBehalfOf: owner + }); + + assertApproxEqAbs( + ownerSupplyBefore - spoke.getUserSuppliedAssets(collateralInfo.reserveId, owner), + withdrawAmount, + 1, + 'TAKER_PM: owner supply should decrease' + ); + assertEq( + takerBalanceBefore + withdrawAmount, + IERC20(collateralInfo.underlying).balanceOf(taker), + 'TAKER_PM: taker should receive withdrawn tokens' + ); + } + + function _testTakerBorrow( + ISpoke spoke, + ITakerPositionManager takerPositionManager, + Types.ReserveInfo memory debtReserveInfo, + Types.ReserveInfo memory collateralInfo, + address owner, + address taker + ) internal { + vm.prank(owner); + spoke.setUsingAsCollateral({ + reserveId: collateralInfo.reserveId, + usingAsCollateral: true, + onBehalfOf: owner + }); + + uint256 borrowAmount = _getTokenAmountByDollarValue({ + oracleAddr: spoke.ORACLE(), + reserveInfo: debtReserveInfo, + dollarValue: 1_000 + }); + _ensureLiquidity({spoke: spoke, reserveInfo: debtReserveInfo, amount: borrowAmount}); + + vm.prank(owner); + takerPositionManager.approveBorrow({ + spoke: address(spoke), + reserveId: debtReserveInfo.reserveId, + spender: taker, + amount: borrowAmount + }); + + uint256 ownerDebtBefore = spoke.getUserTotalDebt(debtReserveInfo.reserveId, owner); + uint256 takerBalanceBefore = IERC20(debtReserveInfo.underlying).balanceOf(taker); + + vm.prank(taker); + takerPositionManager.borrowOnBehalfOf({ + spoke: address(spoke), + reserveId: debtReserveInfo.reserveId, + amount: borrowAmount, + onBehalfOf: owner + }); + + assertApproxEqAbs( + spoke.getUserTotalDebt(debtReserveInfo.reserveId, owner), + ownerDebtBefore + borrowAmount, + 2, + 'TAKER_PM: owner debt should increase' + ); + assertEq( + takerBalanceBefore + borrowAmount, + IERC20(debtReserveInfo.underlying).balanceOf(taker), + 'TAKER_PM: taker should receive borrowed tokens' + ); + } + + /// @notice Test ConfigPositionManager: setUsingAsCollateralOnBehalfOf. + function _testConfigPositionManager( + ISpoke spoke, + Types.ReserveInfo memory collateralInfo + ) internal { + uint256 snapshot = vm.snapshotState(); + console.log('CONFIG_PM: Testing setUsingAsCollateralOnBehalfOf'); + + IConfigPositionManager configPositionManager = AaveV4EthereumPositionManagers + .CONFIG_POSITION_MANAGER; + address oracleAddr = spoke.ORACLE(); + address owner = makeAddr('CONFIG_OWNER'); + address configDelegatee = makeAddr('CONFIG_DELEGATEE'); + + // Owner approves ConfigPositionManager + vm.prank(owner); + spoke.setUserPositionManager(address(configPositionManager), true); + + // Supply collateral for owner + uint256 supplyAmount = _getTokenAmountByDollarValue({ + oracleAddr: oracleAddr, + reserveInfo: collateralInfo, + dollarValue: 10_000 + }); + _supply({spoke: spoke, reserveInfo: collateralInfo, user: owner, amount: supplyAmount}); + + // Owner grants global permission to delegatee + vm.prank(owner); + configPositionManager.setGlobalPermission({ + spoke: address(spoke), + delegatee: configDelegatee, + status: true + }); + + // Delegatee enables collateral on behalf of owner + vm.prank(configDelegatee); + configPositionManager.setUsingAsCollateralOnBehalfOf({ + spoke: address(spoke), + reserveId: collateralInfo.reserveId, + usingAsCollateral: true, + onBehalfOf: owner + }); + + (bool usingAsCollateralBeforeDisable, ) = spoke.getUserReserveStatus( + collateralInfo.reserveId, + owner + ); + assertEq(usingAsCollateralBeforeDisable, true, 'CONFIG_PM: collateral should be enabled'); + + // Delegatee disables collateral on behalf of owner + vm.prank(configDelegatee); + configPositionManager.setUsingAsCollateralOnBehalfOf({ + spoke: address(spoke), + reserveId: collateralInfo.reserveId, + usingAsCollateral: false, + onBehalfOf: owner + }); + + (bool usingAsCollateralAfterDisable, ) = spoke.getUserReserveStatus( + collateralInfo.reserveId, + owner + ); + assertEq(usingAsCollateralAfterDisable, false, 'CONFIG_PM: collateral should be disabled'); + vm.revertToState(snapshot); + } + + /// @notice Test that a paused reserve correctly reverts on all actions. + function e2eTestPausedAsset(ISpoke spoke, Types.ReserveInfo memory pausedAsset) public { + console.log('E2E: Testing paused reserve %s (should revert)', pausedAsset.symbol); + + address oracleAddr = spoke.ORACLE(); + address user = vm.randomAddress(); + uint256 amount = _getTokenAmountByDollarValue({ + oracleAddr: oracleAddr, + reserveInfo: pausedAsset, + dollarValue: 1_000 + }); + + deal2(pausedAsset.underlying, user, amount); + + // Supply should revert with ReservePaused + vm.startPrank(user); + IERC20(pausedAsset.underlying).forceApprove(address(spoke), amount); + vm.expectRevert(ISpoke.ReservePaused.selector); + spoke.supply({reserveId: pausedAsset.reserveId, amount: amount, onBehalfOf: user}); + vm.stopPrank(); + + // Borrow should revert with ReservePaused + vm.prank(user); + vm.expectRevert(ISpoke.ReservePaused.selector); + spoke.borrow({reserveId: pausedAsset.reserveId, amount: amount, onBehalfOf: user}); + + // Withdraw should revert with ReservePaused + vm.prank(user); + vm.expectRevert(ISpoke.ReservePaused.selector); + spoke.withdraw({reserveId: pausedAsset.reserveId, amount: amount, onBehalfOf: user}); + + // Repay should revert with ReservePaused + vm.startPrank(user); + IERC20(pausedAsset.underlying).forceApprove(address(spoke), amount); + vm.expectRevert(ISpoke.ReservePaused.selector); + spoke.repay({reserveId: pausedAsset.reserveId, amount: amount, onBehalfOf: user}); + vm.stopPrank(); + + // Liquidation should revert with ReservePaused (paused as debt asset) + address liquidator = vm.randomAddress(); + vm.prank(liquidator); + vm.expectRevert(ISpoke.ReservePaused.selector); + spoke.liquidationCall({ + collateralReserveId: pausedAsset.reserveId, + debtReserveId: pausedAsset.reserveId, + user: user, + debtToCover: amount, + receiveShares: false + }); + + // setUsingAsCollateral should revert with ReservePaused + vm.prank(user); + vm.expectRevert(ISpoke.ReservePaused.selector); + spoke.setUsingAsCollateral({ + reserveId: pausedAsset.reserveId, + usingAsCollateral: true, + onBehalfOf: user + }); + } + + /// @notice Per-asset e2e test with randomized amounts and extra collaterals. + function e2eTestAsset( + ISpoke spoke, + Types.ReserveInfo[] memory goodCollaterals, + uint256 primaryCollateralIndex, + Types.ReserveInfo memory testAssetInfo + ) public { + Types.ReserveInfo memory collateralInfo = goodCollaterals[primaryCollateralIndex]; + console.log('E2E: Collateral %s, TestAsset %s', collateralInfo.symbol, testAssetInfo.symbol); + require(collateralInfo.collateralEnabled, 'COLLATERAL_CONFIG_MUST_BE_COLLATERAL'); + + uint256 scenarioSnapshot; + + scenarioSnapshot = vm.snapshotState(); + _testZeroAmountReverts({spoke: spoke, reserveInfo: testAssetInfo, user: vm.randomAddress()}); + vm.revertToState(scenarioSnapshot); + + scenarioSnapshot = vm.snapshotState(); + _testCaps({spoke: spoke, reserveInfo: testAssetInfo}); + vm.revertToState(scenarioSnapshot); + + // Set caps to max after cap testing for the rest of the flow + _setCapsToMax(spoke); + + address collateralSupplier = makeAddr('COLLATERAL_SUPPLIER'); + address testAssetSupplier = makeAddr('TEST_ASSET_SUPPLIER'); + + uint256 testAssetAmount = _setupPositions({ + spoke: spoke, + goodCollaterals: goodCollaterals, + primaryCollateralIndex: primaryCollateralIndex, + testAssetInfo: testAssetInfo, + collateralSupplier: collateralSupplier, + testAssetSupplier: testAssetSupplier + }); + + scenarioSnapshot = vm.snapshotState(); + _testPartialWithdrawal({ + spoke: spoke, + testAssetInfo: testAssetInfo, + testAssetSupplier: testAssetSupplier, + testAssetAmount: testAssetAmount + }); + vm.revertToState(scenarioSnapshot); + + scenarioSnapshot = vm.snapshotState(); + _testFullWithdrawal({ + spoke: spoke, + testAssetInfo: testAssetInfo, + testAssetSupplier: testAssetSupplier + }); + vm.revertToState(scenarioSnapshot); + + if (testAssetInfo.borrowable) { + scenarioSnapshot = vm.snapshotState(); + uint256 borrowCeiling = _setupBorrows( + spoke, + testAssetInfo, + collateralSupplier, + testAssetAmount + ); + if (borrowCeiling > 0) { + uint256 postBorrowSnapshot = vm.snapshotState(); + + // Partial repay + _testPartialRepay(spoke, testAssetInfo, collateralSupplier); + vm.revertToState(postBorrowSnapshot); + + // Full repay + _testFullRepay(spoke, testAssetInfo, collateralSupplier); + vm.revertToState(postBorrowSnapshot); + + // Repay after interest accrual + _testRepayAfterInterest(spoke, testAssetInfo, collateralSupplier); + vm.revertToState(postBorrowSnapshot); + + // Liquidation + _testLiquidation(spoke, collateralInfo, testAssetInfo, collateralSupplier); + vm.revertToState(postBorrowSnapshot); + } + vm.revertToState(scenarioSnapshot); + } else { + // Non-borrowable: verify borrow reverts with ReserveNotBorrowable + vm.prank(collateralSupplier); + vm.expectRevert(ISpoke.ReserveNotBorrowable.selector); + spoke.borrow({ + reserveId: testAssetInfo.reserveId, + amount: testAssetAmount, + onBehalfOf: collateralSupplier + }); + } + + // Collateral toggle: disable all, verify borrow fails, re-enable all, verify borrow works + if (collateralInfo.collateralEnabled && testAssetInfo.borrowable) { + scenarioSnapshot = vm.snapshotState(); + _testCollateralToggle({ + spoke: spoke, + goodCollaterals: goodCollaterals, + testAssetInfo: testAssetInfo, + collateralSupplier: collateralSupplier, + testAssetAmount: testAssetAmount + }); + vm.revertToState(scenarioSnapshot); + } + } + + /// @notice Test all tokenization spokes in the array. + function e2eTestAllTokenizationSpokes(address[] memory tokenizationSpokes) public { + for (uint256 i; i < tokenizationSpokes.length; i++) { + console.log('--- E2E: Testing tokenization spoke %s ---', tokenizationSpokes[i]); + console.log('------------------------------------------'); + e2eTestTokenizationSpoke(ITokenizationSpoke(tokenizationSpokes[i])); + } + } + + /// @notice Run all tokenization spoke scenarios for a single spoke. + function e2eTestTokenizationSpoke(ITokenizationSpoke tokenizationSpoke) public { + Types.ReserveInfo memory reserveInfo = _getTokenizationReserveInfo(tokenizationSpoke); + console.log('E2E: TokenizationSpoke asset: %s', reserveInfo.symbol); + + uint256 snapshot = vm.snapshotState(); + + _testTokenizationAddCap(tokenizationSpoke, reserveInfo); + vm.revertToState(snapshot); + + _setTokenizationCapsToMax(tokenizationSpoke); + snapshot = vm.snapshotState(); + + uint256 maxAddAmount = 10_000 * 10 ** reserveInfo.decimals; + + _testTokenizationDepositWithdraw({ + tokenizationSpoke: tokenizationSpoke, + reserveInfo: reserveInfo, + maxAddAmount: maxAddAmount + }); + vm.revertToState(snapshot); + + _testTokenizationMintRedeem({ + tokenizationSpoke: tokenizationSpoke, + reserveInfo: reserveInfo, + maxAddAmount: maxAddAmount + }); + vm.revertToState(snapshot); + + _testTokenizationPermitDeposit({ + tokenizationSpoke: tokenizationSpoke, + reserveInfo: reserveInfo, + maxAddAmount: maxAddAmount + }); + vm.revertToState(snapshot); + + _testTokenizationTimeSkip({ + tokenizationSpoke: tokenizationSpoke, + reserveInfo: reserveInfo, + maxAddAmount: maxAddAmount + }); + vm.revertToState(snapshot); + + _testTokenizationTransferAndWithdraw({ + tokenizationSpoke: tokenizationSpoke, + reserveInfo: reserveInfo, + maxAddAmount: maxAddAmount + }); + vm.revertToState(snapshot); + } + + /// @notice Validate that the executor has no storage changes after payload execution. + function _validateNoExecutorStorageChange( + string memory stateDiffJson, + address executor + ) internal view virtual { + string memory executorKey = Strings.toHexString(uint160(executor), 20); + string memory stateDiffPath = string.concat('.', executorKey, '.stateDiff'); + if (vm.keyExistsJson(stateDiffJson, stateDiffPath)) { + string[] memory slots = vm.parseJsonKeys(stateDiffJson, stateDiffPath); + require( + slots.length == 0, + string.concat( + 'EXECUTOR_MUST_NOT_HAVE_STORAGE_CHANGES: ', + Strings.toString(slots.length), + ' slot(s) modified on executor ', + executorKey + ) + ); + } + } +} diff --git a/src/dependencies/v4/AaveV4EthereumHelpers.sol b/src/dependencies/v4/AaveV4EthereumHelpers.sol new file mode 100644 index 00000000..41f20ddf --- /dev/null +++ b/src/dependencies/v4/AaveV4EthereumHelpers.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IHub, ISpoke, ITokenizationSpoke} from 'aave-address-book/AaveV4.sol'; +import {AaveV4EthereumHubs, AaveV4EthereumSpokes, AaveV4EthereumTokenizationSpokes} from 'aave-address-book/AaveV4Ethereum.sol'; + +/// @dev Helper that returns all hubs as an array (address-book only exposes individual constants). +library AaveV4EthereumHubHelpers { + function getHubs() internal pure returns (IHub[] memory) { + IHub[] memory hubs = new IHub[](3); + hubs[0] = AaveV4EthereumHubs.CORE_HUB; + hubs[1] = AaveV4EthereumHubs.PLUS_HUB; + hubs[2] = AaveV4EthereumHubs.PRIME_HUB; + return hubs; + } +} + +/// @dev Helper that returns spoke arrays (address-book only exposes individual constants). +library AaveV4EthereumSpokeHelpers { + function getUserSpokes() internal pure returns (ISpoke[] memory) { + ISpoke[] memory spokes = new ISpoke[](10); + spokes[0] = AaveV4EthereumSpokes.MAIN_SPOKE; + spokes[1] = AaveV4EthereumSpokes.BLUECHIP_SPOKE; + spokes[2] = AaveV4EthereumSpokes.ETHENA_CORRELATED_SPOKE; + spokes[3] = AaveV4EthereumSpokes.ETHENA_ECOSYSTEM_SPOKE; + spokes[4] = AaveV4EthereumSpokes.ETHERFI_E_SPOKE; + spokes[5] = AaveV4EthereumSpokes.FOREX_SPOKE; + spokes[6] = AaveV4EthereumSpokes.GOLD_SPOKE; + spokes[7] = AaveV4EthereumSpokes.KELP_E_SPOKE; + spokes[8] = AaveV4EthereumSpokes.LIDO_E_SPOKE; + spokes[9] = AaveV4EthereumSpokes.LOMBARD_BTC_SPOKE; + return spokes; + } +} + +/// @dev Helper that returns all tokenization spokes as an address array. +library AaveV4EthereumTokenizationSpokeHelpers { + function getTokenizationSpokes() internal pure returns (address[] memory) { + address[] memory spokes = new address[](31); + // Core Hub + spokes[0] = address(AaveV4EthereumTokenizationSpokes.CORE_WETH_TOKENIZATION_SPOKE); + spokes[1] = address(AaveV4EthereumTokenizationSpokes.CORE_wstETH_TOKENIZATION_SPOKE); + spokes[2] = address(AaveV4EthereumTokenizationSpokes.CORE_weETH_TOKENIZATION_SPOKE); + spokes[3] = address(AaveV4EthereumTokenizationSpokes.CORE_rsETH_TOKENIZATION_SPOKE); + spokes[4] = address(AaveV4EthereumTokenizationSpokes.CORE_WBTC_TOKENIZATION_SPOKE); + spokes[5] = address(AaveV4EthereumTokenizationSpokes.CORE_cbBTC_TOKENIZATION_SPOKE); + spokes[6] = address(AaveV4EthereumTokenizationSpokes.CORE_LBTC_TOKENIZATION_SPOKE); + spokes[7] = address(AaveV4EthereumTokenizationSpokes.CORE_USDT_TOKENIZATION_SPOKE); + spokes[8] = address(AaveV4EthereumTokenizationSpokes.CORE_USDC_TOKENIZATION_SPOKE); + spokes[9] = address(AaveV4EthereumTokenizationSpokes.CORE_LINK_TOKENIZATION_SPOKE); + spokes[10] = address(AaveV4EthereumTokenizationSpokes.CORE_AAVE_TOKENIZATION_SPOKE); + spokes[11] = address(AaveV4EthereumTokenizationSpokes.CORE_GHO_TOKENIZATION_SPOKE); + spokes[12] = address(AaveV4EthereumTokenizationSpokes.CORE_EURC_TOKENIZATION_SPOKE); + spokes[13] = address(AaveV4EthereumTokenizationSpokes.CORE_RLUSD_TOKENIZATION_SPOKE); + spokes[14] = address(AaveV4EthereumTokenizationSpokes.CORE_USDG_TOKENIZATION_SPOKE); + spokes[15] = address(AaveV4EthereumTokenizationSpokes.CORE_frxUSD_TOKENIZATION_SPOKE); + spokes[16] = address(AaveV4EthereumTokenizationSpokes.CORE_XAUt_TOKENIZATION_SPOKE); + // Plus Hub + spokes[17] = address(AaveV4EthereumTokenizationSpokes.PLUS_USDT_TOKENIZATION_SPOKE); + spokes[18] = address(AaveV4EthereumTokenizationSpokes.PLUS_USDC_TOKENIZATION_SPOKE); + spokes[19] = address(AaveV4EthereumTokenizationSpokes.PLUS_GHO_TOKENIZATION_SPOKE); + spokes[20] = address(AaveV4EthereumTokenizationSpokes.PLUS_USDe_TOKENIZATION_SPOKE); + spokes[21] = address(AaveV4EthereumTokenizationSpokes.PLUS_sUSDe_TOKENIZATION_SPOKE); + spokes[22] = address( + AaveV4EthereumTokenizationSpokes.PLUS_PT_sUSDE_7MAY2026_TOKENIZATION_SPOKE + ); + spokes[23] = address(AaveV4EthereumTokenizationSpokes.PLUS_PT_USDe_7MAY2026_TOKENIZATION_SPOKE); + // Prime Hub + spokes[24] = address(AaveV4EthereumTokenizationSpokes.PRIME_WETH_TOKENIZATION_SPOKE); + spokes[25] = address(AaveV4EthereumTokenizationSpokes.PRIME_wstETH_TOKENIZATION_SPOKE); + spokes[26] = address(AaveV4EthereumTokenizationSpokes.PRIME_WBTC_TOKENIZATION_SPOKE); + spokes[27] = address(AaveV4EthereumTokenizationSpokes.PRIME_cbBTC_TOKENIZATION_SPOKE); + spokes[28] = address(AaveV4EthereumTokenizationSpokes.PRIME_USDT_TOKENIZATION_SPOKE); + spokes[29] = address(AaveV4EthereumTokenizationSpokes.PRIME_USDC_TOKENIZATION_SPOKE); + spokes[30] = address(AaveV4EthereumTokenizationSpokes.PRIME_GHO_TOKENIZATION_SPOKE); + return spokes; + } +} diff --git a/src/dependencies/v4/Actions.sol b/src/dependencies/v4/Actions.sol new file mode 100644 index 00000000..182a8d95 --- /dev/null +++ b/src/dependencies/v4/Actions.sol @@ -0,0 +1,467 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; +import {CommonTestBase} from 'src/CommonTestBase.sol'; +import {ISpoke} from 'aave-address-book/AaveV4.sol'; +import {IHubBase} from 'aave-v4/hub/interfaces/IHubBase.sol'; +import {Types} from 'src/dependencies/v4/Types.sol'; + +/// @title Actions +/// @notice Low-level spoke actions with hub and spoke accounting assertions. +abstract contract Actions is CommonTestBase { + using SafeERC20 for IERC20; + + uint256 constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; + uint256 constant MAX_DEAL_UNIT = 1e12; // whole units not accounting for token decimals + + function _getUserAccounting( + ISpoke spoke, + uint256 reserveId, + address user + ) internal view returns (Types.Accounting memory) { + (uint256 drawnDebt, uint256 premiumDebt) = spoke.getUserDebt(reserveId, user); + ISpoke.UserPosition memory position = spoke.getUserPosition(reserveId, user); + return + Types.Accounting({ + collateralShares: position.suppliedShares, + collateralAssets: spoke.getUserSuppliedAssets(reserveId, user), + drawnDebt: drawnDebt, + premiumDebt: premiumDebt, + totalDebt: spoke.getUserTotalDebt(reserveId, user), + drawnShares: position.drawnShares, + premiumShares: position.premiumShares, + premiumOffsetRay: position.premiumOffsetRay + }); + } + + function _getReserveAccounting( + ISpoke spoke, + Types.ReserveInfo memory reserveInfo + ) internal view returns (Types.Accounting memory) { + IHubBase hub = IHubBase(reserveInfo.hub); + uint16 assetId = reserveInfo.assetId; + (uint256 drawnDebt, uint256 premiumDebt) = hub.getSpokeOwed(assetId, address(spoke)); + (uint256 premiumShares, int256 premiumOffsetRay) = hub.getSpokePremiumData( + assetId, + address(spoke) + ); + return + Types.Accounting({ + collateralShares: spoke.getReserveSuppliedShares(reserveInfo.reserveId), + collateralAssets: spoke.getReserveSuppliedAssets(reserveInfo.reserveId), + drawnDebt: drawnDebt, + premiumDebt: premiumDebt, + totalDebt: spoke.getReserveTotalDebt(reserveInfo.reserveId), + drawnShares: hub.getSpokeDrawnShares(assetId, address(spoke)), + premiumShares: premiumShares, + premiumOffsetRay: premiumOffsetRay + }); + } + + function _getSpokeOnHubAccounting( + ISpoke spoke, + Types.ReserveInfo memory reserveInfo + ) internal view returns (Types.Accounting memory) { + IHubBase hub = IHubBase(reserveInfo.hub); + uint16 assetId = reserveInfo.assetId; + address spokeAddr = address(spoke); + (uint256 spokeDrawnOwed, uint256 spokePremiumOwed) = hub.getSpokeOwed(assetId, spokeAddr); + (uint256 premiumShares, int256 premiumOffsetRay) = hub.getSpokePremiumData(assetId, spokeAddr); + return + Types.Accounting({ + collateralShares: hub.getSpokeAddedShares(assetId, spokeAddr), + collateralAssets: hub.getSpokeAddedAssets(assetId, spokeAddr), + drawnDebt: spokeDrawnOwed, + premiumDebt: spokePremiumOwed, + totalDebt: hub.getSpokeTotalOwed(assetId, spokeAddr), + drawnShares: hub.getSpokeDrawnShares(assetId, spokeAddr), + premiumShares: premiumShares, + premiumOffsetRay: premiumOffsetRay + }); + } + + function _getPositionSnapshot( + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + address user + ) internal view returns (Types.PositionSnapshot memory) { + return + Types.PositionSnapshot({ + user: _getUserAccounting({spoke: spoke, reserveId: reserveInfo.reserveId, user: user}), + reserve: _getReserveAccounting({spoke: spoke, reserveInfo: reserveInfo}), + spokeOnHub: _getSpokeOnHubAccounting({spoke: spoke, reserveInfo: reserveInfo}) + }); + } + + /// @notice Skip time, assert debt accounting grew as expected, then revert. + function _skipTimeAndCheckAccounting( + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + address user, + uint256 skipDays + ) internal { + uint256 snapshot = vm.snapshotState(); + + Types.PositionSnapshot memory snapshotBefore = _getPositionSnapshot(spoke, reserveInfo, user); + + skip(skipDays * 1 days); + + Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, reserveInfo, user); + + // User debt should not decrease over time + assertGe( + snapshotAfter.user.totalDebt, + snapshotBefore.user.totalDebt, + 'TIME_SKIP: user total debt decreased' + ); + assertGe( + snapshotAfter.user.drawnDebt, + snapshotBefore.user.drawnDebt, + 'TIME_SKIP: user drawn debt decreased' + ); + + // Reserve debt should not decrease over time + assertGe( + snapshotAfter.reserve.totalDebt, + snapshotBefore.reserve.totalDebt, + 'TIME_SKIP: reserve total debt decreased' + ); + assertGe( + snapshotAfter.reserve.drawnDebt, + snapshotBefore.reserve.drawnDebt, + 'TIME_SKIP: reserve drawn debt decreased' + ); + + // Hub spoke owed should not decrease over time + assertGe( + snapshotAfter.spokeOnHub.totalDebt, + snapshotBefore.spokeOnHub.totalDebt, + 'TIME_SKIP: hub spoke owed decreased' + ); + assertGe( + snapshotAfter.spokeOnHub.drawnDebt, + snapshotBefore.spokeOnHub.drawnDebt, + 'TIME_SKIP: hub spoke drawn decreased' + ); + + // Hub drawn index should have grown + IHubBase hub = IHubBase(reserveInfo.hub); + uint256 drawnIndexAfter = hub.getAssetDrawnIndex(reserveInfo.assetId); + assertGt(drawnIndexAfter, 1e27, 'TIME_SKIP: drawn index should be greater than 1e27'); + + vm.revertToState(snapshot); + } + + function _supply( + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + address user, + uint256 amount + ) internal { + require(!reserveInfo.paused, 'SUPPLY: PAUSED_RESERVE'); + require(!reserveInfo.frozen, 'SUPPLY: FROZEN_RESERVE'); + + Types.PositionSnapshot memory snapshotBefore = _getPositionSnapshot(spoke, reserveInfo, user); + + vm.startPrank(user); + deal2(reserveInfo.underlying, user, amount); + IERC20(reserveInfo.underlying).forceApprove(address(spoke), amount); + _logAction('SUPPLY', reserveInfo.symbol, amount); + (uint256 returnedShares, uint256 returnedAssets) = spoke.supply({ + reserveId: reserveInfo.reserveId, + amount: amount, + onBehalfOf: user + }); + vm.stopPrank(); + + Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, reserveInfo, user); + + assertEq(returnedAssets, amount, 'SUPPLY: returnedAssets mismatch'); + + // User + assertApproxEqAbs( + snapshotAfter.user.collateralAssets, + snapshotBefore.user.collateralAssets + amount, + 1, + 'SUPPLY: user assets mismatch' + ); + assertEq( + snapshotAfter.user.collateralShares, + snapshotBefore.user.collateralShares + returnedShares, + 'SUPPLY: user shares mismatch' + ); + // Spoke accounting on hub + assertApproxEqAbs( + snapshotAfter.spokeOnHub.collateralAssets, + snapshotBefore.spokeOnHub.collateralAssets + amount, + 1, + 'SUPPLY: hub assets mismatch' + ); + uint256 expectedAddedShares = IHubBase(reserveInfo.hub).previewAddByAssets( + reserveInfo.assetId, + amount + ); + assertEq(returnedShares, expectedAddedShares, 'SUPPLY: returnedShares mismatch'); + assertEq( + snapshotAfter.spokeOnHub.collateralShares, + snapshotBefore.spokeOnHub.collateralShares + expectedAddedShares, + 'SUPPLY: hub shares mismatch' + ); + } + + function _withdraw( + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + address user, + uint256 amount + ) internal { + Types.PositionSnapshot memory snapshotBefore = _getPositionSnapshot(spoke, reserveInfo, user); + + vm.startPrank(user); + _logAction('WITHDRAW', reserveInfo.symbol, amount); + (uint256 returnedShares, uint256 withdrawnAmount) = spoke.withdraw({ + reserveId: reserveInfo.reserveId, + amount: amount, + onBehalfOf: user + }); + vm.stopPrank(); + + Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, reserveInfo, user); + + if (amount >= snapshotBefore.user.collateralAssets) { + assertEq(snapshotAfter.user.collateralAssets, 0, 'WITHDRAW: user assets should be zero'); + assertEq(snapshotAfter.user.collateralShares, 0, 'WITHDRAW: user shares should be zero'); + } else { + assertApproxEqAbs( + snapshotAfter.user.collateralAssets, + snapshotBefore.user.collateralAssets - withdrawnAmount, + 1, + 'WITHDRAW: user assets mismatch' + ); + assertEq( + snapshotBefore.user.collateralShares - snapshotAfter.user.collateralShares, + returnedShares, + 'WITHDRAW: user shares delta mismatch' + ); + } + // Hub spoke + assertApproxEqAbs( + snapshotBefore.spokeOnHub.collateralAssets - snapshotAfter.spokeOnHub.collateralAssets, + withdrawnAmount, + 1, + 'WITHDRAW: hub assets mismatch' + ); + assertEq( + snapshotBefore.spokeOnHub.collateralShares - snapshotAfter.spokeOnHub.collateralShares, + returnedShares, + 'WITHDRAW: hub shares mismatch' + ); + } + + function _borrow( + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + address user, + uint256 amount + ) internal { + Types.PositionSnapshot memory snapshotBefore = _getPositionSnapshot(spoke, reserveInfo, user); + uint256 expectedDrawnShares = IHubBase(reserveInfo.hub).previewDrawByAssets( + reserveInfo.assetId, + amount + ); + + _logAction('BORROW', reserveInfo.symbol, amount); + vm.prank(user); + (uint256 returnedShares, uint256 returnedAssets) = spoke.borrow({ + reserveId: reserveInfo.reserveId, + amount: amount, + onBehalfOf: user + }); + + Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, reserveInfo, user); + + assertEq(returnedAssets, amount, 'BORROW: returnedAssets mismatch'); + assertEq(returnedShares, expectedDrawnShares, 'BORROW: returnedShares mismatch'); + + // User debt - up to 2 wei diff due to premium/drawn debt + assertApproxEqAbs( + snapshotAfter.user.totalDebt, + snapshotBefore.user.totalDebt + amount, + 2, + 'BORROW: user debt mismatch' + ); + assertApproxEqAbs( + snapshotAfter.user.drawnDebt, + snapshotBefore.user.drawnDebt + returnedAssets, + 2, + 'BORROW: user drawn debt mismatch' + ); + // Hub spoke - up to 2 wei diff due to premium/drawn debt + assertApproxEqAbs( + snapshotAfter.spokeOnHub.totalDebt, + snapshotBefore.spokeOnHub.totalDebt + amount, + 2, + 'BORROW: hub debt mismatch' + ); + assertEq( + snapshotAfter.spokeOnHub.drawnShares, + snapshotBefore.spokeOnHub.drawnShares + expectedDrawnShares, + 'BORROW: hub drawn shares mismatch' + ); + } + + function _repay( + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + address user, + uint256 amount + ) internal { + Types.PositionSnapshot memory snapshotBefore = _getPositionSnapshot(spoke, reserveInfo, user); + uint256 effectiveRepayAmount = amount >= snapshotBefore.user.totalDebt + ? snapshotBefore.user.totalDebt + : amount; + uint256 drawnRepayAmount = effectiveRepayAmount > snapshotBefore.user.drawnDebt + ? snapshotBefore.user.drawnDebt + : effectiveRepayAmount; + uint256 expectedRestoredShares = IHubBase(reserveInfo.hub).previewRestoreByAssets( + reserveInfo.assetId, + drawnRepayAmount + ); + + vm.startPrank(user); + // deal enough to cover full repay, capped to avoid overflow + uint256 maxDeal = _maxDealAmount(reserveInfo.decimals); + deal2(reserveInfo.underlying, user, maxDeal); + IERC20(reserveInfo.underlying).forceApprove(address(spoke), maxDeal); + _logAction('REPAY', reserveInfo.symbol, amount); + (uint256 returnedShares, uint256 returnedAssets) = spoke.repay({ + reserveId: reserveInfo.reserveId, + amount: amount, + onBehalfOf: user + }); + vm.stopPrank(); + + Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, reserveInfo, user); + + assertEq(returnedAssets, effectiveRepayAmount, 'REPAY: returnedAssets mismatch'); + assertEq(returnedShares, expectedRestoredShares, 'REPAY: returnedShares mismatch'); + + if (amount >= snapshotBefore.user.totalDebt) { + assertEq(snapshotAfter.user.totalDebt, 0, 'REPAY: user debt should be zero'); + } else { + assertApproxEqAbs( + stdMath.delta(snapshotAfter.user.totalDebt, snapshotBefore.user.totalDebt), + amount, + 2, + 'REPAY: user debt mismatch' + ); + } + // Hub spoke - up to 2 wei diff due to premium/drawn debt + assertApproxEqAbs( + stdMath.delta(snapshotBefore.spokeOnHub.totalDebt, snapshotAfter.spokeOnHub.totalDebt), + effectiveRepayAmount, + 2, + 'REPAY: hub debt mismatch' + ); + assertEq( + stdMath.delta(snapshotBefore.spokeOnHub.drawnShares, snapshotAfter.spokeOnHub.drawnShares), + expectedRestoredShares, + 'REPAY: hub drawn shares mismatch' + ); + } + + function _liquidationCall( + ISpoke spoke, + Types.ReserveInfo memory collateralInfo, + Types.ReserveInfo memory debtInfo, + address liquidator, + address borrower, + uint256 debtToCover, + bool receiveShares + ) internal { + Types.PositionSnapshot memory collateralSnapshotBefore = _getPositionSnapshot( + spoke, + collateralInfo, + borrower + ); + Types.PositionSnapshot memory debtSnapshotBefore = _getPositionSnapshot( + spoke, + debtInfo, + borrower + ); + assertGt(debtSnapshotBefore.user.totalDebt, 0, 'LIQUIDATE: borrower has no debt'); + + vm.startPrank(liquidator); + uint256 dealAmount = _maxDealAmount(debtInfo.decimals); + deal2(debtInfo.underlying, liquidator, dealAmount); + IERC20(debtInfo.underlying).forceApprove(address(spoke), debtToCover); + + if (debtToCover == UINT256_MAX) { + console.log( + 'LIQUIDATE: %s, DebtToCover: UINT256_MAX, TotalDebt: %e', + debtInfo.symbol, + debtSnapshotBefore.user.totalDebt + ); + } else { + console.log( + 'LIQUIDATE: %s, DebtToCover: %e, TotalDebt: %e', + debtInfo.symbol, + debtToCover, + debtSnapshotBefore.user.totalDebt + ); + } + + spoke.liquidationCall({ + collateralReserveId: collateralInfo.reserveId, + debtReserveId: debtInfo.reserveId, + user: borrower, + debtToCover: debtToCover, + receiveShares: receiveShares + }); + vm.stopPrank(); + + Types.PositionSnapshot memory collateralSnapshotAfter = _getPositionSnapshot( + spoke, + collateralInfo, + borrower + ); + Types.PositionSnapshot memory debtSnapshotAfter = _getPositionSnapshot( + spoke, + debtInfo, + borrower + ); + + // Debt decreased + assertLt( + debtSnapshotAfter.user.totalDebt, + debtSnapshotBefore.user.totalDebt, + 'LIQUIDATE: debt did not decrease' + ); + assertLt( + debtSnapshotAfter.spokeOnHub.totalDebt, + debtSnapshotBefore.spokeOnHub.totalDebt, + 'LIQUIDATE: hub debt did not decrease' + ); + // Collateral decreased + assertLt( + collateralSnapshotAfter.user.collateralAssets, + collateralSnapshotBefore.user.collateralAssets, + 'LIQUIDATE: collateral did not decrease' + ); + } + + function _maxDealAmount(uint8 decimals) internal pure returns (uint256) { + return MAX_DEAL_UNIT * 10 ** decimals; + } + + function _logAction(string memory action, string memory symbol, uint256 amount) internal pure { + if (amount == UINT256_MAX) { + console.log('%s: %s, Amount: UINT256_MAX', action, symbol); + } else { + console.log('%s: %s, Amount: %e', action, symbol, amount); + } + } +} diff --git a/src/dependencies/v4/GatewayScenarios.sol b/src/dependencies/v4/GatewayScenarios.sol new file mode 100644 index 00000000..cd2556ac --- /dev/null +++ b/src/dependencies/v4/GatewayScenarios.sol @@ -0,0 +1,1012 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; +import {ISpoke, IAaveOracle, INativeTokenGateway, ISignatureGateway} from 'aave-address-book/AaveV4.sol'; +import {IHubBase} from 'aave-v4/hub/interfaces/IHubBase.sol'; +import {Types} from 'src/dependencies/v4/Types.sol'; +import {Helpers} from 'src/dependencies/v4/Helpers.sol'; + +/// @title GatewayScenarios +/// @notice E2E test scenarios for NativeTokenGateway and SignatureGateway. +abstract contract GatewayScenarios is Helpers { + using SafeERC20 for IERC20; + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /// @notice Find the ReserveInfo for the native token wrapper (WETH) on a spoke. + /// Returns (true, info) if found, (false, empty) if not. + function _findNativeTokenReserveInfo( + INativeTokenGateway gateway, + ISpoke spoke + ) internal view returns (bool found, Types.ReserveInfo memory info) { + address weth = gateway.NATIVE_TOKEN_WRAPPER(); + Types.ReserveInfo[] memory allReserves = _getReserveInfo(spoke); + for (uint256 i; i < allReserves.length; i++) { + if (allReserves[i].underlying == weth) { + return (true, allReserves[i]); + } + } + return (false, info); + } + + /// @notice Build EIP-712 digest and sign for signature gateway. + function _signForGateway( + ISignatureGateway gateway, + uint256 privateKey, + bytes32 structHash + ) internal view returns (bytes memory) { + bytes32 digest = keccak256( + abi.encodePacked('\x19\x01', gateway.DOMAIN_SEPARATOR(), structHash) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + return abi.encodePacked(r, s, v); + } + + // ------------------------------------------------------------------------- + // NativeTokenGateway scenario + // ------------------------------------------------------------------------- + + /// @dev Test supply, withdraw, borrow, repay via NativeTokenGateway. + function _testNativeGateway( + INativeTokenGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory wethInfo + ) internal { + console.log('NATIVE_GATEWAY: Testing on spoke with WETH reserveId=%s', wethInfo.reserveId); + uint256 gatewaySnapshot = vm.snapshotState(); + + address user = vm.randomAddress(); + uint256 amount = _halfToken(wethInfo.decimals); + + // Authorize gateway as position manager for user + vm.prank(user); + spoke.setUserPositionManager(address(gateway), true); + + _setupPositions({ + gateway: gateway, + spoke: spoke, + wethInfo: wethInfo, + user: user, + amount: amount + }); + + _testWithdrawNative({ + gateway: gateway, + spoke: spoke, + wethInfo: wethInfo, + user: user, + amount: amount + }); + + // --- Setup collateral for borrow --- + if (wethInfo.borrowable) { + _testBorrowRepayNative({ + gateway: gateway, + spoke: spoke, + wethInfo: wethInfo, + user: user, + amount: amount + }); + } + vm.revertToState(gatewaySnapshot); + } + + function _setupPositions( + INativeTokenGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory wethInfo, + address user, + uint256 amount + ) internal { + uint256 snapshot = vm.snapshotState(); + _supplyNative({gateway: gateway, spoke: spoke, wethInfo: wethInfo, user: user, amount: amount}); + vm.revertToState(snapshot); + _supplyAsCollateralNative({ + gateway: gateway, + spoke: spoke, + wethInfo: wethInfo, + user: user, + amount: amount + }); + } + + function _supplyNative( + INativeTokenGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory wethInfo, + address user, + uint256 amount + ) internal { + uint256 snapshot = vm.snapshotState(); + Types.PositionSnapshot memory snapshotBefore = _getPositionSnapshot(spoke, wethInfo, user); + uint256 sharesSupplied; + uint256 amountSupplied; + + { + vm.deal(user, amount); + vm.prank(user); + _logAction('NATIVE_SUPPLY', wethInfo.symbol, amount); + (sharesSupplied, amountSupplied) = gateway.supplyNative{value: amount}( + address(spoke), + wethInfo.reserveId, + amount + ); + assertEq(amountSupplied, amount, 'NATIVE_SUPPLY: amount mismatch'); + assertEq(user.balance, 0, 'NATIVE_SUPPLY: user ETH not fully consumed'); + } + + Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, wethInfo, user); + assertApproxEqAbs( + stdMath.delta(snapshotAfter.user.collateralAssets, snapshotBefore.user.collateralAssets), + amountSupplied, + 1, + 'NATIVE_SUPPLY: user assets mismatch' + ); + assertEq( + stdMath.delta(snapshotAfter.user.collateralShares, snapshotBefore.user.collateralShares), + sharesSupplied, + 'NATIVE_SUPPLY: user shares mismatch' + ); + assertApproxEqAbs( + stdMath.delta( + snapshotAfter.spokeOnHub.collateralAssets, + snapshotBefore.spokeOnHub.collateralAssets + ), + amountSupplied, + 1, + 'NATIVE_SUPPLY: hub assets mismatch' + ); + vm.revertToState(snapshot); + } + + function _supplyAsCollateralNative( + INativeTokenGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory wethInfo, + address user, + uint256 amount + ) internal { + Types.PositionSnapshot memory snapshotBefore = _getPositionSnapshot(spoke, wethInfo, user); + uint256 sharesSupplied; + uint256 amountSupplied; + + { + vm.deal(user, amount); + vm.prank(user); + _logAction('NATIVE_SUPPLY_AS_COLLATERAL', wethInfo.symbol, amount); + (sharesSupplied, amountSupplied) = gateway.supplyAsCollateralNative{value: amount}( + address(spoke), + wethInfo.reserveId, + amount + ); + assertEq(amountSupplied, amount, 'NATIVE_SUPPLY_AS_COLLATERAL: amount mismatch'); + assertEq(user.balance, 0, 'NATIVE_SUPPLY_AS_COLLATERAL: user ETH not fully consumed'); + } + + Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, wethInfo, user); + assertApproxEqAbs( + stdMath.delta(snapshotAfter.user.collateralAssets, snapshotBefore.user.collateralAssets), + amountSupplied, + 1, + 'NATIVE_SUPPLY_AS_COLLATERAL: user assets mismatch' + ); + assertEq( + stdMath.delta(snapshotAfter.user.collateralShares, snapshotBefore.user.collateralShares), + sharesSupplied, + 'NATIVE_SUPPLY_AS_COLLATERAL: user shares mismatch' + ); + assertApproxEqAbs( + stdMath.delta( + snapshotAfter.spokeOnHub.collateralAssets, + snapshotBefore.spokeOnHub.collateralAssets + ), + amountSupplied, + 1, + 'NATIVE_SUPPLY_AS_COLLATERAL: hub assets mismatch' + ); + } + + function _testWithdrawNative( + INativeTokenGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory wethInfo, + address user, + uint256 amount + ) internal { + uint256 snapshot = vm.snapshotState(); + // --- Partial withdraw native --- + uint256 withdrawAmount = vm.randomUint(1, amount); + _withdrawNative({ + gateway: gateway, + spoke: spoke, + wethInfo: wethInfo, + user: user, + withdrawAmount: withdrawAmount + }); + vm.revertToState(snapshot); + // --- Full withdraw native --- + _withdrawNative({ + gateway: gateway, + spoke: spoke, + wethInfo: wethInfo, + user: user, + withdrawAmount: UINT256_MAX + }); + vm.revertToState(snapshot); + } + + function _withdrawNative( + INativeTokenGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory wethInfo, + address user, + uint256 withdrawAmount + ) internal { + Types.PositionSnapshot memory snapshotBefore = _getPositionSnapshot(spoke, wethInfo, user); + uint256 sharesWithdrawn; + uint256 expectedWithdrawnAmount; + + { + uint256 ethBefore = user.balance; + vm.prank(user); + _logAction('NATIVE_WITHDRAW', wethInfo.symbol, withdrawAmount); + uint256 amountWithdrawn; + (sharesWithdrawn, amountWithdrawn) = gateway.withdrawNative( + address(spoke), + wethInfo.reserveId, + withdrawAmount + ); + expectedWithdrawnAmount = withdrawAmount; + if (withdrawAmount == UINT256_MAX) { + assertEq( + amountWithdrawn, + snapshotBefore.user.collateralAssets, + 'NATIVE_WITHDRAW: amount mismatch' + ); + expectedWithdrawnAmount = amountWithdrawn; + } else { + assertEq(amountWithdrawn, withdrawAmount, 'NATIVE_WITHDRAW: amount mismatch'); + } + assertEq( + stdMath.delta(user.balance, ethBefore), + expectedWithdrawnAmount, + 'NATIVE_WITHDRAW: user ETH mismatch' + ); + } + + Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, wethInfo, user); + + assertApproxEqAbs( + stdMath.delta(snapshotBefore.user.collateralAssets, snapshotAfter.user.collateralAssets), + expectedWithdrawnAmount, + 1, + 'NATIVE_WITHDRAW: user assets mismatch' + ); + assertEq( + stdMath.delta(snapshotBefore.user.collateralShares, snapshotAfter.user.collateralShares), + sharesWithdrawn, + 'NATIVE_WITHDRAW: user shares mismatch' + ); + assertApproxEqAbs( + stdMath.delta( + snapshotBefore.spokeOnHub.collateralAssets, + snapshotAfter.spokeOnHub.collateralAssets + ), + expectedWithdrawnAmount, + 1, + 'NATIVE_WITHDRAW: hub assets mismatch' + ); + assertEq( + stdMath.delta( + snapshotBefore.spokeOnHub.collateralShares, + snapshotAfter.spokeOnHub.collateralShares + ), + sharesWithdrawn, + 'NATIVE_WITHDRAW: hub shares mismatch' + ); + + if (withdrawAmount == UINT256_MAX) { + assertEq( + snapshotAfter.user.collateralAssets, + 0, + 'NATIVE_WITHDRAW: collateral should be zero after full withdraw' + ); + assertEq( + snapshotAfter.user.collateralShares, + 0, + 'NATIVE_WITHDRAW: shares should be zero after full withdraw' + ); + } + } + + function _testBorrowRepayNative( + INativeTokenGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory wethInfo, + address user, + uint256 amount + ) internal { + // Ensure user has enough collateral to borrow (uses any available collateral on spoke) + { + IAaveOracle oracle = IAaveOracle(spoke.ORACLE()); + uint256 price = oracle.getReservePrice(wethInfo.reserveId); + uint256 borrowDollarValue = (amount * price) / 10 ** (oracle.decimals() + wethInfo.decimals); + _ensureBorrowCapacity({ + spoke: spoke, + borrower: user, + borrowAmountInDollars: borrowDollarValue + }); + } + + // Ensure there is liquidity to borrow + _ensureLiquidity({spoke: spoke, reserveInfo: wethInfo, amount: amount}); + + // --- Borrow native --- + // borrow random amount within collateral factor + uint256 borrowAmount = vm.randomUint(1, amount / 2); + _borrowNative({ + gateway: gateway, + spoke: spoke, + wethInfo: wethInfo, + user: user, + borrowAmount: borrowAmount + }); + + // --- Repay native --- + uint256 repayAmount = vm.randomUint(1, borrowAmount); + _repayNative({ + gateway: gateway, + spoke: spoke, + wethInfo: wethInfo, + user: user, + repayAmount: repayAmount + }); + } + + function _borrowNative( + INativeTokenGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory wethInfo, + address user, + uint256 borrowAmount + ) internal { + Types.PositionSnapshot memory snapshotBefore = _getPositionSnapshot(spoke, wethInfo, user); + uint256 sharesBorrowed; + + { + uint256 ethBefore = user.balance; + vm.prank(user); + _logAction('NATIVE_BORROW', wethInfo.symbol, borrowAmount); + uint256 amountBorrowed; + (sharesBorrowed, amountBorrowed) = gateway.borrowNative( + address(spoke), + wethInfo.reserveId, + borrowAmount + ); + assertEq(amountBorrowed, borrowAmount, 'NATIVE_BORROW: amount mismatch'); + assertEq( + stdMath.delta(user.balance, ethBefore), + borrowAmount, + 'NATIVE_BORROW: user ETH mismatch' + ); + } + + Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, wethInfo, user); + assertEq( + stdMath.delta(snapshotAfter.user.drawnShares, snapshotBefore.user.drawnShares), + sharesBorrowed, + 'NATIVE_BORROW: user drawn shares mismatch' + ); + assertApproxEqAbs( + stdMath.delta(snapshotAfter.user.totalDebt, snapshotBefore.user.totalDebt), + borrowAmount, + 2, + 'NATIVE_BORROW: user debt asset mismatch' + ); + assertApproxEqAbs( + stdMath.delta(snapshotAfter.spokeOnHub.totalDebt, snapshotBefore.spokeOnHub.totalDebt), + borrowAmount, + 2, + 'NATIVE_BORROW: hub debt mismatch' + ); + assertEq( + stdMath.delta(snapshotAfter.spokeOnHub.drawnShares, snapshotBefore.spokeOnHub.drawnShares), + sharesBorrowed, + 'NATIVE_BORROW: hub drawn shares mismatch' + ); + } + + function _repayNative( + INativeTokenGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory wethInfo, + address user, + uint256 repayAmount + ) internal { + Types.PositionSnapshot memory snapshotBefore = _getPositionSnapshot(spoke, wethInfo, user); + uint256 sharesRepaid; + + { + vm.deal(user, repayAmount); + uint256 ethBefore = user.balance; + vm.prank(user); + _logAction('NATIVE_REPAY', wethInfo.symbol, repayAmount); + uint256 amountRepaid; + (sharesRepaid, amountRepaid) = gateway.repayNative{value: repayAmount}( + address(spoke), + wethInfo.reserveId, + repayAmount + ); + assertEq(amountRepaid, repayAmount, 'NATIVE_REPAY: amount mismatch'); + assertEq( + stdMath.delta(user.balance, ethBefore), + repayAmount, + 'NATIVE_REPAY: user ETH mismatch' + ); + } + + Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, wethInfo, user); + assertEq( + stdMath.delta(snapshotBefore.user.drawnShares, snapshotAfter.user.drawnShares), + sharesRepaid, + 'NATIVE_REPAY: user drawn shares mismatch' + ); + assertApproxEqAbs( + stdMath.delta(snapshotBefore.user.totalDebt, snapshotAfter.user.totalDebt), + repayAmount, + 2, + 'NATIVE_REPAY: user debt mismatch' + ); + assertApproxEqAbs( + stdMath.delta(snapshotBefore.spokeOnHub.totalDebt, snapshotAfter.spokeOnHub.totalDebt), + repayAmount, + 2, + 'NATIVE_REPAY: hub debt mismatch' + ); + assertEq( + stdMath.delta(snapshotBefore.spokeOnHub.drawnShares, snapshotAfter.spokeOnHub.drawnShares), + sharesRepaid, + 'NATIVE_REPAY: hub drawn shares mismatch' + ); + + if (repayAmount == UINT256_MAX) { + assertEq( + snapshotAfter.user.totalDebt, + 0, + 'NATIVE_REPAY: debt should be zero after full repay' + ); + } + } + + // ------------------------------------------------------------------------- + // SignatureGateway scenario + // ------------------------------------------------------------------------- + + /// @dev Test supply, withdraw, borrow, repay via SignatureGateway with EIP-712 signatures. + function _testSignatureGateway( + ISignatureGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + Types.ReserveInfo memory collateralInfo + ) internal { + uint256 privateKey = vm.randomUint(1, type(uint248).max); + address user = vm.addr(privateKey); + uint256 amount = _halfToken(reserveInfo.decimals); + + // Authorize gateway as position manager for user + vm.prank(user); + spoke.setUserPositionManager(address(gateway), true); + + // --- Supply with sig --- + _sigSupply({ + gateway: gateway, + spoke: spoke, + reserveInfo: reserveInfo, + privateKey: privateKey, + user: user, + amount: amount + }); + + // --- Partial withdraw with sig --- + _sigWithdraw({ + gateway: gateway, + spoke: spoke, + reserveInfo: reserveInfo, + privateKey: privateKey, + user: user, + amount: amount / 4 + }); + + // --- Borrow + repay with sig (if borrowable) --- + if (reserveInfo.borrowable) { + _sigSetupCollateralAndBorrowRepay({ + gateway: gateway, + spoke: spoke, + reserveInfo: reserveInfo, + collateralInfo: collateralInfo, + privateKey: privateKey, + user: user, + amount: amount + }); + } + } + + function _sigSupply( + ISignatureGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + uint256 privateKey, + address user, + uint256 amount + ) internal { + uint256 userAssetsBefore = spoke.getUserSuppliedAssets(reserveInfo.reserveId, user); + uint256 userSharesBefore = spoke.getUserSuppliedShares(reserveInfo.reserveId, user); + uint256 hubAssetsBefore = IHubBase(reserveInfo.hub).getSpokeAddedAssets( + reserveInfo.assetId, + address(spoke) + ); + + (uint256 sharesSupplied, uint256 amountSupplied) = _executeSigSupply({ + gateway: gateway, + spoke: spoke, + reserveInfo: reserveInfo, + privateKey: privateKey, + user: user, + amount: amount + }); + + assertApproxEqAbs( + spoke.getUserSuppliedAssets(reserveInfo.reserveId, user) - userAssetsBefore, + amountSupplied, + 1, + 'SIG_SUPPLY: user assets mismatch' + ); + assertEq( + spoke.getUserSuppliedShares(reserveInfo.reserveId, user) - userSharesBefore, + sharesSupplied, + 'SIG_SUPPLY: user shares mismatch' + ); + assertApproxEqAbs( + IHubBase(reserveInfo.hub).getSpokeAddedAssets(reserveInfo.assetId, address(spoke)) - + hubAssetsBefore, + amountSupplied, + 1, + 'SIG_SUPPLY: hub assets mismatch' + ); + } + + function _executeSigSupply( + ISignatureGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + uint256 privateKey, + address user, + uint256 amount + ) internal returns (uint256 sharesSupplied, uint256 amountSupplied) { + uint256 nonceBefore = gateway.nonces(user, 0); + uint256 deadline = vm.getBlockTimestamp() + 1 hours; + + deal2(reserveInfo.underlying, user, amount); + vm.prank(user); + IERC20(reserveInfo.underlying).forceApprove(address(gateway), amount); + + bytes32 structHash = keccak256( + abi.encode( + gateway.SUPPLY_TYPEHASH(), + address(spoke), + reserveInfo.reserveId, + amount, + user, + nonceBefore, + deadline + ) + ); + bytes memory sig = _signForGateway(gateway, privateKey, structHash); + + _logAction('SIG_SUPPLY', reserveInfo.symbol, amount); + ISignatureGateway.Supply memory params = ISignatureGateway.Supply({ + spoke: address(spoke), + reserveId: reserveInfo.reserveId, + amount: amount, + onBehalfOf: user, + nonce: nonceBefore, + deadline: deadline + }); + (sharesSupplied, amountSupplied) = gateway.supplyWithSig(params, sig); + + assertEq(amountSupplied, amount, 'SIG_SUPPLY: amount mismatch'); + assertEq(gateway.nonces(user, 0), nonceBefore + 1, 'SIG_SUPPLY: nonce not incremented'); + } + + function _sigWithdraw( + ISignatureGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + uint256 privateKey, + address user, + uint256 amount + ) internal { + uint256 userAssetsBefore = spoke.getUserSuppliedAssets(reserveInfo.reserveId, user); + uint256 userSharesBefore = spoke.getUserSuppliedShares(reserveInfo.reserveId, user); + uint256 hubAssetsBefore = IHubBase(reserveInfo.hub).getSpokeAddedAssets( + reserveInfo.assetId, + address(spoke) + ); + + (uint256 sharesWithdrawn, uint256 amountWithdrawn) = _executeSigWithdraw({ + gateway: gateway, + spoke: spoke, + reserveInfo: reserveInfo, + privateKey: privateKey, + user: user, + amount: amount + }); + + assertApproxEqAbs( + userAssetsBefore - spoke.getUserSuppliedAssets(reserveInfo.reserveId, user), + amountWithdrawn, + 1, + 'SIG_WITHDRAW: user assets mismatch' + ); + assertEq( + userSharesBefore - spoke.getUserSuppliedShares(reserveInfo.reserveId, user), + sharesWithdrawn, + 'SIG_WITHDRAW: user shares mismatch' + ); + assertApproxEqAbs( + hubAssetsBefore - + IHubBase(reserveInfo.hub).getSpokeAddedAssets(reserveInfo.assetId, address(spoke)), + amountWithdrawn, + 1, + 'SIG_WITHDRAW: hub assets mismatch' + ); + + if (amount == UINT256_MAX) { + assertEq( + spoke.getUserSuppliedAssets(reserveInfo.reserveId, user), + 0, + 'SIG_WITHDRAW: collateral should be zero after full withdraw' + ); + } + } + + function _executeSigWithdraw( + ISignatureGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + uint256 privateKey, + address user, + uint256 amount + ) internal returns (uint256 sharesWithdrawn, uint256 amountWithdrawn) { + uint256 nonceBefore = gateway.nonces(user, 0); + uint256 deadline = vm.getBlockTimestamp() + 1 hours; + + bytes32 structHash = keccak256( + abi.encode( + gateway.WITHDRAW_TYPEHASH(), + address(spoke), + reserveInfo.reserveId, + amount, + user, + nonceBefore, + deadline + ) + ); + bytes memory sig = _signForGateway(gateway, privateKey, structHash); + + _logAction('SIG_WITHDRAW', reserveInfo.symbol, amount); + ISignatureGateway.Withdraw memory params = ISignatureGateway.Withdraw({ + spoke: address(spoke), + reserveId: reserveInfo.reserveId, + amount: amount, + onBehalfOf: user, + nonce: nonceBefore, + deadline: deadline + }); + (sharesWithdrawn, amountWithdrawn) = gateway.withdrawWithSig(params, sig); + + if (amount != UINT256_MAX) { + assertEq(amountWithdrawn, amount, 'SIG_WITHDRAW: amount mismatch'); + } + assertEq(gateway.nonces(user, 0), nonceBefore + 1, 'SIG_WITHDRAW: nonce not incremented'); + } + + function _sigSetupCollateralAndBorrowRepay( + ISignatureGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + Types.ReserveInfo memory collateralInfo, + uint256 privateKey, + address user, + uint256 amount + ) internal { + // Supply collateral + enable as collateral via sig + uint256 collateralAmount = _halfToken(collateralInfo.decimals); + _sigSupply({ + gateway: gateway, + spoke: spoke, + reserveInfo: collateralInfo, + privateKey: privateKey, + user: user, + amount: collateralAmount + }); + _sigSetUsingAsCollateral({ + gateway: gateway, + spoke: spoke, + reserveInfo: collateralInfo, + privateKey: privateKey, + user: user + }); + + // Ensure liquidity + borrow + repay + _ensureLiquidity({spoke: spoke, reserveInfo: reserveInfo, amount: amount}); + uint256 borrowAmount = amount / 4; + _sigBorrow({ + gateway: gateway, + spoke: spoke, + reserveInfo: reserveInfo, + privateKey: privateKey, + user: user, + amount: borrowAmount + }); + // repay partial + _sigRepay({ + gateway: gateway, + spoke: spoke, + reserveInfo: reserveInfo, + privateKey: privateKey, + user: user, + amount: vm.randomUint(1, borrowAmount) + }); + // repay + _sigRepay({ + gateway: gateway, + spoke: spoke, + reserveInfo: reserveInfo, + privateKey: privateKey, + user: user, + amount: UINT256_MAX + }); + } + + function _sigSetUsingAsCollateral( + ISignatureGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + uint256 privateKey, + address user + ) internal { + uint256 nonceBefore = gateway.nonces(user, 0); + ISignatureGateway.SetUsingAsCollateral memory setParams = ISignatureGateway + .SetUsingAsCollateral({ + spoke: address(spoke), + reserveId: reserveInfo.reserveId, + useAsCollateral: true, + onBehalfOf: user, + nonce: nonceBefore, + deadline: vm.getBlockTimestamp() + 1 hours + }); + bytes32 structHash = keccak256( + abi.encode( + gateway.SET_USING_AS_COLLATERAL_TYPEHASH(), + setParams.spoke, + setParams.reserveId, + setParams.useAsCollateral, + setParams.onBehalfOf, + setParams.nonce, + setParams.deadline + ) + ); + bytes memory sig = _signForGateway(gateway, privateKey, structHash); + gateway.setUsingAsCollateralWithSig(setParams, sig); + + assertEq(gateway.nonces(user, 0), nonceBefore + 1, 'SIG_SET_COLLATERAL: nonce not incremented'); + (bool isUsingAsCollateral, ) = spoke.getUserReserveStatus(reserveInfo.reserveId, user); + assertTrue(isUsingAsCollateral, 'SIG_SET_COLLATERAL: not set as collateral'); + } + + function _sigBorrow( + ISignatureGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + uint256 privateKey, + address user, + uint256 amount + ) internal { + uint256 userDebtBefore = spoke.getUserTotalDebt(reserveInfo.reserveId, user); + uint256 userDrawnSharesBefore = spoke.getUserPosition(reserveInfo.reserveId, user).drawnShares; + uint256 hubDebtBefore = IHubBase(reserveInfo.hub).getSpokeTotalOwed( + reserveInfo.assetId, + address(spoke) + ); + uint256 hubDrawnSharesBefore = IHubBase(reserveInfo.hub).getSpokeDrawnShares( + reserveInfo.assetId, + address(spoke) + ); + + (uint256 sharesBorrowed, uint256 amountBorrowed) = _executeSigBorrow({ + gateway: gateway, + spoke: spoke, + reserveInfo: reserveInfo, + privateKey: privateKey, + user: user, + amount: amount + }); + + assertApproxEqAbs( + spoke.getUserTotalDebt(reserveInfo.reserveId, user) - userDebtBefore, + amountBorrowed, + 2, + 'SIG_BORROW: user debt mismatch' + ); + assertEq( + spoke.getUserPosition(reserveInfo.reserveId, user).drawnShares - userDrawnSharesBefore, + sharesBorrowed, + 'SIG_BORROW: user drawn shares mismatch' + ); + assertApproxEqAbs( + IHubBase(reserveInfo.hub).getSpokeTotalOwed(reserveInfo.assetId, address(spoke)) - + hubDebtBefore, + amountBorrowed, + 2, + 'SIG_BORROW: hub debt mismatch' + ); + assertEq( + IHubBase(reserveInfo.hub).getSpokeDrawnShares(reserveInfo.assetId, address(spoke)) - + hubDrawnSharesBefore, + sharesBorrowed, + 'SIG_BORROW: hub drawn shares mismatch' + ); + } + + function _executeSigBorrow( + ISignatureGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + uint256 privateKey, + address user, + uint256 amount + ) internal returns (uint256 sharesBorrowed, uint256 amountBorrowed) { + uint256 nonceBefore = gateway.nonces(user, 0); + uint256 deadline = vm.getBlockTimestamp() + 1 hours; + + bytes32 structHash = keccak256( + abi.encode( + gateway.BORROW_TYPEHASH(), + address(spoke), + reserveInfo.reserveId, + amount, + user, + nonceBefore, + deadline + ) + ); + bytes memory sig = _signForGateway(gateway, privateKey, structHash); + + _logAction('SIG_BORROW', reserveInfo.symbol, amount); + ISignatureGateway.Borrow memory params = ISignatureGateway.Borrow({ + spoke: address(spoke), + reserveId: reserveInfo.reserveId, + amount: amount, + onBehalfOf: user, + nonce: nonceBefore, + deadline: deadline + }); + (sharesBorrowed, amountBorrowed) = gateway.borrowWithSig(params, sig); + + assertEq(amountBorrowed, amount, 'SIG_BORROW: amount mismatch'); + assertEq(gateway.nonces(user, 0), nonceBefore + 1, 'SIG_BORROW: nonce not incremented'); + } + + function _sigRepay( + ISignatureGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + uint256 privateKey, + address user, + uint256 amount + ) internal { + uint256 userDebtBefore = spoke.getUserTotalDebt(reserveInfo.reserveId, user); + uint256 userDrawnSharesBefore = spoke.getUserPosition(reserveInfo.reserveId, user).drawnShares; + uint256 hubDebtBefore = IHubBase(reserveInfo.hub).getSpokeTotalOwed( + reserveInfo.assetId, + address(spoke) + ); + uint256 hubDrawnSharesBefore = IHubBase(reserveInfo.hub).getSpokeDrawnShares( + reserveInfo.assetId, + address(spoke) + ); + + (uint256 sharesRepaid, uint256 amountRepaid) = _executeSigRepay({ + gateway: gateway, + spoke: spoke, + reserveInfo: reserveInfo, + privateKey: privateKey, + user: user, + amount: amount + }); + + assertApproxEqAbs( + userDebtBefore - spoke.getUserTotalDebt(reserveInfo.reserveId, user), + amountRepaid, + 2, + 'SIG_REPAY: user debt mismatch' + ); + assertEq( + userDrawnSharesBefore - spoke.getUserPosition(reserveInfo.reserveId, user).drawnShares, + sharesRepaid, + 'SIG_REPAY: user drawn shares mismatch' + ); + assertApproxEqAbs( + hubDebtBefore - + IHubBase(reserveInfo.hub).getSpokeTotalOwed(reserveInfo.assetId, address(spoke)), + amountRepaid, + 2, + 'SIG_REPAY: hub debt mismatch' + ); + assertEq( + hubDrawnSharesBefore - + IHubBase(reserveInfo.hub).getSpokeDrawnShares(reserveInfo.assetId, address(spoke)), + sharesRepaid, + 'SIG_REPAY: hub drawn shares mismatch' + ); + + if (amount == UINT256_MAX) { + assertEq( + spoke.getUserTotalDebt(reserveInfo.reserveId, user), + 0, + 'SIG_REPAY: debt should be zero after full repay' + ); + } + } + + function _executeSigRepay( + ISignatureGateway gateway, + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + uint256 privateKey, + address user, + uint256 amount + ) internal returns (uint256 sharesRepaid, uint256 amountRepaid) { + uint256 nonceBefore = gateway.nonces(user, 0); + uint256 deadline = vm.getBlockTimestamp() + 1 hours; + + uint256 mintAmount = amount == UINT256_MAX + ? spoke.getUserTotalDebt(reserveInfo.reserveId, user) + : amount; + deal2(reserveInfo.underlying, user, mintAmount + 2); + vm.prank(user); + IERC20(reserveInfo.underlying).forceApprove(address(gateway), mintAmount + 2); + + bytes32 structHash = keccak256( + abi.encode( + gateway.REPAY_TYPEHASH(), + address(spoke), + reserveInfo.reserveId, + amount, + user, + nonceBefore, + deadline + ) + ); + bytes memory sig = _signForGateway(gateway, privateKey, structHash); + + _logAction('SIG_REPAY', reserveInfo.symbol, amount); + ISignatureGateway.Repay memory params = ISignatureGateway.Repay({ + spoke: address(spoke), + reserveId: reserveInfo.reserveId, + amount: amount, + onBehalfOf: user, + nonce: nonceBefore, + deadline: deadline + }); + (sharesRepaid, amountRepaid) = gateway.repayWithSig(params, sig); + + assertEq(gateway.nonces(user, 0), nonceBefore + 1, 'SIG_REPAY: nonce not incremented'); + } +} diff --git a/src/dependencies/v4/Helpers.sol b/src/dependencies/v4/Helpers.sol new file mode 100644 index 00000000..4f9fd2c7 --- /dev/null +++ b/src/dependencies/v4/Helpers.sol @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20Metadata} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {ISpoke, IHubConfigurator, IAaveOracle} from 'aave-address-book/AaveV4.sol'; +import {AaveV4Ethereum} from 'aave-address-book/AaveV4Ethereum.sol'; +import {Types} from 'src/dependencies/v4/Types.sol'; +import {Actions} from 'src/dependencies/v4/Actions.sol'; + +/// @title Helpers +/// @notice Query and utility functions for V4 e2e tests. +abstract contract Helpers is Actions { + /// @notice Build ReserveInfo[] for all reserves on a spoke. + function _getReserveInfo(ISpoke spoke) internal view returns (Types.ReserveInfo[] memory) { + uint256 count = spoke.getReserveCount(); + Types.ReserveInfo[] memory info = new Types.ReserveInfo[](count); + + for (uint256 i; i < count; i++) { + ISpoke.Reserve memory reserve = spoke.getReserve(i); + ISpoke.ReserveConfig memory config = spoke.getReserveConfig(i); + ISpoke.DynamicReserveConfig memory dynamicConfig = spoke.getDynamicReserveConfig( + i, + reserve.dynamicConfigKey + ); + + string memory symbol = _safeSymbol(reserve.underlying); + + info[i] = Types.ReserveInfo({ + reserveId: i, + underlying: reserve.underlying, + hub: address(reserve.hub), + assetId: reserve.assetId, + symbol: symbol, + decimals: reserve.decimals, + paused: config.paused, + frozen: config.frozen, + borrowable: config.borrowable, + collateralEnabled: dynamicConfig.collateralFactor > 0, + collateralFactor: dynamicConfig.collateralFactor, + maxLiquidationBonus: dynamicConfig.maxLiquidationBonus, + liquidationFee: dynamicConfig.liquidationFee + }); + } + return info; + } + + /// @notice Return all usable collaterals: not paused, not frozen, collateralFactor > 0. + function _getAllUsableCollaterals( + Types.ReserveInfo[] memory infos + ) internal pure returns (Types.ReserveInfo[] memory) { + uint256 count; + for (uint256 i; i < infos.length; i++) { + if (!infos[i].paused && !infos[i].frozen && infos[i].collateralEnabled) { + count++; + } + } + Types.ReserveInfo[] memory result = new Types.ReserveInfo[](count); + uint256 index; + for (uint256 i; i < infos.length; i++) { + if (!infos[i].paused && !infos[i].frozen && infos[i].collateralEnabled) { + result[index] = infos[i]; + index++; + } + } + return result; + } + + /// @notice Return all usable debt reserves: not paused, not frozen, borrowable. + function _getAllUsableDebtReserves( + Types.ReserveInfo[] memory infos + ) internal pure returns (Types.ReserveInfo[] memory) { + uint256 count; + for (uint256 i; i < infos.length; i++) { + if (!infos[i].paused && !infos[i].frozen && infos[i].borrowable) { + count++; + } + } + Types.ReserveInfo[] memory result = new Types.ReserveInfo[](count); + uint256 index; + for (uint256 i; i < infos.length; i++) { + if (!infos[i].paused && !infos[i].frozen && infos[i].borrowable) { + result[index] = infos[i]; + index++; + } + } + return result; + } + + /// @notice Ensure the hub has enough liquidity for a borrow by supplying on the given spoke. + /// Assumes addCaps have been set to max via _setCapsToMax before calling. + function _ensureLiquidity( + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + uint256 amount + ) internal { + _supply({spoke: spoke, reserveInfo: reserveInfo, user: vm.randomAddress(), amount: amount}); + } + + /// @notice Supply collateral to borrower on the same spoke, then enable as collateral. + /// Assumes addCaps have been set to max via _setCapsToMax before calling. + function _ensureCollateral( + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + address borrower, + uint256 amount + ) internal { + _supply({spoke: spoke, reserveInfo: reserveInfo, user: borrower, amount: amount}); + vm.prank(borrower); + spoke.setUsingAsCollateral({ + reserveId: reserveInfo.reserveId, + usingAsCollateral: true, + onBehalfOf: borrower + }); + } + + /// @notice Ensure borrower has enough collateral to borrow a given dollar amount. + /// Loops over all collateral-enabled reserves, supplying until capacity is sufficient. + /// Compares against CF-adjusted totalCollateralValue, so it may use multiple reserves. + function _ensureBorrowCapacity( + ISpoke spoke, + address borrower, + uint256 borrowAmountInDollars + ) internal { + Types.ReserveInfo[] memory goodCollaterals = _getAllUsableCollaterals(_getReserveInfo(spoke)); + address oracleAddr = spoke.ORACLE(); + uint8 oracleDecimals = IAaveOracle(oracleAddr).decimals(); + uint256 targetCollateralDollarAmount = borrowAmountInDollars * 3; + uint256 targetCollateralValue = targetCollateralDollarAmount * 10 ** oracleDecimals; + + for (uint256 i; i < goodCollaterals.length; i++) { + uint256 supplyAmount = _getTokenAmountByDollarValue({ + oracleAddr: oracleAddr, + reserveInfo: goodCollaterals[i], + dollarValue: targetCollateralDollarAmount + }); + + _ensureCollateral({ + spoke: spoke, + reserveInfo: goodCollaterals[i], + borrower: borrower, + amount: supplyAmount + }); + + // Check after supplying — totalCollateralValue is CF-adjusted, so we may need + // multiple reserves to reach the target raw collateral value. + ISpoke.UserAccountData memory account = spoke.getUserAccountData(borrower); + if (account.totalCollateralValue > targetCollateralValue) { + break; + } + } + } + + /// @notice Convert a dollar value to token amount using the spoke oracle. + function _getTokenAmountByDollarValue( + address oracleAddr, + Types.ReserveInfo memory reserveInfo, + uint256 dollarValue + ) internal view returns (uint256) { + IAaveOracle oracle = IAaveOracle(oracleAddr); + uint256 price = oracle.getReservePrice(reserveInfo.reserveId); + uint8 oracleDecimals = oracle.decimals(); + return (dollarValue * 10 ** (oracleDecimals + reserveInfo.decimals)) / price; + } + + /// @notice Supply up to `extraCount` of additional collaterals for the user, up to `maxUserReserves`. + function _supplyRandomExtraCollaterals( + ISpoke spoke, + Types.ReserveInfo[] memory goodCollaterals, + uint256 primaryIndex, + uint256 testAssetReserveId, + address oracleAddr, + address user, + uint256 extraCount + ) internal { + if (goodCollaterals.length <= 1 || extraCount == 0) { + return; + } + + uint16 maxUserReserves = spoke.MAX_USER_RESERVES_LIMIT(); + + // Track collateral count before starting + ISpoke.UserAccountData memory accountBefore = spoke.getUserAccountData(user); + uint256 expectedCollateralCount = accountBefore.activeCollateralCount; + + uint256 supplied; + for (uint256 index; index < goodCollaterals.length && supplied < extraCount; index++) { + // skip the primary collateral and the test asset + if (index == primaryIndex || goodCollaterals[index].reserveId == testAssetReserveId) { + continue; + } + + // When at the limit, assert the next collateral enable reverts, then restore state + if (expectedCollateralCount + 1 > maxUserReserves) { + _assertMaxUserReservesReverts({ + spoke: spoke, + reserveInfo: goodCollaterals[index], + oracleAddr: oracleAddr, + user: user, + isCollateral: true + }); + break; + } + + // adding too much collateral will mean user's HF is too high to make liquidatable easily + uint256 extraDollars = vm.randomUint(1_000, 10_000); + uint256 extraAmount = _getTokenAmountByDollarValue({ + oracleAddr: oracleAddr, + reserveInfo: goodCollaterals[index], + dollarValue: extraDollars + }); + + _supply({spoke: spoke, reserveInfo: goodCollaterals[index], user: user, amount: extraAmount}); + vm.prank(user); + spoke.setUsingAsCollateral({ + reserveId: goodCollaterals[index].reserveId, + usingAsCollateral: true, + onBehalfOf: user + }); + + supplied++; + expectedCollateralCount++; + + // Verify activeCollateralCount matches expected + ISpoke.UserAccountData memory accountAfter = spoke.getUserAccountData(user); + assertEq( + accountAfter.activeCollateralCount, + expectedCollateralCount, + 'EXTRA_COLLATERAL: activeCollateralCount mismatch' + ); + assertLe( + accountAfter.activeCollateralCount, + maxUserReserves, + 'EXTRA_COLLATERAL: exceeds MAX_USER_RESERVES_LIMIT' + ); + } + } + + /// @notice Borrow from a random number of extra debt reserves for the user. + /// Supplies liquidity from a separate provider before each borrow. + function _borrowRandomExtraReserves( + ISpoke spoke, + Types.ReserveInfo[] memory usableDebtReserves, + uint256 primaryReserveId, + address oracleAddr, + address user, + uint256 extraCount + ) internal { + if (usableDebtReserves.length <= 1 || extraCount == 0) { + return; + } + + uint16 maxUserReserves = spoke.MAX_USER_RESERVES_LIMIT(); + + ISpoke.UserAccountData memory accountBefore = spoke.getUserAccountData(user); + uint256 expectedBorrowCount = accountBefore.borrowCount; + + uint256 borrowed; + for (uint256 index; index < usableDebtReserves.length && borrowed < extraCount; index++) { + Types.ReserveInfo memory debtReserve = usableDebtReserves[index]; + + if (debtReserve.reserveId == primaryReserveId) { + continue; + } + + // When at the limit, assert the next borrow reverts, then restore state + if (expectedBorrowCount + 1 > maxUserReserves) { + _assertMaxUserReservesReverts({ + spoke: spoke, + reserveInfo: debtReserve, + oracleAddr: oracleAddr, + user: user, + isCollateral: false + }); + break; + } + + uint256 extraDollars = vm.randomUint(100, 1_000); + uint256 extraAmount = _getTokenAmountByDollarValue({ + oracleAddr: oracleAddr, + reserveInfo: debtReserve, + dollarValue: extraDollars + }); + + _ensureLiquidity({spoke: spoke, reserveInfo: debtReserve, amount: extraAmount}); + _borrow({spoke: spoke, reserveInfo: debtReserve, user: user, amount: extraAmount}); + + borrowed++; + expectedBorrowCount++; + + // Verify borrowCount within limit + ISpoke.UserAccountData memory accountAfter = spoke.getUserAccountData(user); + assertLe( + accountAfter.borrowCount, + maxUserReserves, + 'EXTRA_BORROW: exceeds MAX_USER_RESERVES_LIMIT' + ); + assertEq(accountAfter.borrowCount, expectedBorrowCount, 'EXTRA_BORROW: borrowCount mismatch'); + } + } + + /// @notice Assert that exceeding MAX_USER_RESERVES_LIMIT reverts, then restore state. + function _assertMaxUserReservesReverts( + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + address oracleAddr, + address user, + bool isCollateral + ) internal { + uint256 snapshot = vm.snapshotState(); + + uint256 dollarValue = vm.randomUint(1_000, 50_000); + uint256 amount = _getTokenAmountByDollarValue({ + oracleAddr: oracleAddr, + reserveInfo: reserveInfo, + dollarValue: dollarValue + }); + + if (isCollateral) { + _supply({spoke: spoke, reserveInfo: reserveInfo, user: user, amount: amount}); + vm.prank(user); + vm.expectRevert(ISpoke.MaximumUserReservesExceeded.selector); + spoke.setUsingAsCollateral({ + reserveId: reserveInfo.reserveId, + usingAsCollateral: true, + onBehalfOf: user + }); + } else { + _ensureLiquidity({spoke: spoke, reserveInfo: reserveInfo, amount: amount}); + vm.prank(user); + vm.expectRevert(ISpoke.MaximumUserReservesExceeded.selector); + spoke.borrow({reserveId: reserveInfo.reserveId, amount: amount, onBehalfOf: user}); + } + + vm.revertToState(snapshot); + } + + /// @notice Set all addCap/drawCap to max for every reserve on the spoke. + function _setCapsToMax(ISpoke spoke) internal { + IHubConfigurator hubConfigurator = AaveV4Ethereum.HUB_CONFIGURATOR; + + Types.ReserveInfo[] memory infos = _getReserveInfo(spoke); + vm.mockCall( + address(AaveV4Ethereum.ACCESS_MANAGER), + abi.encodeWithSelector(bytes4(keccak256('canCall(address,address,bytes4)'))), + abi.encode(true, uint32(0)) + ); + for (uint256 i; i < infos.length; i++) { + hubConfigurator.updateSpokeCaps({ + hub: infos[i].hub, + assetId: infos[i].assetId, + spoke: address(spoke), + addCap: type(uint40).max, + drawCap: type(uint40).max + }); + } + vm.clearMockedCalls(); + } + + /// @notice Set all addCap to max for every reserve on the spoke (leaves drawCap unchanged). + function _setAddCapsToMax(ISpoke spoke) internal { + IHubConfigurator hubConfigurator = AaveV4Ethereum.HUB_CONFIGURATOR; + + Types.ReserveInfo[] memory infos = _getReserveInfo(spoke); + vm.mockCall( + address(AaveV4Ethereum.ACCESS_MANAGER), + abi.encodeWithSelector(bytes4(keccak256('canCall(address,address,bytes4)'))), + abi.encode(true, uint32(0)) + ); + for (uint256 i; i < infos.length; i++) { + hubConfigurator.updateSpokeAddCap({ + hub: infos[i].hub, + assetId: infos[i].assetId, + spoke: address(spoke), + addCap: type(uint40).max + }); + } + vm.clearMockedCalls(); + } + + /// @notice Safely get the ERC20 symbol, fallback to "UNKNOWN". + function _safeSymbol(address token) internal view returns (string memory) { + try IERC20Metadata(token).symbol() returns (string memory s) { + return s; + } catch { + return 'UNKNOWN'; + } + } + + /// @notice Half a token in the asset's native decimals. + function _halfToken(uint8 decimals) internal pure returns (uint256) { + return 10 ** decimals / 2; + } +} diff --git a/src/dependencies/v4/Scenarios.sol b/src/dependencies/v4/Scenarios.sol new file mode 100644 index 00000000..58d56e06 --- /dev/null +++ b/src/dependencies/v4/Scenarios.sol @@ -0,0 +1,690 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; +import {ISpoke, IHub, IAaveOracle, ISpokeConfigurator} from 'aave-address-book/AaveV4.sol'; +import {AaveV4Ethereum} from 'aave-address-book/AaveV4Ethereum.sol'; +import {IPriceOracle} from 'aave-v4/spoke/interfaces/IPriceOracle.sol'; +import {Types} from 'src/dependencies/v4/Types.sol'; +import {Helpers} from 'src/dependencies/v4/Helpers.sol'; + +/// @title Scenarios +/// @notice Test scenario orchestration for V4 e2e tests. +abstract contract Scenarios is Helpers { + using SafeERC20 for IERC20; + + /// @dev Makes a user liquidatable by reducing collateral factors and manipulating oracle prices. + /// Two-passes: CF updates first, then oracle mocks + /// in a separate pass so clearMockedCalls doesn't wipe earlier price mocks. + function _makeUserLiquidatable(ISpoke spoke, address user) internal virtual { + address oracle = spoke.ORACLE(); + uint256 reserveCount = spoke.getReserveCount(); + + // Pass 1: reduce collateral factors (each call mocks ACCESS_MANAGER then clears all mocks) + for (uint256 i; i < reserveCount; i++) { + if (spoke.getUserSuppliedAssets(i, user) > 0) { + _updateCollateralFactor({spoke: spoke, reserveId: i, user: user, collateralFactor: 1}); + } + } + + // Pass 2: manipulate oracle prices (safe from clearMockedCalls now) + for (uint256 i; i < reserveCount; i++) { + uint256 userSupply = spoke.getUserSuppliedAssets(i, user); + uint256 userDebt = spoke.getUserTotalDebt(i, user); + + if (userSupply > 0 && userDebt == 0) { + uint256 currentPrice = IAaveOracle(oracle).getReservePrice(i); + vm.mockCall( + oracle, + abi.encodeWithSelector(IPriceOracle.getReservePrice.selector, i), + abi.encode(currentPrice / 1000) + ); + } else if (userDebt > 0 && userSupply == 0) { + uint256 currentPrice = IAaveOracle(oracle).getReservePrice(i); + vm.mockCall( + oracle, + abi.encodeWithSelector(IPriceOracle.getReservePrice.selector, i), + abi.encode(currentPrice * 1000) + ); + } + } + + ISpoke.UserAccountData memory accountData = spoke.getUserAccountData(user); + assertLt( + accountData.healthFactor, + HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + 'MAKE_LIQUIDATABLE: health factor not below 1' + ); + } + + /// @dev Update the collateral factor on the user's existing dynamic config key. + /// Mocks ACCESS_MANAGER to bypass auth, then calls SpokeConfigurator.updateCollateralFactor. + function _updateCollateralFactor( + ISpoke spoke, + uint256 reserveId, + address user, + uint16 collateralFactor + ) internal { + uint32 userConfigKey = spoke.getUserPosition(reserveId, user).dynamicConfigKey; + vm.mockCall( + address(AaveV4Ethereum.ACCESS_MANAGER), + abi.encodeWithSelector(bytes4(keccak256('canCall(address,address,bytes4)'))), + abi.encode(true, uint32(0)) + ); + AaveV4Ethereum.SPOKE_CONFIGURATOR.updateCollateralFactor({ + spoke: address(spoke), + reserveId: reserveId, + dynamicConfigKey: userConfigKey, + collateralFactor: collateralFactor + }); + vm.clearMockedCalls(); + + assertEq( + collateralFactor, + spoke.getDynamicReserveConfig(reserveId, userConfigKey).collateralFactor + ); + } + + /// @dev Supply collateral(s) and test asset, return the test asset amount. + function _setupPositions( + ISpoke spoke, + Types.ReserveInfo[] memory goodCollaterals, + uint256 primaryCollateralIndex, + Types.ReserveInfo memory testAssetInfo, + address collateralSupplier, + address testAssetSupplier + ) internal returns (uint256 testAssetAmount) { + Types.ReserveInfo memory collateralInfo = goodCollaterals[primaryCollateralIndex]; + address oracle = spoke.ORACLE(); + + uint256 collateralDollars = vm.randomUint(50_000, 200_000); + uint256 testAssetDollars = vm.randomUint(1_000, 20_000); + uint256 collateralAmount = _getTokenAmountByDollarValue({ + oracleAddr: oracle, + reserveInfo: collateralInfo, + dollarValue: collateralDollars + }); + testAssetAmount = _getTokenAmountByDollarValue({ + oracleAddr: oracle, + reserveInfo: testAssetInfo, + dollarValue: testAssetDollars + }); + + // Supply primary collateral + _supply({ + spoke: spoke, + reserveInfo: collateralInfo, + user: collateralSupplier, + amount: collateralAmount + }); + vm.prank(collateralSupplier); + spoke.setUsingAsCollateral({ + reserveId: collateralInfo.reserveId, + usingAsCollateral: true, + onBehalfOf: collateralSupplier + }); + + { + ISpoke.UserAccountData memory accountAfterCollateral = spoke.getUserAccountData( + collateralSupplier + ); + assertEq( + accountAfterCollateral.activeCollateralCount, + 1, + 'SETUP: activeCollateralCount should be 1 after primary collateral' + ); + } + + // Supply random extra collaterals up to remaining capacity + { + uint256 extraCount = _randomExtraCount({ + spoke: spoke, + user: collateralSupplier, + available: goodCollaterals.length > 1 ? goodCollaterals.length - 1 : 0 + }); + _supplyRandomExtraCollaterals({ + spoke: spoke, + goodCollaterals: goodCollaterals, + primaryIndex: primaryCollateralIndex, + testAssetReserveId: testAssetInfo.reserveId, + oracleAddr: oracle, + user: collateralSupplier, + extraCount: extraCount + }); + } + + // Supply test asset + _supply({ + spoke: spoke, + reserveInfo: testAssetInfo, + user: testAssetSupplier, + amount: testAssetAmount + }); + } + + function _testPartialWithdrawal( + ISpoke spoke, + Types.ReserveInfo memory testAssetInfo, + address testAssetSupplier, + uint256 testAssetAmount + ) internal { + uint256 partialWithdraw = testAssetAmount > 1 + ? vm.randomUint(1, testAssetAmount - 1) + : testAssetAmount; + _withdraw(spoke, testAssetInfo, testAssetSupplier, partialWithdraw); + } + + function _testFullWithdrawal( + ISpoke spoke, + Types.ReserveInfo memory testAssetInfo, + address testAssetSupplier + ) internal { + _withdraw(spoke, testAssetInfo, testAssetSupplier, UINT256_MAX); + } + + /// @dev Setup borrows: calculate ceiling, first+second borrow, extras. + /// Returns the borrow ceiling (0 means no borrow was possible). + function _setupBorrows( + ISpoke spoke, + Types.ReserveInfo memory testAssetInfo, + address collateralSupplier, + uint256 testAssetAmount + ) internal returns (uint256 borrowCeiling) { + // Cap borrow by user's available borrowing power to avoid HealthFactorBelowThreshold. + { + ISpoke.UserAccountData memory accountData = spoke.getUserAccountData(collateralSupplier); + // maxDebtValue = CF-weighted collateral value (HF=1 threshold) + uint256 maxDebtValue = (accountData.totalCollateralValue * accountData.avgCollateralFactor) / + 1e18; + uint256 currentDebtValue = accountData.totalDebtValueRay / 1e27; + uint256 availableDebtValue = maxDebtValue > currentDebtValue + ? maxDebtValue - currentDebtValue + : 0; + + // Convert to test asset tokens + address oracleAddr = spoke.ORACLE(); + uint256 testAssetPrice = IAaveOracle(oracleAddr).getReservePrice(testAssetInfo.reserveId); + uint256 maxBorrowableAmount = (availableDebtValue * 10 ** testAssetInfo.decimals) / + testAssetPrice; + // Use 50% of max for safety margin + maxBorrowableAmount = maxBorrowableAmount / 2; + borrowCeiling = testAssetAmount < maxBorrowableAmount ? testAssetAmount : maxBorrowableAmount; + } + if (borrowCeiling == 0) { + return 0; + } + + // First borrow (random partial amount) + uint256 firstBorrow = borrowCeiling > 2 ? vm.randomUint(1, borrowCeiling / 2) : borrowCeiling; + _borrow({ + spoke: spoke, + reserveInfo: testAssetInfo, + user: collateralSupplier, + amount: firstBorrow + }); + + // Health factor + reserves limit check after first borrow + { + ISpoke.UserAccountData memory accountData = spoke.getUserAccountData(collateralSupplier); + assertGe( + accountData.healthFactor, + HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + 'HEALTH: health factor below 1 after borrow' + ); + assertLe( + accountData.borrowCount, + spoke.MAX_USER_RESERVES_LIMIT(), + 'BORROW: borrowCount exceeds MAX_USER_RESERVES_LIMIT' + ); + } + + // Second sequential borrow on same reserve + uint256 remaining = borrowCeiling - firstBorrow; + if (remaining > 0) { + uint256 secondBorrow = vm.randomUint(1, remaining); + _borrow({ + spoke: spoke, + reserveInfo: testAssetInfo, + user: collateralSupplier, + amount: secondBorrow + }); + + // Verify borrow count unchanged (same reserve, not a new borrow position) + ISpoke.UserAccountData memory accountAfterSecond = spoke.getUserAccountData( + collateralSupplier + ); + assertLe( + accountAfterSecond.borrowCount, + spoke.MAX_USER_RESERVES_LIMIT(), + 'BORROW: borrowCount exceeds MAX_USER_RESERVES_LIMIT after second borrow' + ); + } + + // Borrow from random extra borrowable reserves up to remaining capacity + _borrowExtrasWithinLimit({ + spoke: spoke, + primaryReserveId: testAssetInfo.reserveId, + user: collateralSupplier + }); + } + + function _testPartialRepay( + ISpoke spoke, + Types.ReserveInfo memory testAssetInfo, + address collateralSupplier + ) internal { + uint256 actualDebt = spoke.getUserTotalDebt(testAssetInfo.reserveId, collateralSupplier); + if (actualDebt > 1) { + uint256 partialRepay = vm.randomUint(1, actualDebt - 1); + _repay({ + spoke: spoke, + reserveInfo: testAssetInfo, + user: collateralSupplier, + amount: partialRepay + }); + } + } + + function _testFullRepay( + ISpoke spoke, + Types.ReserveInfo memory testAssetInfo, + address collateralSupplier + ) internal { + uint256 actualDebt = spoke.getUserTotalDebt(testAssetInfo.reserveId, collateralSupplier); + _repay({ + spoke: spoke, + reserveInfo: testAssetInfo, + user: collateralSupplier, + amount: actualDebt + }); + } + + function _testRepayAfterInterest( + ISpoke spoke, + Types.ReserveInfo memory testAssetInfo, + address collateralSupplier + ) internal { + _skipTimeAndCheckAccounting({ + spoke: spoke, + reserveInfo: testAssetInfo, + user: collateralSupplier, + skipDays: vm.randomUint(1, 450) + }); + + skip(vm.randomUint(1, 30) * 1 days); + uint256 debtAfterAccrual = spoke.getUserTotalDebt(testAssetInfo.reserveId, collateralSupplier); + _repay({ + spoke: spoke, + reserveInfo: testAssetInfo, + user: collateralSupplier, + amount: debtAfterAccrual + }); + } + + /// @dev Test liquidation: partial, full (receive underlying), and full (receive shares). + function _testLiquidation( + ISpoke spoke, + Types.ReserveInfo memory collateralInfo, + Types.ReserveInfo memory testAssetInfo, + address collateralSupplier + ) internal { + _makeUserLiquidatable(spoke, collateralSupplier); + + // Skip random 1-90 days to let interest accrue before liquidation + uint256 skipDays = vm.randomUint(1, 90); + skip(skipDays * 1 days); + + // Verify health factor is below 1 after making liquidatable + ISpoke.UserAccountData memory accountData = spoke.getUserAccountData(collateralSupplier); + assertLt( + accountData.healthFactor, + HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + 'HEALTH: should be below 1 for liquidation' + ); + + address liquidator = vm.randomAddress(); + uint256 snapshotBeforeLiquidation = vm.snapshotState(); + + bool receiveSharesEnabled = spoke + .getReserveConfig(collateralInfo.reserveId) + .receiveSharesEnabled; + + // Partial liquidation — only if no dust remains + _testPartialLiquidation({ + spoke: spoke, + collateralInfo: collateralInfo, + testAssetInfo: testAssetInfo, + liquidator: liquidator, + borrower: collateralSupplier, + receiveShares: false + }); + if (receiveSharesEnabled) { + _testPartialLiquidation({ + spoke: spoke, + collateralInfo: collateralInfo, + testAssetInfo: testAssetInfo, + liquidator: liquidator, + borrower: collateralSupplier, + receiveShares: true + }); + } + + // Full liquidation - receive underlying + _liquidationCall({ + spoke: spoke, + collateralInfo: collateralInfo, + debtInfo: testAssetInfo, + liquidator: liquidator, + borrower: collateralSupplier, + debtToCover: UINT256_MAX, + receiveShares: false + }); + vm.revertToState(snapshotBeforeLiquidation); + + // Full liquidation - receive shares (only if enabled on collateral reserve) + if (receiveSharesEnabled) { + _liquidationCall({ + spoke: spoke, + collateralInfo: collateralInfo, + debtInfo: testAssetInfo, + liquidator: liquidator, + borrower: collateralSupplier, + debtToCover: UINT256_MAX, + receiveShares: true + }); + vm.revertToState(snapshotBeforeLiquidation); + } + + // Clear oracle price mocks + vm.clearMockedCalls(); + } + + /// @dev Partial liquidation: only for coll/debt amounts that won't trigger dust threshold reverts + function _testPartialLiquidation( + ISpoke spoke, + Types.ReserveInfo memory collateralInfo, + Types.ReserveInfo memory testAssetInfo, + address liquidator, + address borrower, + bool receiveShares + ) internal { + uint256 snapshot = vm.snapshotState(); + + address oracleAddr = spoke.ORACLE(); + uint256 totalDebt = spoke.getUserTotalDebt(testAssetInfo.reserveId, borrower); + uint256 totalCollateral = spoke.getUserSuppliedAssets(collateralInfo.reserveId, borrower); + // only execute partial liquidations above $1.5k + uint256 liquidationThreshold = 1_500; + uint256 minDebtAssets = _getTokenAmountByDollarValue({ + oracleAddr: oracleAddr, + reserveInfo: testAssetInfo, + dollarValue: liquidationThreshold + }); + uint256 minCollateralAssets = _getTokenAmountByDollarValue({ + oracleAddr: oracleAddr, + reserveInfo: collateralInfo, + dollarValue: liquidationThreshold + }); + + // Skip if either debt or collateral is too small — partial liq leads to dust + if (totalDebt <= minDebtAssets || totalCollateral <= minCollateralAssets) { + console.log('PARTIAL_LIQUIDATION: skipping, position too small after oracle manipulation'); + vm.revertToState(snapshot); + return; + } + + // liquidate only up to $400 so that remaining amounts won't trigger dust threshold reverts + uint256 partialDebt = _getTokenAmountByDollarValue({ + oracleAddr: oracleAddr, + reserveInfo: testAssetInfo, + dollarValue: vm.randomUint(1, 400) + }); + // simple check - ensure at least 1 share worth of debt assets is liquidated + // technically possible to liquidate less if premium debt exists, but serves as a basic check + if ( + spoke.getReserve(testAssetInfo.reserveId).hub.previewRestoreByAssets( + testAssetInfo.reserveId, + partialDebt + ) == 0 + ) { + partialDebt = spoke.getReserve(testAssetInfo.reserveId).hub.previewRestoreByShares( + testAssetInfo.reserveId, + 1 + ); + } + _liquidationCall({ + spoke: spoke, + collateralInfo: collateralInfo, + debtInfo: testAssetInfo, + liquidator: liquidator, + borrower: borrower, + debtToCover: partialDebt, + receiveShares: receiveShares + }); + assertGt( + spoke.getUserTotalDebt(testAssetInfo.reserveId, borrower), + 0, + 'PARTIAL_LIQUIDATION: debt should not be fully repaid' + ); + + vm.revertToState(snapshot); + } + + /// @dev Disable all collaterals, verify borrow reverts, re-enable all, verify borrow works. + function _testCollateralToggle( + ISpoke spoke, + Types.ReserveInfo[] memory goodCollaterals, + Types.ReserveInfo memory testAssetInfo, + address collateralSupplier, + uint256 testAssetAmount + ) internal { + // Disable all active collaterals + for (uint256 i; i < goodCollaterals.length; i++) { + uint256 supplied = spoke.getUserSuppliedAssets( + goodCollaterals[i].reserveId, + collateralSupplier + ); + if (supplied == 0) { + continue; + } + vm.prank(collateralSupplier); + spoke.setUsingAsCollateral({ + reserveId: goodCollaterals[i].reserveId, + usingAsCollateral: false, + onBehalfOf: collateralSupplier + }); + } + + // Borrow should revert with HealthFactorBelowThreshold (no collateral backing) + uint256 smallBorrow = testAssetAmount > 10 ? testAssetAmount / 10 : testAssetAmount; + _ensureLiquidity({spoke: spoke, reserveInfo: testAssetInfo, amount: smallBorrow}); + vm.prank(collateralSupplier); + vm.expectRevert(ISpoke.HealthFactorBelowThreshold.selector); + spoke.borrow({ + reserveId: testAssetInfo.reserveId, + amount: smallBorrow, + onBehalfOf: collateralSupplier + }); + + // Re-enable all collaterals + for (uint256 i; i < goodCollaterals.length; i++) { + uint256 supplied = spoke.getUserSuppliedAssets( + goodCollaterals[i].reserveId, + collateralSupplier + ); + if (supplied == 0) { + continue; + } + vm.prank(collateralSupplier); + spoke.setUsingAsCollateral({ + reserveId: goodCollaterals[i].reserveId, + usingAsCollateral: true, + onBehalfOf: collateralSupplier + }); + } + + // Borrow should succeed now + _borrow({ + spoke: spoke, + reserveInfo: testAssetInfo, + user: collateralSupplier, + amount: smallBorrow + }); + } + + /// @dev Compute a random extra count bounded by remaining reserve slots and available reserves. + function _randomExtraCount( + ISpoke spoke, + address user, + uint256 available + ) internal view returns (uint256) { + uint16 maxUserReserves = spoke.MAX_USER_RESERVES_LIMIT(); + uint256 currentCount = spoke.getUserAccountData(user).activeCollateralCount; + uint256 remainingSlots = currentCount < maxUserReserves ? maxUserReserves - currentCount : 0; + uint256 maxExtra = remainingSlots < available ? remainingSlots : available; + return maxExtra > 0 ? vm.randomUint(0, maxExtra) : 0; + } + + /// @dev Borrow from random extra reserves, respecting MAX_USER_RESERVES_LIMIT. + function _borrowExtrasWithinLimit(ISpoke spoke, uint256 primaryReserveId, address user) internal { + Types.ReserveInfo[] memory allReserves = _getReserveInfo(spoke); + Types.ReserveInfo[] memory usableDebtReserves = _getAllUsableDebtReserves(allReserves); + uint16 maxUserReserves = spoke.MAX_USER_RESERVES_LIMIT(); + uint256 currentBorrowCount = spoke.getUserAccountData(user).borrowCount; + uint256 remainingSlots = currentBorrowCount < maxUserReserves + ? maxUserReserves - currentBorrowCount + : 0; + if (remainingSlots == 0) { + return; + } + uint256 extraBorrowCount = vm.randomUint(0, remainingSlots); + _borrowRandomExtraReserves({ + spoke: spoke, + usableDebtReserves: usableDebtReserves, + primaryReserveId: primaryReserveId, + oracleAddr: spoke.ORACLE(), + user: user, + extraCount: extraBorrowCount + }); + } + + /// @dev Test spoke addCap and drawCap by incrementally filling to the cap, then verify overflow reverts. + function _testCaps(ISpoke spoke, Types.ReserveInfo memory reserveInfo) internal { + IHub.SpokeConfig memory spokeConfig = IHub(reserveInfo.hub).getSpokeConfig( + reserveInfo.assetId, + address(spoke) + ); + + if (spokeConfig.addCap > 0 && spokeConfig.addCap < type(uint40).max) { + uint256 snap = vm.snapshotState(); + _testAddCap({spoke: spoke, reserveInfo: reserveInfo, addCap: spokeConfig.addCap}); + vm.revertToState(snap); + } + + if ( + spokeConfig.drawCap > 0 && spokeConfig.drawCap < type(uint40).max && reserveInfo.borrowable + ) { + uint256 snap = vm.snapshotState(); + _testDrawCap({spoke: spoke, reserveInfo: reserveInfo, drawCap: spokeConfig.drawCap}); + vm.revertToState(snap); + } + } + + /// @dev Fill supply up to addCap in random chunks, then verify overflow reverts. + function _testAddCap(ISpoke spoke, Types.ReserveInfo memory reserveInfo, uint40 addCap) internal { + uint256 addCapScaled = uint256(addCap) * 10 ** reserveInfo.decimals; + uint256 currentSupply = spoke.getReserveSuppliedAssets(reserveInfo.reserveId); + if (addCapScaled <= currentSupply) { + return; + } + + uint256 room = addCapScaled - currentSupply; + address supplier = vm.randomAddress(); + + // Supply more than addCap — should revert with AddCapExceeded + uint256 overflowAmount = room + 10 ** reserveInfo.decimals; + vm.startPrank(supplier); + deal2({asset: reserveInfo.underlying, user: supplier, amount: overflowAmount}); + IERC20(reserveInfo.underlying).forceApprove(address(spoke), overflowAmount); + vm.expectRevert(abi.encodeWithSelector(IHub.AddCapExceeded.selector, uint256(addCap))); + spoke.supply({reserveId: reserveInfo.reserveId, amount: overflowAmount, onBehalfOf: supplier}); + vm.stopPrank(); + } + + /// @dev Fill borrows up to drawCap in random chunks, then verify overflow reverts. + function _testDrawCap( + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + uint40 drawCap + ) internal { + // Remove addCaps so enough collateral can be supplied to borrow up to drawCap + _setAddCapsToMax(spoke); + + console.log('TEST_DRAW_CAP: drawCap=%e', drawCap); + address borrower = vm.randomAddress(); + uint256 drawCapScaled = uint256(drawCap) * 10 ** reserveInfo.decimals; + uint256 currentDebt = spoke.getReserveTotalDebt(reserveInfo.reserveId); + if (drawCapScaled <= currentDebt) { + return; + } + + uint256 room = drawCapScaled - currentDebt; + + // Supply the debt asset itself as collateral (3x room for borrow headroom) + liquidity + uint256 collateralAmount = room * 10; + _supply({spoke: spoke, reserveInfo: reserveInfo, user: borrower, amount: collateralAmount}); + vm.prank(borrower); + spoke.setUsingAsCollateral({ + reserveId: reserveInfo.reserveId, + usingAsCollateral: true, + onBehalfOf: borrower + }); + + // Supply liquidity from a separate provider + address liquidityProvider = vm.randomAddress(); + _supply({spoke: spoke, reserveInfo: reserveInfo, user: liquidityProvider, amount: room}); + + // Borrow more than drawCap — should revert with DrawCapExceeded + uint256 overflowAmount = room + 10 ** reserveInfo.decimals; + vm.prank(borrower); + vm.expectRevert(abi.encodeWithSelector(IHub.DrawCapExceeded.selector, uint256(drawCap))); + spoke.borrow({reserveId: reserveInfo.reserveId, amount: overflowAmount, onBehalfOf: borrower}); + } + + /// @dev Test that 0-amount operations revert. + function _testZeroAmountReverts( + ISpoke spoke, + Types.ReserveInfo memory reserveInfo, + address user + ) internal { + uint256 reserveId = reserveInfo.reserveId; + + // Supply 0 + vm.startPrank(user); + IERC20(reserveInfo.underlying).forceApprove(address(spoke), 0); + vm.expectRevert(); + spoke.supply({reserveId: reserveId, amount: 0, onBehalfOf: user}); + vm.stopPrank(); + + // Withdraw 0 + vm.prank(user); + vm.expectRevert(); + spoke.withdraw({reserveId: reserveId, amount: 0, onBehalfOf: user}); + + // Borrow 0 + if (reserveInfo.borrowable) { + vm.prank(user); + vm.expectRevert(); + spoke.borrow({reserveId: reserveId, amount: 0, onBehalfOf: user}); + } + + // Repay 0 + vm.startPrank(user); + IERC20(reserveInfo.underlying).forceApprove(address(spoke), 0); + vm.expectRevert(); + spoke.repay({reserveId: reserveId, amount: 0, onBehalfOf: user}); + vm.stopPrank(); + } +} diff --git a/src/dependencies/v4/SnapshotV4.sol b/src/dependencies/v4/SnapshotV4.sol new file mode 100644 index 00000000..501f58fd --- /dev/null +++ b/src/dependencies/v4/SnapshotV4.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IERC20Metadata} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {ISpoke, IHub, IAaveOracle} from 'aave-address-book/AaveV4.sol'; +import {IAssetInterestRateStrategy} from 'aave-v4/hub/interfaces/IAssetInterestRateStrategy.sol'; +import {Types} from 'src/dependencies/v4/Types.sol'; +import {V4DiffWriter} from 'src/dependencies/v4/V4DiffWriter.sol'; +import {Helpers} from 'src/dependencies/v4/Helpers.sol'; + +/// @title SnapshotV4 +/// @notice Snapshot capture for Aave V4. JSON + markdown diff delegated to V4DiffWriter. +abstract contract SnapshotV4 is Helpers { + /// @notice Capture a full V4 configuration snapshot from the given spokes and hubs. + function createV4Snapshot( + ISpoke[] memory spokes, + IHub[] memory hubs + ) internal view returns (Types.V4Snapshot memory snapshot) { + snapshot.spokeReserves = _snapshotSpokeReserves(spokes); + snapshot.spokeLiquidationConfigs = _snapshotSpokeLiqConfigs(spokes); + snapshot.hubAssets = _snapshotHubAssets(hubs); + snapshot.spokeCaps = _snapshotSpokeCaps(hubs); + } + + /// @notice Write a V4 snapshot to JSON file. + function writeV4SnapshotJson(string memory name, Types.V4Snapshot memory snap) internal { + V4DiffWriter.writeSnapshotJson(name, snap); + } + + /// @notice Generate markdown diff between two snapshots. + function diffV4Snapshots( + string memory reportName, + Types.V4Snapshot memory snapBefore, + Types.V4Snapshot memory snapAfter + ) internal { + V4DiffWriter.writeDiff(reportName, snapBefore, snapAfter); + } + + // Spoke reserves + + function _snapshotSpokeReserves( + ISpoke[] memory spokes + ) private view returns (Types.SpokeReserveSnapshot[] memory) { + uint256 total; + for (uint256 s; s < spokes.length; s++) total += spokes[s].getReserveCount(); + + Types.SpokeReserveSnapshot[] memory result = new Types.SpokeReserveSnapshot[](total); + uint256 idx; + for (uint256 s; s < spokes.length; s++) { + uint256 count = spokes[s].getReserveCount(); + for (uint256 i; i < count; i++) { + result[idx++] = _snapshotReserve(spokes[s], i); + } + } + return result; + } + + function _snapshotReserve( + ISpoke spoke, + uint256 reserveId + ) private view returns (Types.SpokeReserveSnapshot memory snap) { + ISpoke.Reserve memory reserve = spoke.getReserve(reserveId); + ISpoke.ReserveConfig memory config = spoke.getReserveConfig(reserveId); + ISpoke.DynamicReserveConfig memory dyn = spoke.getDynamicReserveConfig( + reserveId, + reserve.dynamicConfigKey + ); + + snap.spokeAddress = address(spoke); + snap.reserveId = reserveId; + snap.underlying = reserve.underlying; + snap.symbol = _safeSymbol(reserve.underlying); + snap.hub = address(reserve.hub); + snap.assetId = reserve.assetId; + snap.decimals = reserve.decimals; + snap.collateralRisk = config.collateralRisk; + snap.paused = config.paused; + snap.frozen = config.frozen; + snap.borrowable = config.borrowable; + snap.receiveSharesEnabled = config.receiveSharesEnabled; + snap.dynamicConfigKey = reserve.dynamicConfigKey; + snap.collateralFactor = dyn.collateralFactor; + snap.maxLiquidationBonus = dyn.maxLiquidationBonus; + snap.liquidationFee = dyn.liquidationFee; + + address oracleAddr = spoke.ORACLE(); + snap.oracleAddress = oracleAddr; + try IAaveOracle(oracleAddr).getReserveSource(reserveId) returns (address src) { + snap.priceSource = src; + } catch {} + try IAaveOracle(oracleAddr).getReservePrice(reserveId) returns (uint256 price) { + snap.oraclePrice = price; + } catch {} + } + + // Spoke liquidation configs + + function _snapshotSpokeLiqConfigs( + ISpoke[] memory spokes + ) private view returns (Types.SpokeLiquidationSnapshot[] memory) { + Types.SpokeLiquidationSnapshot[] memory result = new Types.SpokeLiquidationSnapshot[]( + spokes.length + ); + for (uint256 s; s < spokes.length; s++) { + ISpoke.LiquidationConfig memory liq = spokes[s].getLiquidationConfig(); + result[s] = Types.SpokeLiquidationSnapshot({ + spokeAddress: address(spokes[s]), + targetHealthFactor: liq.targetHealthFactor, + healthFactorForMaxBonus: liq.healthFactorForMaxBonus, + liquidationBonusFactor: liq.liquidationBonusFactor, + maxUserReservesLimit: spokes[s].MAX_USER_RESERVES_LIMIT() + }); + } + return result; + } + + // Hub assets + + function _snapshotHubAssets( + IHub[] memory hubs + ) private view returns (Types.HubAssetSnapshot[] memory) { + uint256 total; + for (uint256 h; h < hubs.length; h++) total += hubs[h].getAssetCount(); + + Types.HubAssetSnapshot[] memory result = new Types.HubAssetSnapshot[](total); + uint256 idx; + for (uint256 h; h < hubs.length; h++) { + uint256 count = hubs[h].getAssetCount(); + for (uint256 a; a < count; a++) { + result[idx++] = _snapshotHubAsset(hubs[h], a); + } + } + return result; + } + + function _snapshotHubAsset( + IHub hub, + uint256 assetId + ) private view returns (Types.HubAssetSnapshot memory snap) { + IHub.AssetConfig memory config = hub.getAssetConfig(assetId); + (address underlying, uint8 decimals) = hub.getAssetUnderlyingAndDecimals(assetId); + + snap.hubAddress = address(hub); + snap.assetId = assetId; + snap.underlying = underlying; + snap.symbol = _safeSymbol(underlying); + snap.decimals = decimals; + snap.liquidityFee = config.liquidityFee; + snap.irStrategy = config.irStrategy; + snap.feeReceiver = config.feeReceiver; + snap.reinvestmentController = config.reinvestmentController; + + if (config.irStrategy != address(0)) { + try IAssetInterestRateStrategy(config.irStrategy).getInterestRateData(assetId) returns ( + IAssetInterestRateStrategy.InterestRateData memory irData + ) { + snap.optimalUsageRatio = irData.optimalUsageRatio; + snap.baseDrawnRate = irData.baseDrawnRate; + snap.rateGrowthBeforeOptimal = irData.rateGrowthBeforeOptimal; + snap.rateGrowthAfterOptimal = irData.rateGrowthAfterOptimal; + } catch {} + try IAssetInterestRateStrategy(config.irStrategy).getMaxDrawnRate(assetId) returns ( + uint256 rate + ) { + snap.maxDrawnRate = rate; + } catch {} + } + } + + // Hub spoke caps + + function _snapshotSpokeCaps( + IHub[] memory hubs + ) private view returns (Types.SpokeCapSnapshot[] memory) { + uint256 total; + for (uint256 h; h < hubs.length; h++) { + uint256 ac = hubs[h].getAssetCount(); + for (uint256 a; a < ac; a++) total += hubs[h].getSpokeCount(a); + } + + Types.SpokeCapSnapshot[] memory result = new Types.SpokeCapSnapshot[](total); + uint256 idx; + for (uint256 h; h < hubs.length; h++) { + idx = _snapshotCapsForHub(hubs[h], result, idx); + } + return result; + } + + function _snapshotCapsForHub( + IHub hub, + Types.SpokeCapSnapshot[] memory result, + uint256 idx + ) private view returns (uint256) { + uint256 ac = hub.getAssetCount(); + for (uint256 a; a < ac; a++) { + (address underlying, ) = hub.getAssetUnderlyingAndDecimals(a); + string memory sym = _safeSymbol(underlying); + uint256 sc = hub.getSpokeCount(a); + for (uint256 sp; sp < sc; sp++) { + address spokeAddr = hub.getSpokeAddress(a, sp); + IHub.SpokeConfig memory cfg = hub.getSpokeConfig(a, spokeAddr); + result[idx++] = Types.SpokeCapSnapshot({ + hubAddress: address(hub), + assetId: a, + assetSymbol: sym, + spokeAddress: spokeAddr, + addCap: cfg.addCap, + drawCap: cfg.drawCap, + riskPremiumThreshold: cfg.riskPremiumThreshold, + active: cfg.active, + halted: cfg.halted + }); + } + } + return idx; + } +} diff --git a/src/dependencies/v4/TokenizationActions.sol b/src/dependencies/v4/TokenizationActions.sol new file mode 100644 index 00000000..53adef0e --- /dev/null +++ b/src/dependencies/v4/TokenizationActions.sol @@ -0,0 +1,568 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; +import {ITokenizationSpoke, ISpoke} from 'aave-address-book/AaveV4.sol'; +import {IHubBase} from 'aave-v4/hub/interfaces/IHubBase.sol'; +import {Types} from 'src/dependencies/v4/Types.sol'; +import {Helpers} from 'src/dependencies/v4/Helpers.sol'; + +/// @title TokenizationActions +/// @notice Low-level tokenization spoke (ERC4626) actions with hub accounting assertions. +abstract contract TokenizationActions is Helpers { + using SafeERC20 for IERC20; + + // ------------------------------------------------------------------------- + // Snapshot getter + // ------------------------------------------------------------------------- + function _getTokenizationSnapshot( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + address user + ) internal view returns (Types.TokenizationSnapshot memory) { + uint256 userShares = tokenizationSpoke.balanceOf(user); + return + Types.TokenizationSnapshot({ + userShares: userShares, + userAssets: userShares > 0 ? tokenizationSpoke.convertToAssets(userShares) : 0, + totalShares: tokenizationSpoke.totalSupply(), + totalAssets: tokenizationSpoke.totalAssets(), + spokeOnHub: _getSpokeOnHubAccounting(ISpoke(address(tokenizationSpoke)), reserveInfo) + }); + } + + // ------------------------------------------------------------------------- + // Hub invariant: tokenization spoke never borrows + // ------------------------------------------------------------------------- + function _assertTokenizationNoDebt(Types.TokenizationSnapshot memory snapshot) internal pure { + assertEq(snapshot.spokeOnHub.drawnDebt, 0, 'TOKENIZATION: hub drawn debt should be zero'); + assertEq(snapshot.spokeOnHub.drawnShares, 0, 'TOKENIZATION: hub drawn shares should be zero'); + assertEq(snapshot.spokeOnHub.totalDebt, 0, 'TOKENIZATION: hub total debt should be zero'); + } + + // ------------------------------------------------------------------------- + // Actions + // ------------------------------------------------------------------------- + function _tokenizationDeposit( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + address user, + uint256 assets + ) internal { + Types.TokenizationSnapshot memory snapshotBefore = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + + uint256 expectedShares = tokenizationSpoke.previewDeposit(assets); + + vm.startPrank(user); + deal2(reserveInfo.underlying, user, assets); + IERC20(reserveInfo.underlying).forceApprove(address(tokenizationSpoke), assets); + _logAction('TOKENIZATION_DEPOSIT', reserveInfo.symbol, assets); + uint256 sharesReturned = tokenizationSpoke.deposit(assets, user); + vm.stopPrank(); + + Types.TokenizationSnapshot memory snapshotAfter = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + + // Returned shares should match preview + assertEq( + sharesReturned, + expectedShares, + 'TOKENIZATION_DEPOSIT: returned shares mismatch with preview' + ); + // User shares increased + assertEq( + snapshotAfter.userShares, + snapshotBefore.userShares + sharesReturned, + 'TOKENIZATION_DEPOSIT: user shares mismatch' + ); + // Vault totalAssets increased + assertApproxEqAbs( + snapshotAfter.totalAssets, + snapshotBefore.totalAssets + assets, + 1, + 'TOKENIZATION_DEPOSIT: totalAssets mismatch' + ); + // Hub spoke collateral increased + assertApproxEqAbs( + snapshotAfter.spokeOnHub.collateralAssets, + snapshotBefore.spokeOnHub.collateralAssets + assets, + 1, + 'TOKENIZATION_DEPOSIT: hub collateral assets mismatch' + ); + { + uint256 expectedAddedShares = IHubBase(reserveInfo.hub).previewAddByAssets( + reserveInfo.assetId, + assets + ); + assertEq( + snapshotAfter.spokeOnHub.collateralShares, + snapshotBefore.spokeOnHub.collateralShares + expectedAddedShares, + 'TOKENIZATION_DEPOSIT: hub collateral shares mismatch' + ); + } + _assertTokenizationNoDebt(snapshotAfter); + } + + function _tokenizationMint( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + address user, + uint256 shares + ) internal { + Types.TokenizationSnapshot memory snapshotBefore = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + + uint256 expectedAssets = tokenizationSpoke.previewMint(shares); + + vm.startPrank(user); + // Add some extra assets to avoid rounding errors + deal2(reserveInfo.underlying, user, expectedAssets * 2); + IERC20(reserveInfo.underlying).forceApprove(address(tokenizationSpoke), expectedAssets * 2); + _logAction('TOKENIZATION_MINT', reserveInfo.symbol, shares); + uint256 assetsDeposited = tokenizationSpoke.mint(shares, user); + vm.stopPrank(); + + Types.TokenizationSnapshot memory snapshotAfter = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + + // Assets deposited should match preview + assertEq( + assetsDeposited, + expectedAssets, + 'TOKENIZATION_MINT: deposited assets mismatch with preview' + ); + // User shares increased by exact amount + assertEq( + snapshotAfter.userShares, + snapshotBefore.userShares + shares, + 'TOKENIZATION_MINT: user shares mismatch' + ); + // Vault totalAssets increased + assertApproxEqAbs( + snapshotAfter.totalAssets, + snapshotBefore.totalAssets + assetsDeposited, + 1, + 'TOKENIZATION_MINT: totalAssets mismatch' + ); + // Hub spoke collateral increased + assertApproxEqAbs( + snapshotAfter.spokeOnHub.collateralAssets, + snapshotBefore.spokeOnHub.collateralAssets + assetsDeposited, + 1, + 'TOKENIZATION_MINT: hub collateral assets mismatch' + ); + _assertTokenizationNoDebt(snapshotAfter); + } + + function _tokenizationWithdraw( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + address user, + uint256 assets + ) internal { + Types.TokenizationSnapshot memory snapshotBefore = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + + uint256 expectedSharesBurned = tokenizationSpoke.previewWithdraw(assets); + + vm.prank(user); + _logAction('TOKENIZATION_WITHDRAW', reserveInfo.symbol, assets); + uint256 sharesBurned = tokenizationSpoke.withdraw(assets, user, user); + + Types.TokenizationSnapshot memory snapshotAfter = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + + // Shares burned should match preview + assertEq( + sharesBurned, + expectedSharesBurned, + 'TOKENIZATION_WITHDRAW: shares burned mismatch with preview' + ); + // User shares decreased + assertEq( + snapshotAfter.userShares, + snapshotBefore.userShares - sharesBurned, + 'TOKENIZATION_WITHDRAW: user shares mismatch' + ); + // Vault totalAssets decreased + assertApproxEqAbs( + snapshotBefore.totalAssets - snapshotAfter.totalAssets, + assets, + 1, + 'TOKENIZATION_WITHDRAW: totalAssets mismatch' + ); + // Hub spoke collateral decreased + assertApproxEqAbs( + snapshotBefore.spokeOnHub.collateralAssets - snapshotAfter.spokeOnHub.collateralAssets, + assets, + 1, + 'TOKENIZATION_WITHDRAW: hub collateral assets mismatch' + ); + _assertTokenizationNoDebt(snapshotAfter); + } + + function _tokenizationRedeem( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + address user, + uint256 shares + ) internal { + Types.TokenizationSnapshot memory snapshotBefore = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + + uint256 expectedAssets = tokenizationSpoke.previewRedeem(shares); + + vm.prank(user); + _logAction('TOKENIZATION_REDEEM', reserveInfo.symbol, shares); + uint256 assetsReceived = tokenizationSpoke.redeem(shares, user, user); + + Types.TokenizationSnapshot memory snapshotAfter = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + + // Assets received should match preview + assertEq( + assetsReceived, + expectedAssets, + 'TOKENIZATION_REDEEM: assets received mismatch with preview' + ); + // User shares decreased by exact amount + assertEq( + snapshotAfter.userShares, + snapshotBefore.userShares - shares, + 'TOKENIZATION_REDEEM: user shares mismatch' + ); + // If full redeem, shares should be zero + if (shares == snapshotBefore.userShares) { + assertEq(snapshotAfter.userShares, 0, 'TOKENIZATION_REDEEM: user shares should be zero'); + } + // Vault totalAssets decreased + assertApproxEqAbs( + snapshotBefore.totalAssets - snapshotAfter.totalAssets, + assetsReceived, + 1, + 'TOKENIZATION_REDEEM: totalAssets mismatch' + ); + // Hub spoke collateral decreased + assertApproxEqAbs( + snapshotBefore.spokeOnHub.collateralAssets - snapshotAfter.spokeOnHub.collateralAssets, + assetsReceived, + 1, + 'TOKENIZATION_REDEEM: hub collateral assets mismatch' + ); + _assertTokenizationNoDebt(snapshotAfter); + } + + function _tokenizationMintWithSig( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + uint256 privateKey, + uint256 shares + ) internal { + address user = vm.addr(privateKey); + uint256 userSharesBefore = tokenizationSpoke.balanceOf(user); + uint256 totalAssetsBefore = tokenizationSpoke.totalAssets(); + uint256 hubCollateralBefore = IHubBase(reserveInfo.hub).getSpokeAddedAssets( + reserveInfo.assetId, + address(tokenizationSpoke) + ); + + uint256 assetsDeposited = _executeTokenizationMintWithSig({ + tokenizationSpoke: tokenizationSpoke, + reserveInfo: reserveInfo, + privateKey: privateKey, + user: user, + shares: shares + }); + + assertEq( + tokenizationSpoke.balanceOf(user) - userSharesBefore, + shares, + 'TOKENIZATION_MINT_WITH_SIG: user shares mismatch' + ); + assertApproxEqAbs( + tokenizationSpoke.totalAssets() - totalAssetsBefore, + assetsDeposited, + 1, + 'TOKENIZATION_MINT_WITH_SIG: totalAssets mismatch' + ); + assertApproxEqAbs( + IHubBase(reserveInfo.hub).getSpokeAddedAssets( + reserveInfo.assetId, + address(tokenizationSpoke) + ) - hubCollateralBefore, + assetsDeposited, + 1, + 'TOKENIZATION_MINT_WITH_SIG: hub collateral assets mismatch' + ); + } + + function _executeTokenizationMintWithSig( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + uint256 privateKey, + address user, + uint256 shares + ) internal returns (uint256 assetsDeposited) { + uint256 expectedAssets = tokenizationSpoke.previewMint(shares); + _logAction('TOKENIZATION_MINT_WITH_SIG', reserveInfo.symbol, shares); + + deal2(reserveInfo.underlying, user, expectedAssets * 2); + vm.prank(user); + IERC20(reserveInfo.underlying).forceApprove(address(tokenizationSpoke), expectedAssets * 2); + + uint192 nonceKey = tokenizationSpoke.PERMIT_NONCE_NAMESPACE(); + uint256 nonce = tokenizationSpoke.nonces(user, nonceKey); + + { + uint256 deadline = vm.getBlockTimestamp() + 1 hours; + ITokenizationSpoke.TokenizedMint memory params = ITokenizationSpoke.TokenizedMint({ + depositor: user, + shares: shares, + receiver: user, + nonce: nonce, + deadline: deadline + }); + bytes32 structHash = keccak256( + abi.encode( + tokenizationSpoke.MINT_TYPEHASH(), + params.depositor, + params.shares, + params.receiver, + params.nonce, + params.deadline + ) + ); + bytes32 digest = keccak256( + abi.encodePacked('\x19\x01', tokenizationSpoke.DOMAIN_SEPARATOR(), structHash) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + assetsDeposited = tokenizationSpoke.mintWithSig(params, abi.encodePacked(r, s, v)); + } + + assertEq( + assetsDeposited, + expectedAssets, + 'TOKENIZATION_MINT_WITH_SIG: assets mismatch with preview' + ); + assertEq( + tokenizationSpoke.nonces(user, nonceKey), + nonce + 1, + 'TOKENIZATION_MINT_WITH_SIG: nonce not incremented' + ); + } + + function _tokenizationRedeemWithSig( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + uint256 userPrivateKey, + uint256 shares + ) internal { + address user = vm.addr(userPrivateKey); + uint256 userSharesBefore = tokenizationSpoke.balanceOf(user); + uint256 totalAssetsBefore = tokenizationSpoke.totalAssets(); + uint256 hubCollateralBefore = IHubBase(reserveInfo.hub).getSpokeAddedAssets( + reserveInfo.assetId, + address(tokenizationSpoke) + ); + + uint256 assetsReceived = _executeTokenizationRedeemWithSig({ + tokenizationSpoke: tokenizationSpoke, + reserveInfo: reserveInfo, + privateKey: userPrivateKey, + user: user, + shares: shares + }); + + assertEq( + userSharesBefore - tokenizationSpoke.balanceOf(user), + shares, + 'TOKENIZATION_REDEEM_WITH_SIG: user shares mismatch' + ); + if (shares == userSharesBefore) { + assertEq( + tokenizationSpoke.balanceOf(user), + 0, + 'TOKENIZATION_REDEEM_WITH_SIG: user shares should be zero' + ); + } + assertApproxEqAbs( + totalAssetsBefore - tokenizationSpoke.totalAssets(), + assetsReceived, + 1, + 'TOKENIZATION_REDEEM_WITH_SIG: totalAssets mismatch' + ); + assertApproxEqAbs( + hubCollateralBefore - + IHubBase(reserveInfo.hub).getSpokeAddedAssets( + reserveInfo.assetId, + address(tokenizationSpoke) + ), + assetsReceived, + 1, + 'TOKENIZATION_REDEEM_WITH_SIG: hub collateral assets mismatch' + ); + } + + function _executeTokenizationRedeemWithSig( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + uint256 privateKey, + address user, + uint256 shares + ) internal returns (uint256 assetsReceived) { + uint256 expectedAssets = tokenizationSpoke.previewRedeem(shares); + _logAction('TOKENIZATION_REDEEM_WITH_SIG', reserveInfo.symbol, shares); + + uint192 nonceKey = tokenizationSpoke.PERMIT_NONCE_NAMESPACE(); + uint256 nonce = tokenizationSpoke.nonces(user, nonceKey); + + { + uint256 deadline = vm.getBlockTimestamp() + 1 hours; + ITokenizationSpoke.TokenizedRedeem memory params = ITokenizationSpoke.TokenizedRedeem({ + owner: user, + shares: shares, + receiver: user, + nonce: nonce, + deadline: deadline + }); + bytes32 structHash = keccak256( + abi.encode( + tokenizationSpoke.REDEEM_TYPEHASH(), + params.owner, + params.shares, + params.receiver, + params.nonce, + params.deadline + ) + ); + bytes32 digest = keccak256( + abi.encodePacked('\x19\x01', tokenizationSpoke.DOMAIN_SEPARATOR(), structHash) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + assetsReceived = tokenizationSpoke.redeemWithSig(params, abi.encodePacked(r, s, v)); + } + + assertEq( + assetsReceived, + expectedAssets, + 'TOKENIZATION_REDEEM_WITH_SIG: assets mismatch with preview' + ); + assertEq( + tokenizationSpoke.nonces(user, nonceKey), + nonce + 1, + 'TOKENIZATION_REDEEM_WITH_SIG: nonce not incremented' + ); + } + + /// @dev Build the EIP-2612 permit digest for the underlying token. + /// `depositWithPermit` permits the underlying, not the vault share token. + function _buildUnderlyingPermitDigest( + address underlying, + address owner, + address spender, + uint256 value, + uint256 deadline + ) internal view returns (bytes32) { + // Query nonces and DOMAIN_SEPARATOR from the underlying ERC20Permit token + (, bytes memory nonceData) = underlying.staticcall( + abi.encodeWithSignature('nonces(address)', owner) + ); + uint256 nonce = abi.decode(nonceData, (uint256)); + + (, bytes memory dsData) = underlying.staticcall(abi.encodeWithSignature('DOMAIN_SEPARATOR()')); + bytes32 domainSeparator = abi.decode(dsData, (bytes32)); + + bytes32 permitTypehash = keccak256( + 'Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)' + ); + bytes32 structHash = keccak256( + abi.encode(permitTypehash, owner, spender, value, nonce, deadline) + ); + return keccak256(abi.encodePacked('\x19\x01', domainSeparator, structHash)); + } + + function _tokenizationDepositWithPermit( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + uint256 assets + ) internal { + (address user, uint256 userPrivateKey) = makeAddrAndKey('user'); + + Types.TokenizationSnapshot memory snapshotBefore = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + + deal2(reserveInfo.underlying, user, assets); + + uint256 deadline = vm.getBlockTimestamp() + 1 hours; + bytes32 digest = _buildUnderlyingPermitDigest( + reserveInfo.underlying, + user, + address(tokenizationSpoke), + assets, + deadline + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); + + vm.prank(user); + _logAction('TOKENIZATION_DEPOSIT_WITH_PERMIT', reserveInfo.symbol, assets); + uint256 sharesReturned = tokenizationSpoke.depositWithPermit(assets, user, deadline, v, r, s); + + Types.TokenizationSnapshot memory snapshotAfter = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + + // User shares increased + assertEq( + snapshotAfter.userShares, + snapshotBefore.userShares + sharesReturned, + 'TOKENIZATION_DEPOSIT_WITH_PERMIT: user shares mismatch' + ); + // Vault totalAssets increased + assertApproxEqAbs( + snapshotAfter.totalAssets, + snapshotBefore.totalAssets + assets, + 1, + 'TOKENIZATION_DEPOSIT_WITH_PERMIT: totalAssets mismatch' + ); + // Hub spoke collateral increased + assertApproxEqAbs( + snapshotAfter.spokeOnHub.collateralAssets, + snapshotBefore.spokeOnHub.collateralAssets + assets, + 1, + 'TOKENIZATION_DEPOSIT_WITH_PERMIT: hub collateral assets mismatch' + ); + _assertTokenizationNoDebt(snapshotAfter); + } +} diff --git a/src/dependencies/v4/TokenizationScenarios.sol b/src/dependencies/v4/TokenizationScenarios.sol new file mode 100644 index 00000000..fed57f2e --- /dev/null +++ b/src/dependencies/v4/TokenizationScenarios.sol @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; +import {ITokenizationSpoke, IHub, IHubConfigurator} from 'aave-address-book/AaveV4.sol'; +import {AaveV4Ethereum} from 'aave-address-book/AaveV4Ethereum.sol'; +import {Types} from 'src/dependencies/v4/Types.sol'; +import {TokenizationActions} from 'src/dependencies/v4/TokenizationActions.sol'; + +/// @title TokenizationScenarios +/// @notice Test scenario orchestration for tokenization spoke (ERC4626) e2e tests. +abstract contract TokenizationScenarios is TokenizationActions { + using SafeERC20 for IERC20; + + /// @notice Build ReserveInfo from a tokenization spoke's identity getters. + function _getTokenizationReserveInfo( + ITokenizationSpoke tokenizationSpoke + ) internal view returns (Types.ReserveInfo memory) { + address hub = tokenizationSpoke.hub(); + uint16 assetId = uint16(tokenizationSpoke.assetId()); + address underlying = tokenizationSpoke.asset(); + uint8 decimals = tokenizationSpoke.decimals(); + string memory symbol = _safeSymbol(underlying); + + return + Types.ReserveInfo({ + reserveId: 0, + underlying: underlying, + hub: hub, + assetId: assetId, + symbol: symbol, + decimals: decimals, + paused: false, + frozen: false, + borrowable: false, + collateralEnabled: false, + collateralFactor: 0, + maxLiquidationBonus: 0, + liquidationFee: 0 + }); + } + + /// @notice Set addCap to max for a tokenization spoke's asset. + function _setTokenizationCapsToMax(ITokenizationSpoke tokenizationSpoke) internal { + vm.mockCall( + address(AaveV4Ethereum.ACCESS_MANAGER), + abi.encodeWithSelector(bytes4(keccak256('canCall(address,address,bytes4)'))), + abi.encode(true, uint32(0)) + ); + AaveV4Ethereum.HUB_CONFIGURATOR.updateSpokeCaps({ + hub: tokenizationSpoke.hub(), + assetId: tokenizationSpoke.assetId(), + spoke: address(tokenizationSpoke), + addCap: type(uint40).max, + drawCap: type(uint40).max + }); + vm.clearMockedCalls(); + } + + // ------------------------------------------------------------------------- + // Scenarios + // ------------------------------------------------------------------------- + + /// @dev Test deposit + partial withdraw + full redeem cycle. + function _testTokenizationDepositWithdraw( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + uint256 maxAddAmount + ) internal { + (address user, uint256 userPrivateKey) = makeAddrAndKey('user'); + uint256 depositAmount = vm.randomUint(1, maxAddAmount); + + // Deposit + _tokenizationDeposit(tokenizationSpoke, reserveInfo, user, depositAmount); + + // Partial withdraw + uint256 userAssets = tokenizationSpoke.convertToAssets(tokenizationSpoke.balanceOf(user)); + uint256 snapshot = vm.snapshotState(); + if (userAssets > 1) { + uint256 partialWithdraw = vm.randomUint(1, userAssets - 1); + _tokenizationWithdraw(tokenizationSpoke, reserveInfo, user, partialWithdraw); + vm.revertToState(snapshot); + } + + // Full redeem + _tokenizationRedeem(tokenizationSpoke, reserveInfo, user, tokenizationSpoke.balanceOf(user)); + assertEq(tokenizationSpoke.balanceOf(user), 0, 'DEPOSIT_WITHDRAW: user should have no shares'); + vm.revertToState(snapshot); + + // Full redeem with sig + _tokenizationRedeemWithSig( + tokenizationSpoke, + reserveInfo, + userPrivateKey, + tokenizationSpoke.balanceOf(user) + ); + assertEq(tokenizationSpoke.balanceOf(user), 0, 'DEPOSIT_WITHDRAW: user should have no shares'); + vm.revertToState(snapshot); + } + + /// @dev Test mint + partial redeem + full redeem cycle, including mintWithSig. + function _testTokenizationMintRedeem( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + uint256 maxAddAmount + ) internal { + (address user, uint256 userPrivateKey) = makeAddrAndKey('mintUser'); + uint256 mintAssets = vm.randomUint(1, maxAddAmount); + uint256 mintShares = tokenizationSpoke.convertToShares(mintAssets); + + uint256 snapshot = vm.snapshotState(); + + // Mint + _tokenizationMint(tokenizationSpoke, reserveInfo, user, mintShares); + uint256 userShares = tokenizationSpoke.balanceOf(user); + + // Partial redeem + if (userShares > 1) { + uint256 postMintSnapshot = vm.snapshotState(); + uint256 partialRedeem = vm.randomUint(1, userShares - 1); + _tokenizationRedeem(tokenizationSpoke, reserveInfo, user, partialRedeem); + vm.revertToState(postMintSnapshot); + } + + // Full redeem + _tokenizationRedeem(tokenizationSpoke, reserveInfo, user, userShares); + assertEq(tokenizationSpoke.balanceOf(user), 0, 'MINT_REDEEM: user should have no shares'); + vm.revertToState(snapshot); + + // Mint with sig (clean slate — no prior mint consuming addCap) + _tokenizationMintWithSig({ + tokenizationSpoke: tokenizationSpoke, + reserveInfo: reserveInfo, + privateKey: userPrivateKey, + shares: mintShares + }); + assertEq( + tokenizationSpoke.balanceOf(user), + mintShares, + 'MINT_WITH_SIG: user should have shares' + ); + } + + /// @dev Test deposit with EIP-2612 permit signature. + /// Skips if the underlying token does not support EIP-2612 (WETH). + function _testTokenizationPermitDeposit( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + uint256 maxAddAmount + ) internal { + // Skip tokens that don't support EIP-2612 permit (WETH has no nonces function) + (bool success, ) = reserveInfo.underlying.staticcall( + abi.encodeWithSignature('nonces(address)', address(this)) + ); + if (!success) { + console.log('TOKENIZATION_PERMIT: skipping %s (no EIP-2612 support)', reserveInfo.symbol); + return; + } + + uint256 depositAmount = vm.randomUint(1, maxAddAmount); + _tokenizationDepositWithPermit(tokenizationSpoke, reserveInfo, depositAmount); + } + + /// @dev Test addCap enforcement on tokenization spoke deposits. + function _testTokenizationAddCap( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo + ) internal { + IHub hub = IHub(reserveInfo.hub); + IHub.SpokeConfig memory spokeConfig = hub.getSpokeConfig( + reserveInfo.assetId, + address(tokenizationSpoke) + ); + + if (spokeConfig.addCap == 0 || spokeConfig.addCap == type(uint40).max) { + return; + } + + uint256 addCapScaled = uint256(spokeConfig.addCap) * 10 ** reserveInfo.decimals; + uint256 currentAdded = hub.getSpokeAddedAssets(reserveInfo.assetId, address(tokenizationSpoke)); + if (addCapScaled <= currentAdded) { + return; + } + + uint256 room = addCapScaled - currentAdded; + address depositor = vm.randomAddress(); + + // Deposit more than remaining room — should revert with AddCapExceeded + uint256 overflowAmount = room + 10 ** reserveInfo.decimals; + vm.startPrank(depositor); + deal2(reserveInfo.underlying, depositor, overflowAmount); + IERC20(reserveInfo.underlying).forceApprove(address(tokenizationSpoke), overflowAmount); + vm.expectRevert( + abi.encodeWithSelector(IHub.AddCapExceeded.selector, uint256(spokeConfig.addCap)) + ); + tokenizationSpoke.deposit(overflowAmount, depositor); + vm.stopPrank(); + } + + /// @dev Test that share value does not decrease over time (yield accrual). + function _testTokenizationTimeSkip( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + uint256 maxAddAmount + ) internal { + address user = vm.randomAddress(); + uint256 depositAmount = vm.randomUint(1, maxAddAmount); + + // Deposit first + _tokenizationDeposit(tokenizationSpoke, reserveInfo, user, depositAmount); + + Types.TokenizationSnapshot memory snapshotBefore = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + + uint256 skipDays = vm.randomUint(1, 365); + skip(skipDays * 1 days); + + Types.TokenizationSnapshot memory snapshotAfter = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + + // totalAssets should not decrease + assertGe( + snapshotAfter.totalAssets, + snapshotBefore.totalAssets, + 'TIME_SKIP: totalAssets decreased' + ); + + // User's asset value should not decrease (share value grows with yield) + assertGe( + snapshotAfter.userAssets, + snapshotBefore.userAssets, + 'TIME_SKIP: user asset value decreased' + ); + + // Share count should remain the same (no shares minted/burned) + assertEq( + snapshotAfter.userShares, + snapshotBefore.userShares, + 'TIME_SKIP: user share count changed' + ); + + // Hub spoke collateral should not decrease + assertGe( + snapshotAfter.spokeOnHub.collateralAssets, + snapshotBefore.spokeOnHub.collateralAssets, + 'TIME_SKIP: hub collateral assets decreased' + ); + + _assertTokenizationNoDebt(snapshotAfter); + } + + /// @dev Test that vault shares can be transferred to third parties who can then redeem. + function _testTokenizationTransferAndWithdraw( + ITokenizationSpoke tokenizationSpoke, + Types.ReserveInfo memory reserveInfo, + uint256 maxAddAmount + ) internal { + address depositor = makeAddr('TRANSFER_DEPOSITOR'); + address[3] memory recipients = [ + makeAddr('TRANSFER_RECIPIENT_0'), + makeAddr('TRANSFER_RECIPIENT_1'), + makeAddr('TRANSFER_RECIPIENT_2') + ]; + + uint256 totalSupplyBefore = tokenizationSpoke.totalSupply(); + + uint256 depositAmount = vm.randomUint(3, maxAddAmount); + _tokenizationDeposit(tokenizationSpoke, reserveInfo, depositor, depositAmount); + + uint256 totalShares = tokenizationSpoke.balanceOf(depositor); + uint256 sharePerRecipient = totalShares / 3; + require(sharePerRecipient > 0, 'TRANSFER: share per recipient is zero'); + + // Transfer shares to each recipient + vm.startPrank(depositor); + for (uint256 i; i < 3; i++) { + _logAction('TOKENIZATION_TRANSFER', reserveInfo.symbol, sharePerRecipient); + uint256 amount = i < 2 ? sharePerRecipient : tokenizationSpoke.balanceOf(depositor); + tokenizationSpoke.transfer(recipients[i], amount); + } + vm.stopPrank(); + + assertEq( + tokenizationSpoke.balanceOf(depositor), + 0, + 'TRANSFER: depositor should have 0 shares after transfers' + ); + + // Each recipient redeems all their shares + for (uint256 i; i < 3; i++) { + uint256 recipientShares = tokenizationSpoke.balanceOf(recipients[i]); + assertGt(recipientShares, 0, 'TRANSFER: recipient should have shares'); + + uint256 underlyingBefore = IERC20(reserveInfo.underlying).balanceOf(recipients[i]); + + _logAction('TOKENIZATION_REDEEM', reserveInfo.symbol, recipientShares); + vm.prank(recipients[i]); + uint256 assetsRedeemed = tokenizationSpoke.redeem( + recipientShares, + recipients[i], + recipients[i] + ); + + assertEq( + tokenizationSpoke.balanceOf(recipients[i]), + 0, + 'TRANSFER: recipient should have 0 shares after redeem' + ); + assertEq( + IERC20(reserveInfo.underlying).balanceOf(recipients[i]), + underlyingBefore + assetsRedeemed, + 'TRANSFER: recipient should have received underlying tokens' + ); + } + + assertEq( + tokenizationSpoke.totalSupply(), + totalSupplyBefore, + 'TRANSFER: totalSupply should return to pre-deposit level after all redeems' + ); + } +} diff --git a/src/dependencies/v4/Types.sol b/src/dependencies/v4/Types.sol new file mode 100644 index 00000000..3524c783 --- /dev/null +++ b/src/dependencies/v4/Types.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library Types { + /// @notice Accounting state: collateral + debt (drawn + premium), shares + assets. + struct Accounting { + // Supply / collateral side + uint256 collateralShares; + uint256 collateralAssets; + // Debt side (drawn + premium) + uint256 drawnDebt; + uint256 premiumDebt; + uint256 totalDebt; + // Drawn/premium shares + uint256 drawnShares; + uint256 premiumShares; + int256 premiumOffsetRay; + } + + /// @notice Full position snapshot at spoke-user, spoke-reserve, and hub-spoke levels. + struct PositionSnapshot { + Accounting user; + Accounting reserve; + Accounting spokeOnHub; + } + + /// @notice Tokenization spoke snapshot at user, vault, and hub-spoke levels. + struct TokenizationSnapshot { + uint256 userShares; + uint256 userAssets; + uint256 totalShares; + uint256 totalAssets; + Accounting spokeOnHub; + } + + /// @notice Per-reserve info struct used throughout V4 e2e tests. + struct ReserveInfo { + uint256 reserveId; + address underlying; + address hub; + uint16 assetId; + string symbol; + uint8 decimals; + bool paused; + bool frozen; + bool borrowable; + bool collateralEnabled; // collateralFactor > 0 + uint16 collateralFactor; // BPS + uint32 maxLiquidationBonus; // BPS + uint16 liquidationFee; // BPS + } + + struct SpokeReserveSnapshot { + address spokeAddress; + uint256 reserveId; + address underlying; + string symbol; + address hub; + uint16 assetId; + uint8 decimals; + // ReserveConfig + uint24 collateralRisk; + bool paused; + bool frozen; + bool borrowable; + bool receiveSharesEnabled; + // DynamicReserveConfig (latest key) + uint32 dynamicConfigKey; + uint16 collateralFactor; + uint32 maxLiquidationBonus; + uint16 liquidationFee; + // Oracle + address oracleAddress; + address priceSource; + uint256 oraclePrice; + } + + struct SpokeLiquidationSnapshot { + address spokeAddress; + uint128 targetHealthFactor; + uint64 healthFactorForMaxBonus; + uint16 liquidationBonusFactor; + uint16 maxUserReservesLimit; + } + + struct HubAssetSnapshot { + address hubAddress; + uint256 assetId; + address underlying; + string symbol; + uint8 decimals; + uint16 liquidityFee; + address irStrategy; + address feeReceiver; + address reinvestmentController; + // IR params + uint16 optimalUsageRatio; + uint32 baseDrawnRate; + uint32 rateGrowthBeforeOptimal; + uint32 rateGrowthAfterOptimal; + uint256 maxDrawnRate; + } + + struct SpokeCapSnapshot { + address hubAddress; + uint256 assetId; + string assetSymbol; + address spokeAddress; + uint40 addCap; + uint40 drawCap; + uint24 riskPremiumThreshold; + bool active; + bool halted; + } + + struct V4Snapshot { + SpokeReserveSnapshot[] spokeReserves; + SpokeLiquidationSnapshot[] spokeLiquidationConfigs; + HubAssetSnapshot[] hubAssets; + SpokeCapSnapshot[] spokeCaps; + } +} diff --git a/src/dependencies/v4/V4DiffWriter.sol b/src/dependencies/v4/V4DiffWriter.sol new file mode 100644 index 00000000..b73393d7 --- /dev/null +++ b/src/dependencies/v4/V4DiffWriter.sol @@ -0,0 +1,598 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Vm} from 'forge-std/Vm.sol'; +import {Types} from 'src/dependencies/v4/Types.sol'; + +/// @title V4DiffWriter +/// @notice Internal library for V4 JSON serialization and markdown diff generation. +/// Using an internal library means functions are inlined via delegatecall context, +/// keeping cheatcodes working while avoiding stack-too-deep in the inheritance chain. +library V4DiffWriter { + Vm private constant vm = Vm(address(uint160(uint256(keccak256('hevm cheat code'))))); + + function writeSnapshotJson(string memory reportName, Types.V4Snapshot memory snapshot) internal { + string memory path = string.concat('./reports/', reportName, '.json'); + vm.writeFile( + path, + '{ "spokeReserves": {}, "spokeLiquidationConfigs": {}, "hubAssets": {}, "spokeCaps": {}, "raw": {} }' + ); + vm.serializeUint('root', 'chainId', block.chainid); + + _writeSpokeReserves(path, snapshot.spokeReserves); + _writeSpokeLiqConfigs(path, snapshot.spokeLiquidationConfigs); + _writeHubAssets(path, snapshot.hubAssets); + _writeSpokeCaps(path, snapshot.spokeCaps); + } + + function _writeSpokeReserves( + string memory path, + Types.SpokeReserveSnapshot[] memory reserves + ) private { + string memory sectionKey = 'spokeReserves'; + string memory content = '{}'; + vm.serializeJson(sectionKey, '{}'); + + for (uint256 i; i < reserves.length; i++) { + string memory obj = _serReserve(reserves[i]); + + string memory spokeKey = string.concat('spoke_', vm.toString(reserves[i].spokeAddress)); + if (reserves[i].reserveId == 0) vm.serializeJson(spokeKey, '{}'); + string memory spokeObj = vm.serializeString( + spokeKey, + vm.toString(reserves[i].reserveId), + obj + ); + + if (_isLastForSpoke(reserves, i)) { + content = vm.serializeString(sectionKey, vm.toString(reserves[i].spokeAddress), spokeObj); + } + } + vm.writeJson(vm.serializeString('root', 'spokeReserves', content), path); + } + + function _isLastForSpoke( + Types.SpokeReserveSnapshot[] memory arr, + uint256 idx + ) private pure returns (bool) { + for (uint256 j = idx + 1; j < arr.length; j++) { + if (arr[j].spokeAddress == arr[idx].spokeAddress) return false; + } + return true; + } + + function _serReserve(Types.SpokeReserveSnapshot memory r) private returns (string memory) { + string memory k = string.concat(vm.toString(r.spokeAddress), '_', vm.toString(r.reserveId)); + vm.serializeJson(k, '{}'); + vm.serializeString(k, 'symbol', r.symbol); + vm.serializeAddress(k, 'underlying', r.underlying); + vm.serializeAddress(k, 'hub', r.hub); + vm.serializeUint(k, 'assetId', r.assetId); + vm.serializeUint(k, 'decimals', r.decimals); + vm.serializeUint(k, 'collateralRisk', r.collateralRisk); + vm.serializeBool(k, 'paused', r.paused); + vm.serializeBool(k, 'frozen', r.frozen); + vm.serializeBool(k, 'borrowable', r.borrowable); + vm.serializeBool(k, 'receiveSharesEnabled', r.receiveSharesEnabled); + vm.serializeUint(k, 'dynamicConfigKey', r.dynamicConfigKey); + vm.serializeUint(k, 'collateralFactor', r.collateralFactor); + vm.serializeUint(k, 'maxLiquidationBonus', r.maxLiquidationBonus); + vm.serializeUint(k, 'liquidationFee', r.liquidationFee); + vm.serializeAddress(k, 'oracleAddress', r.oracleAddress); + vm.serializeAddress(k, 'priceSource', r.priceSource); + return vm.serializeString(k, 'oraclePrice', vm.toString(r.oraclePrice)); + } + + function _writeSpokeLiqConfigs( + string memory path, + Types.SpokeLiquidationSnapshot[] memory configs + ) private { + string memory sectionKey = 'spokeLiqConfigs'; + string memory content = '{}'; + vm.serializeJson(sectionKey, '{}'); + + for (uint256 i; i < configs.length; i++) { + string memory k = string.concat('liq_', vm.toString(configs[i].spokeAddress)); + vm.serializeJson(k, '{}'); + vm.serializeString(k, 'targetHealthFactor', vm.toString(configs[i].targetHealthFactor)); + vm.serializeString( + k, + 'healthFactorForMaxBonus', + vm.toString(configs[i].healthFactorForMaxBonus) + ); + vm.serializeUint(k, 'liquidationBonusFactor', configs[i].liquidationBonusFactor); + string memory obj = vm.serializeUint( + k, + 'maxUserReservesLimit', + configs[i].maxUserReservesLimit + ); + content = vm.serializeString(sectionKey, vm.toString(configs[i].spokeAddress), obj); + } + vm.writeJson(vm.serializeString('root', 'spokeLiquidationConfigs', content), path); + } + + function _writeHubAssets(string memory path, Types.HubAssetSnapshot[] memory assets) private { + string memory sectionKey = 'hubAssets'; + string memory content = '{}'; + vm.serializeJson(sectionKey, '{}'); + + for (uint256 i; i < assets.length; i++) { + string memory obj = _serHubAsset(assets[i]); + + string memory hubKey = string.concat('hub_', vm.toString(assets[i].hubAddress)); + if (assets[i].assetId == 0) vm.serializeJson(hubKey, '{}'); + string memory hubObj = vm.serializeString(hubKey, vm.toString(assets[i].assetId), obj); + + if (_isLastForHub(assets, i)) { + content = vm.serializeString(sectionKey, vm.toString(assets[i].hubAddress), hubObj); + } + } + vm.writeJson(vm.serializeString('root', 'hubAssets', content), path); + } + + function _isLastForHub( + Types.HubAssetSnapshot[] memory arr, + uint256 idx + ) private pure returns (bool) { + for (uint256 j = idx + 1; j < arr.length; j++) { + if (arr[j].hubAddress == arr[idx].hubAddress) return false; + } + return true; + } + + function _serHubAsset(Types.HubAssetSnapshot memory a) private returns (string memory) { + string memory k = string.concat(vm.toString(a.hubAddress), '_', vm.toString(a.assetId)); + vm.serializeJson(k, '{}'); + vm.serializeString(k, 'symbol', a.symbol); + vm.serializeAddress(k, 'underlying', a.underlying); + vm.serializeUint(k, 'decimals', a.decimals); + vm.serializeUint(k, 'liquidityFee', a.liquidityFee); + vm.serializeAddress(k, 'irStrategy', a.irStrategy); + vm.serializeAddress(k, 'feeReceiver', a.feeReceiver); + vm.serializeAddress(k, 'reinvestmentController', a.reinvestmentController); + vm.serializeUint(k, 'optimalUsageRatio', a.optimalUsageRatio); + vm.serializeUint(k, 'baseDrawnRate', a.baseDrawnRate); + vm.serializeUint(k, 'rateGrowthBeforeOptimal', a.rateGrowthBeforeOptimal); + vm.serializeUint(k, 'rateGrowthAfterOptimal', a.rateGrowthAfterOptimal); + return vm.serializeString(k, 'maxDrawnRate', vm.toString(a.maxDrawnRate)); + } + + function _writeSpokeCaps(string memory path, Types.SpokeCapSnapshot[] memory caps) private { + string memory sectionKey = 'spokeCaps'; + string memory content = '{}'; + vm.serializeJson(sectionKey, '{}'); + + for (uint256 i; i < caps.length; i++) { + string memory k = string.concat( + vm.toString(caps[i].hubAddress), + '_', + vm.toString(caps[i].assetId), + '_', + vm.toString(caps[i].spokeAddress) + ); + vm.serializeJson(k, '{}'); + vm.serializeString(k, 'assetSymbol', caps[i].assetSymbol); + vm.serializeString(k, 'addCap', vm.toString(uint256(caps[i].addCap))); + vm.serializeString(k, 'drawCap', vm.toString(uint256(caps[i].drawCap))); + vm.serializeUint(k, 'riskPremiumThreshold', caps[i].riskPremiumThreshold); + vm.serializeBool(k, 'active', caps[i].active); + string memory obj = vm.serializeBool(k, 'halted', caps[i].halted); + content = vm.serializeString(sectionKey, k, obj); + } + vm.writeJson(vm.serializeString('root', 'spokeCaps', content), path); + } + + function writeDiff( + string memory reportName, + Types.V4Snapshot memory snapBefore, + Types.V4Snapshot memory snapAfter + ) internal { + string memory md = ''; + md = string.concat(md, _diffSpokeReserves(snapBefore.spokeReserves, snapAfter.spokeReserves)); + md = string.concat(md, _diffHubAssets(snapBefore.hubAssets, snapAfter.hubAssets)); + md = string.concat(md, _diffSpokeCaps(snapBefore.spokeCaps, snapAfter.spokeCaps)); + md = string.concat( + md, + _diffSpokeLiq(snapBefore.spokeLiquidationConfigs, snapAfter.spokeLiquidationConfigs) + ); + + if (bytes(md).length == 0) md = 'No configuration changes detected.\n'; + + vm.writeFile(string.concat('./diffs/', reportName, '_before_', reportName, '_after.md'), md); + } + + function _diffSpokeReserves( + Types.SpokeReserveSnapshot[] memory arrB, + Types.SpokeReserveSnapshot[] memory arrA + ) private pure returns (string memory section) { + string memory body = ''; + for (uint256 i; i < arrA.length; i++) { + (bool found, uint256 bi) = _findRes(arrB, arrA[i].spokeAddress, arrA[i].reserveId); + if (found) { + string memory rows = _cmpRes(arrB[bi], arrA[i]); + if (bytes(rows).length > 0) { + body = string.concat(body, _resHdr(arrA[i]), _header(), rows, '\n'); + } + } else { + body = string.concat(body, _newRes(arrA[i])); + } + } + for (uint256 i; i < arrB.length; i++) { + (bool f, ) = _findRes(arrA, arrB[i].spokeAddress, arrB[i].reserveId); + if (!f) body = string.concat(body, _resHdr(arrB[i]), '**REMOVED**\n\n'); + } + if (bytes(body).length > 0) section = string.concat('## Spoke Reserve Changes\n\n', body); + } + + function _resHdr(Types.SpokeReserveSnapshot memory r) private pure returns (string memory) { + return + string.concat( + '### ', + r.symbol, + ' (', + vm.toString(r.underlying), + ') on Spoke ', + vm.toString(r.spokeAddress), + ' [reserveId: ', + vm.toString(r.reserveId), + ']\n\n' + ); + } + + function _cmpRes( + Types.SpokeReserveSnapshot memory b, + Types.SpokeReserveSnapshot memory a + ) private pure returns (string memory rows) { + rows = string.concat( + _dU('collateralRisk', b.collateralRisk, a.collateralRisk), + _dB('paused', b.paused, a.paused), + _dB('frozen', b.frozen, a.frozen), + _dB('borrowable', b.borrowable, a.borrowable), + _dB('receiveSharesEnabled', b.receiveSharesEnabled, a.receiveSharesEnabled), + _dU('dynamicConfigKey', b.dynamicConfigKey, a.dynamicConfigKey) + ); + rows = string.concat( + rows, + _dP('collateralFactor', b.collateralFactor, a.collateralFactor), + _dU('maxLiquidationBonus', b.maxLiquidationBonus, a.maxLiquidationBonus), + _dP('liquidationFee', b.liquidationFee, a.liquidationFee), + _dA('priceSource', b.priceSource, a.priceSource), + _dU('oraclePrice', b.oraclePrice, a.oraclePrice) + ); + } + + function _newRes(Types.SpokeReserveSnapshot memory r) private pure returns (string memory) { + string memory p1 = string.concat( + _resHdr(r), + '**NEW RESERVE**\n\n', + _header(), + _row('collateralRisk', vm.toString(uint256(r.collateralRisk))), + _row('paused', _bs(r.paused)), + _row('frozen', _bs(r.frozen)), + _row('borrowable', _bs(r.borrowable)), + _row('receiveSharesEnabled', _bs(r.receiveSharesEnabled)) + ); + return + string.concat( + p1, + _row('collateralFactor', _ps(r.collateralFactor)), + _row('maxLiquidationBonus', vm.toString(uint256(r.maxLiquidationBonus))), + _row('liquidationFee', _ps(r.liquidationFee)), + _row('priceSource', vm.toString(r.priceSource)), + _row('oraclePrice', vm.toString(r.oraclePrice)), + '\n' + ); + } + + function _findRes( + Types.SpokeReserveSnapshot[] memory a, + address s, + uint256 id + ) private pure returns (bool, uint256) { + for (uint256 i; i < a.length; i++) { + if (a[i].spokeAddress == s && a[i].reserveId == id) return (true, i); + } + return (false, 0); + } + + function _diffHubAssets( + Types.HubAssetSnapshot[] memory arrB, + Types.HubAssetSnapshot[] memory arrA + ) private pure returns (string memory section) { + string memory body = ''; + for (uint256 i; i < arrA.length; i++) { + (bool found, uint256 bi) = _findHA(arrB, arrA[i].hubAddress, arrA[i].assetId); + if (found) { + string memory rows = _cmpHA(arrB[bi], arrA[i]); + if (bytes(rows).length > 0) { + body = string.concat(body, _haHdr(arrA[i]), _header(), rows, '\n'); + } + } else { + body = string.concat(body, _newHA(arrA[i])); + } + } + for (uint256 i; i < arrB.length; i++) { + (bool f, ) = _findHA(arrA, arrB[i].hubAddress, arrB[i].assetId); + if (!f) body = string.concat(body, _haHdr(arrB[i]), '**REMOVED**\n\n'); + } + if (bytes(body).length > 0) section = string.concat('## Hub Asset Changes\n\n', body); + } + + function _haHdr(Types.HubAssetSnapshot memory a) private pure returns (string memory) { + return + string.concat( + '### ', + a.symbol, + ' (assetId: ', + vm.toString(a.assetId), + ') on Hub ', + vm.toString(a.hubAddress), + '\n\n' + ); + } + + function _cmpHA( + Types.HubAssetSnapshot memory b, + Types.HubAssetSnapshot memory a + ) private pure returns (string memory rows) { + rows = string.concat( + _dP('liquidityFee', b.liquidityFee, a.liquidityFee), + _dA('irStrategy', b.irStrategy, a.irStrategy), + _dA('feeReceiver', b.feeReceiver, a.feeReceiver), + _dA('reinvestmentController', b.reinvestmentController, a.reinvestmentController), + _dP('optimalUsageRatio', b.optimalUsageRatio, a.optimalUsageRatio) + ); + rows = string.concat( + rows, + _dP('baseDrawnRate', uint256(b.baseDrawnRate), uint256(a.baseDrawnRate)), + _dP( + 'rateGrowthBeforeOptimal', + uint256(b.rateGrowthBeforeOptimal), + uint256(a.rateGrowthBeforeOptimal) + ), + _dP( + 'rateGrowthAfterOptimal', + uint256(b.rateGrowthAfterOptimal), + uint256(a.rateGrowthAfterOptimal) + ), + _dP('maxDrawnRate', b.maxDrawnRate, a.maxDrawnRate) + ); + } + + function _newHA(Types.HubAssetSnapshot memory a) private pure returns (string memory) { + string memory p1 = string.concat( + _haHdr(a), + '**NEW ASSET**\n\n', + _header(), + _row('liquidityFee', _ps(a.liquidityFee)), + _row('irStrategy', vm.toString(a.irStrategy)), + _row('feeReceiver', vm.toString(a.feeReceiver)), + _row('reinvestmentController', vm.toString(a.reinvestmentController)) + ); + return + string.concat( + p1, + _row('optimalUsageRatio', _ps(a.optimalUsageRatio)), + _row('baseDrawnRate', _ps(uint256(a.baseDrawnRate))), + _row('rateGrowthBeforeOptimal', _ps(uint256(a.rateGrowthBeforeOptimal))), + _row('rateGrowthAfterOptimal', _ps(uint256(a.rateGrowthAfterOptimal))), + _row('maxDrawnRate', _ps(a.maxDrawnRate)), + '\n' + ); + } + + function _findHA( + Types.HubAssetSnapshot[] memory a, + address h, + uint256 id + ) private pure returns (bool, uint256) { + for (uint256 i; i < a.length; i++) { + if (a[i].hubAddress == h && a[i].assetId == id) return (true, i); + } + return (false, 0); + } + + // --- hub spoke caps diff --- + + function _diffSpokeCaps( + Types.SpokeCapSnapshot[] memory arrB, + Types.SpokeCapSnapshot[] memory arrA + ) private pure returns (string memory section) { + string memory body = ''; + for (uint256 i; i < arrA.length; i++) { + (bool found, uint256 bi) = _findSC( + arrB, + arrA[i].hubAddress, + arrA[i].assetId, + arrA[i].spokeAddress + ); + if (found) { + string memory rows = _cmpSC(arrB[bi], arrA[i]); + if (bytes(rows).length > 0) { + body = string.concat(body, _scHdr(arrA[i]), _header(), rows, '\n'); + } + } else { + body = string.concat(body, _newSC(arrA[i])); + } + } + for (uint256 i; i < arrB.length; i++) { + (bool f, ) = _findSC(arrA, arrB[i].hubAddress, arrB[i].assetId, arrB[i].spokeAddress); + if (!f) body = string.concat(body, _scHdr(arrB[i]), '**REMOVED**\n\n'); + } + if (bytes(body).length > 0) section = string.concat('## Hub Spoke Cap Changes\n\n', body); + } + + function _scHdr(Types.SpokeCapSnapshot memory c) private pure returns (string memory) { + return + string.concat( + '### ', + c.assetSymbol, + ' (assetId: ', + vm.toString(c.assetId), + ') on Hub ', + vm.toString(c.hubAddress), + ' / Spoke ', + vm.toString(c.spokeAddress), + '\n\n' + ); + } + + function _cmpSC( + Types.SpokeCapSnapshot memory b, + Types.SpokeCapSnapshot memory a + ) private pure returns (string memory) { + return + string.concat( + _dU('addCap', uint256(b.addCap), uint256(a.addCap)), + _dU('drawCap', uint256(b.drawCap), uint256(a.drawCap)), + _dU( + 'riskPremiumThreshold', + uint256(b.riskPremiumThreshold), + uint256(a.riskPremiumThreshold) + ), + _dB('active', b.active, a.active), + _dB('halted', b.halted, a.halted) + ); + } + + function _newSC(Types.SpokeCapSnapshot memory c) private pure returns (string memory) { + return + string.concat( + _scHdr(c), + '**NEW SPOKE**\n\n', + _header(), + _row('addCap', vm.toString(uint256(c.addCap))), + _row('drawCap', vm.toString(uint256(c.drawCap))), + _row('riskPremiumThreshold', vm.toString(uint256(c.riskPremiumThreshold))), + _row('active', _bs(c.active)), + _row('halted', _bs(c.halted)), + '\n' + ); + } + + function _findSC( + Types.SpokeCapSnapshot[] memory a, + address h, + uint256 id, + address s + ) private pure returns (bool, uint256) { + for (uint256 i; i < a.length; i++) { + if (a[i].hubAddress == h && a[i].assetId == id && a[i].spokeAddress == s) return (true, i); + } + return (false, 0); + } + + function _diffSpokeLiq( + Types.SpokeLiquidationSnapshot[] memory arrB, + Types.SpokeLiquidationSnapshot[] memory arrA + ) private pure returns (string memory section) { + string memory body = ''; + for (uint256 i; i < arrA.length; i++) { + (bool found, uint256 bi) = _findSL(arrB, arrA[i].spokeAddress); + if (found) { + string memory rows = _cmpSL(arrB[bi], arrA[i]); + if (bytes(rows).length > 0) { + body = string.concat( + body, + '### Spoke ', + vm.toString(arrA[i].spokeAddress), + '\n\n', + _header(), + rows, + '\n' + ); + } + } else { + body = string.concat( + body, + '### Spoke ', + vm.toString(arrA[i].spokeAddress), + ' **NEW**\n\n', + _header(), + _row('targetHealthFactor', vm.toString(uint256(arrA[i].targetHealthFactor))), + _row('healthFactorForMaxBonus', vm.toString(uint256(arrA[i].healthFactorForMaxBonus))), + _row('liquidationBonusFactor', vm.toString(uint256(arrA[i].liquidationBonusFactor))), + _row('maxUserReservesLimit', vm.toString(uint256(arrA[i].maxUserReservesLimit))), + '\n' + ); + } + } + if (bytes(body).length > 0) { + section = string.concat('## Spoke Liquidation Config Changes\n\n', body); + } + } + + function _cmpSL( + Types.SpokeLiquidationSnapshot memory b, + Types.SpokeLiquidationSnapshot memory a + ) private pure returns (string memory) { + return + string.concat( + _dU('targetHealthFactor', uint256(b.targetHealthFactor), uint256(a.targetHealthFactor)), + _dU( + 'healthFactorForMaxBonus', + uint256(b.healthFactorForMaxBonus), + uint256(a.healthFactorForMaxBonus) + ), + _dU( + 'liquidationBonusFactor', + uint256(b.liquidationBonusFactor), + uint256(a.liquidationBonusFactor) + ), + _dU( + 'maxUserReservesLimit', + uint256(b.maxUserReservesLimit), + uint256(a.maxUserReservesLimit) + ) + ); + } + + function _findSL( + Types.SpokeLiquidationSnapshot[] memory a, + address s + ) private pure returns (bool, uint256) { + for (uint256 i; i < a.length; i++) { + if (a[i].spokeAddress == s) return (true, i); + } + return (false, 0); + } + + function _header() private pure returns (string memory) { + return '| description | value before | value after |\n| --- | --- | --- |\n'; + } + + function _row(string memory n, string memory v) private pure returns (string memory) { + return string.concat('| ', n, ' | - | ', v, ' |\n'); + } + + function _dU(string memory n, uint256 b, uint256 a) private pure returns (string memory) { + if (b == a) return ''; + return string.concat('| ', n, ' | ', vm.toString(b), ' | ', vm.toString(a), ' |\n'); + } + + function _dP(string memory n, uint256 b, uint256 a) private pure returns (string memory) { + if (b == a) return ''; + return string.concat('| ', n, ' | ', _ps(b), ' | ', _ps(a), ' |\n'); + } + + function _dB(string memory n, bool b, bool a) private pure returns (string memory) { + if (b == a) return ''; + return string.concat('| ', n, ' | ', _bs(b), ' | ', _bs(a), ' |\n'); + } + + function _dA(string memory n, address b, address a) private pure returns (string memory) { + if (b == a) return ''; + return string.concat('| ', n, ' | ', vm.toString(b), ' | ', vm.toString(a), ' |\n'); + } + + function _bs(bool v) private pure returns (string memory) { + return v ? 'true' : 'false'; + } + + function _ps(uint256 bps) private pure returns (string memory) { + uint256 w = bps / 100; + uint256 f = bps % 100; + string memory fs = f < 10 ? string.concat('0', vm.toString(f)) : vm.toString(f); + return string.concat(vm.toString(w), '.', fs, ' % [', vm.toString(bps), ']'); + } +} diff --git a/src/v4-config-engine/AaveV4PayloadEthereum.sol b/src/v4-config-engine/AaveV4PayloadEthereum.sol new file mode 100644 index 00000000..cb1aec7e --- /dev/null +++ b/src/v4-config-engine/AaveV4PayloadEthereum.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AaveV4Ethereum} from 'aave-address-book/AaveV4Ethereum.sol'; +import {AaveV4Payload} from 'aave-v4/config-engine/AaveV4Payload.sol'; + +/** + * @dev Base smart contract for an Aave V4 governance payload on Ethereum. + * @author Aave Labs + */ +abstract contract AaveV4PayloadEthereum is AaveV4Payload(AaveV4Ethereum.CONFIG_ENGINE) {} diff --git a/tests/ProtocolV4TestBase.t.sol b/tests/ProtocolV4TestBase.t.sol new file mode 100644 index 00000000..9caa2d44 --- /dev/null +++ b/tests/ProtocolV4TestBase.t.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {ProtocolV4TestBase} from '../src/ProtocolV4TestBase.sol'; +import {ISpoke, ITokenizationSpoke, ISpokeConfigurator} from 'aave-address-book/AaveV4.sol'; +import {AaveV4Ethereum, AaveV4EthereumSpokes, AaveV4EthereumHubs, AaveV4EthereumTokenizationSpokes, AaveV4EthereumPositionManagers} from 'aave-address-book/AaveV4Ethereum.sol'; +import {AaveV4EthereumHubHelpers, AaveV4EthereumSpokeHelpers, AaveV4EthereumTokenizationSpokeHelpers} from 'src/dependencies/v4/AaveV4EthereumHelpers.sol'; +import {Types} from 'src/dependencies/v4/Types.sol'; +import {PayloadWithEmit} from './mocks/PayloadWithEmit.sol'; +import {PayloadWithStorage} from './mocks/PayloadWithStorage.sol'; + +contract ProtocolV4TestBaseTest is ProtocolV4TestBase { + uint256 public constant BLOCK_NUMBER = 24829000; + + function setUp() public { + vm.createSelectFork('mainnet', BLOCK_NUMBER); + } + + modifier gasless() { + vm.pauseGasMetering(); + _; + vm.resumeGasMetering(); + } + + function _mockAccessManager() internal { + vm.mockCall( + address(AaveV4Ethereum.ACCESS_MANAGER), + abi.encodeWithSelector(bytes4(keccak256('canCall(address,address,bytes4)'))), + abi.encode(true, uint32(0)) + ); + } + + function _updatePaused(address spoke, uint256 reserveId, bool paused) internal { + _mockAccessManager(); + AaveV4Ethereum.SPOKE_CONFIGURATOR.updatePaused({ + spoke: spoke, + reserveId: reserveId, + paused: paused + }); + vm.clearMockedCalls(); + } + + function _updateFrozen(address spoke, uint256 reserveId, bool frozen) internal { + _mockAccessManager(); + AaveV4Ethereum.SPOKE_CONFIGURATOR.updateFrozen({ + spoke: spoke, + reserveId: reserveId, + frozen: frozen + }); + vm.clearMockedCalls(); + } + + function _cleanupArtifacts(string memory reportName) internal { + string memory beforePath = string.concat('./reports/', reportName, '_before.json'); + string memory afterPath = string.concat('./reports/', reportName, '_after.json'); + string memory diffPath = string.concat( + './diffs/', + reportName, + '_before_', + reportName, + '_after.md' + ); + if (vm.exists(beforePath)) { + vm.removeFile(beforePath); + } + if (vm.exists(afterPath)) { + vm.removeFile(afterPath); + } + if (vm.exists(diffPath)) { + vm.removeFile(diffPath); + } + } +} + +contract ProtocolV4TestE2ESingleSpoke is ProtocolV4TestBaseTest { + function test_e2eMainSpoke() public gasless { + e2eTestSpoke({spoke: AaveV4EthereumSpokes.MAIN_SPOKE}); + } +} + +contract ProtocolV4TestE2EDistinctSpokes is ProtocolV4TestBaseTest { + function test_e2eBluechipSpoke() public gasless { + e2eTestSpoke({spoke: AaveV4EthereumSpokes.BLUECHIP_SPOKE}); + } + + function test_e2eEthenaCorrelatedSpoke() public gasless { + e2eTestSpoke({spoke: AaveV4EthereumSpokes.ETHENA_CORRELATED_SPOKE}); + } + + function test_e2eLombardBtcSpoke() public gasless { + e2eTestSpoke({spoke: AaveV4EthereumSpokes.LOMBARD_BTC_SPOKE}); + } +} + +contract ProtocolV4TestE2EAllSpokes is ProtocolV4TestBaseTest { + function test_e2eAllSpokes() public gasless { + e2eTestAllSpokes({ + spokes: AaveV4EthereumSpokeHelpers.getUserSpokes(), + testPositionManagers: true + }); + } +} + +contract ProtocolV4TestE2ETokenizationSpokes is ProtocolV4TestBaseTest { + function test_e2eSingleTokenizationSpoke() public gasless { + e2eTestTokenizationSpoke(AaveV4EthereumTokenizationSpokes.CORE_WETH_TOKENIZATION_SPOKE); + } + + function test_e2eAllTokenizationSpokes() public gasless { + e2eTestAllTokenizationSpokes({ + tokenizationSpokes: AaveV4EthereumTokenizationSpokeHelpers.getTokenizationSpokes() + }); + } +} + +contract ProtocolV4TestPositionManagers is ProtocolV4TestBaseTest { + function test_e2eGatewaysMainSpoke() public gasless { + e2eTestGateways({spoke: AaveV4EthereumSpokes.MAIN_SPOKE}); + } + + function test_e2eRegularPositionManagers() public gasless { + e2eTestRegularPositionManagers({spoke: AaveV4EthereumSpokes.MAIN_SPOKE}); + } + + function test_e2ePositionManagersBluechip() public gasless { + e2eTestPositionManagers({spoke: AaveV4EthereumSpokes.BLUECHIP_SPOKE}); + } +} + +contract ProtocolV4TestPausedFrozenAssets is ProtocolV4TestBaseTest { + function test_pausedAssetReverts() public gasless { + ISpoke spoke = AaveV4EthereumSpokes.MAIN_SPOKE; + Types.ReserveInfo[] memory reserves = _getReserveInfo(spoke); + require(reserves.length > 0, 'No reserves found'); + + // Find first non-paused reserve + uint256 targetIdx; + bool found; + for (uint256 i; i < reserves.length; i++) { + if (!reserves[i].paused) { + targetIdx = i; + found = true; + break; + } + } + require(found, 'No non-paused reserve found'); + + _updatePaused({spoke: address(spoke), reserveId: reserves[targetIdx].reserveId, paused: true}); + + // Update the reserve info to reflect paused state + reserves[targetIdx].paused = true; + + e2eTestPausedAsset({spoke: spoke, pausedAsset: reserves[targetIdx]}); + } + + function test_frozenAssetReverts() public gasless { + ISpoke spoke = AaveV4EthereumSpokes.MAIN_SPOKE; + Types.ReserveInfo[] memory reserves = _getReserveInfo(spoke); + require(reserves.length > 0, 'No reserves found'); + + // Find first non-frozen, non-paused reserve + uint256 targetIdx; + bool found; + for (uint256 i; i < reserves.length; i++) { + if (!reserves[i].frozen && !reserves[i].paused) { + targetIdx = i; + found = true; + break; + } + } + require(found, 'No non-frozen reserve found'); + + _updateFrozen({spoke: address(spoke), reserveId: reserves[targetIdx].reserveId, frozen: true}); + + // Update the reserve info to reflect frozen state + reserves[targetIdx].frozen = true; + + e2eTestFrozenAsset({spoke: spoke, frozenAsset: reserves[targetIdx]}); + } +} + +contract ProtocolV4TestSnapshot is ProtocolV4TestBaseTest { + function test_snapshotState() public { + string memory name = 'v4_snapshot'; + Types.V4Snapshot memory snapshot = createV4Snapshot({ + spokes: AaveV4EthereumSpokeHelpers.getUserSpokes(), + hubs: AaveV4EthereumHubHelpers.getHubs() + }); + writeV4SnapshotJson({name: name, snap: snapshot}); + vm.removeFile(string.concat('./reports/', name, '.json')); + } +} + +contract ProtocolV4TestDefaultTest is ProtocolV4TestBaseTest { + function test_defaultTestWithPayload() public { + string memory name = 'v4_emit_payload'; + defaultTest({ + reportName: name, + spokes: AaveV4EthereumSpokeHelpers.getUserSpokes(), + tokenizationSpokes: AaveV4EthereumTokenizationSpokeHelpers.getTokenizationSpokes(), + payload: address(new PayloadWithEmit()) + }); + _cleanupArtifacts(name); + } + + function test_defaultTestNoE2E() public { + string memory name = 'v4_no_e2e'; + defaultTest({ + reportName: name, + spokes: AaveV4EthereumSpokeHelpers.getUserSpokes(), + tokenizationSpokes: AaveV4EthereumTokenizationSpokeHelpers.getTokenizationSpokes(), + payload: address(new PayloadWithEmit()), + runE2E: false, + testPositionManagers: false + }); + _cleanupArtifacts(name); + } +} + +contract ProtocolV4TestStorageValidation is ProtocolV4TestBaseTest { + function test_noExecutorStorageChange_passes() public { + string memory name = 'v4_storage_pass'; + defaultTest({ + reportName: name, + spokes: AaveV4EthereumSpokeHelpers.getUserSpokes(), + tokenizationSpokes: AaveV4EthereumTokenizationSpokeHelpers.getTokenizationSpokes(), + payload: address(new PayloadWithEmit()), + runE2E: false, + testPositionManagers: false + }); + _cleanupArtifacts(name); + } + + function test_executorStorageChange_reverts() public { + string memory name = 'v4_storage_fail'; + address payload = address(new PayloadWithStorage()); + vm.expectRevert(); + this.defaultTest({ + reportName: name, + spokes: AaveV4EthereumSpokeHelpers.getUserSpokes(), + tokenizationSpokes: AaveV4EthereumTokenizationSpokeHelpers.getTokenizationSpokes(), + payload: payload, + runE2E: false, + testPositionManagers: false + }); + // filesystem writes persist through EVM reverts, so clean up manually + _cleanupArtifacts(name); + } +} From 54456c061c654f24694fed10f9cf24477000a75f Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:11:30 -0500 Subject: [PATCH 02/29] fix: pr comments --- src/dependencies/v4/Actions.sol | 63 ++++++++------ src/dependencies/v4/GatewayScenarios.sol | 102 +++++++++++------------ src/dependencies/v4/Helpers.sol | 32 +++---- src/dependencies/v4/Scenarios.sol | 4 +- src/dependencies/v4/SnapshotV4.sol | 1 - 5 files changed, 103 insertions(+), 99 deletions(-) diff --git a/src/dependencies/v4/Actions.sol b/src/dependencies/v4/Actions.sol index 182a8d95..55f41d0a 100644 --- a/src/dependencies/v4/Actions.sol +++ b/src/dependencies/v4/Actions.sol @@ -2,17 +2,18 @@ pragma solidity ^0.8.0; import 'forge-std/Test.sol'; -import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; -import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; +import {SafeERC20, IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; import {CommonTestBase} from 'src/CommonTestBase.sol'; import {ISpoke} from 'aave-address-book/AaveV4.sol'; import {IHubBase} from 'aave-v4/hub/interfaces/IHubBase.sol'; +import {WadRayMath} from 'aave-v4/libraries/math/WadRayMath.sol'; import {Types} from 'src/dependencies/v4/Types.sol'; /// @title Actions /// @notice Low-level spoke actions with hub and spoke accounting assertions. abstract contract Actions is CommonTestBase { using SafeERC20 for IERC20; + using stdMath for uint256; uint256 constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; uint256 constant MAX_DEAL_UNIT = 1e12; // whole units not accounting for token decimals @@ -150,7 +151,11 @@ abstract contract Actions is CommonTestBase { // Hub drawn index should have grown IHubBase hub = IHubBase(reserveInfo.hub); uint256 drawnIndexAfter = hub.getAssetDrawnIndex(reserveInfo.assetId); - assertGt(drawnIndexAfter, 1e27, 'TIME_SKIP: drawn index should be greater than 1e27'); + assertGt( + drawnIndexAfter, + WadRayMath.RAY, + 'TIME_SKIP: drawn index should be greater than default 1 RAY' + ); vm.revertToState(snapshot); } @@ -166,16 +171,15 @@ abstract contract Actions is CommonTestBase { Types.PositionSnapshot memory snapshotBefore = _getPositionSnapshot(spoke, reserveInfo, user); - vm.startPrank(user); - deal2(reserveInfo.underlying, user, amount); - IERC20(reserveInfo.underlying).forceApprove(address(spoke), amount); + _forceApprove({spoke: spoke, underlying: reserveInfo.underlying, user: user, amount: amount}); + _logAction('SUPPLY', reserveInfo.symbol, amount); + vm.prank(user); (uint256 returnedShares, uint256 returnedAssets) = spoke.supply({ reserveId: reserveInfo.reserveId, amount: amount, onBehalfOf: user }); - vm.stopPrank(); Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, reserveInfo, user); @@ -212,6 +216,13 @@ abstract contract Actions is CommonTestBase { ); } + /// @notice Deal to the user and force approve the spoke to spend the amount of the underlying token for the user + function _forceApprove(ISpoke spoke, address underlying, address user, uint256 amount) internal { + deal2(underlying, user, amount); + vm.prank(user); + IERC20(underlying).forceApprove(address(spoke), amount); + } + function _withdraw( ISpoke spoke, Types.ReserveInfo memory reserveInfo, @@ -323,26 +334,28 @@ abstract contract Actions is CommonTestBase { uint256 effectiveRepayAmount = amount >= snapshotBefore.user.totalDebt ? snapshotBefore.user.totalDebt : amount; - uint256 drawnRepayAmount = effectiveRepayAmount > snapshotBefore.user.drawnDebt - ? snapshotBefore.user.drawnDebt - : effectiveRepayAmount; + uint256 drawnRepayAmount = effectiveRepayAmount > snapshotBefore.user.premiumDebt + ? effectiveRepayAmount - snapshotBefore.user.premiumDebt + : 0; uint256 expectedRestoredShares = IHubBase(reserveInfo.hub).previewRestoreByAssets( reserveInfo.assetId, drawnRepayAmount ); - vm.startPrank(user); - // deal enough to cover full repay, capped to avoid overflow - uint256 maxDeal = _maxDealAmount(reserveInfo.decimals); - deal2(reserveInfo.underlying, user, maxDeal); - IERC20(reserveInfo.underlying).forceApprove(address(spoke), maxDeal); + _forceApprove({ + spoke: spoke, + underlying: reserveInfo.underlying, + user: user, + amount: effectiveRepayAmount + }); + _logAction('REPAY', reserveInfo.symbol, amount); + vm.prank(user); (uint256 returnedShares, uint256 returnedAssets) = spoke.repay({ reserveId: reserveInfo.reserveId, amount: amount, onBehalfOf: user }); - vm.stopPrank(); Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, reserveInfo, user); @@ -353,7 +366,7 @@ abstract contract Actions is CommonTestBase { assertEq(snapshotAfter.user.totalDebt, 0, 'REPAY: user debt should be zero'); } else { assertApproxEqAbs( - stdMath.delta(snapshotAfter.user.totalDebt, snapshotBefore.user.totalDebt), + snapshotAfter.user.totalDebt.delta(snapshotBefore.user.totalDebt), amount, 2, 'REPAY: user debt mismatch' @@ -361,13 +374,13 @@ abstract contract Actions is CommonTestBase { } // Hub spoke - up to 2 wei diff due to premium/drawn debt assertApproxEqAbs( - stdMath.delta(snapshotBefore.spokeOnHub.totalDebt, snapshotAfter.spokeOnHub.totalDebt), + snapshotBefore.spokeOnHub.totalDebt.delta(snapshotAfter.spokeOnHub.totalDebt), effectiveRepayAmount, 2, 'REPAY: hub debt mismatch' ); assertEq( - stdMath.delta(snapshotBefore.spokeOnHub.drawnShares, snapshotAfter.spokeOnHub.drawnShares), + snapshotBefore.spokeOnHub.drawnShares.delta(snapshotAfter.spokeOnHub.drawnShares), expectedRestoredShares, 'REPAY: hub drawn shares mismatch' ); @@ -394,10 +407,12 @@ abstract contract Actions is CommonTestBase { ); assertGt(debtSnapshotBefore.user.totalDebt, 0, 'LIQUIDATE: borrower has no debt'); - vm.startPrank(liquidator); - uint256 dealAmount = _maxDealAmount(debtInfo.decimals); - deal2(debtInfo.underlying, liquidator, dealAmount); - IERC20(debtInfo.underlying).forceApprove(address(spoke), debtToCover); + _forceApprove({ + spoke: spoke, + underlying: debtInfo.underlying, + user: liquidator, + amount: debtSnapshotBefore.user.totalDebt + }); if (debtToCover == UINT256_MAX) { console.log( @@ -414,6 +429,7 @@ abstract contract Actions is CommonTestBase { ); } + vm.prank(liquidator); spoke.liquidationCall({ collateralReserveId: collateralInfo.reserveId, debtReserveId: debtInfo.reserveId, @@ -421,7 +437,6 @@ abstract contract Actions is CommonTestBase { debtToCover: debtToCover, receiveShares: receiveShares }); - vm.stopPrank(); Types.PositionSnapshot memory collateralSnapshotAfter = _getPositionSnapshot( spoke, diff --git a/src/dependencies/v4/GatewayScenarios.sol b/src/dependencies/v4/GatewayScenarios.sol index cd2556ac..8c332eb7 100644 --- a/src/dependencies/v4/GatewayScenarios.sol +++ b/src/dependencies/v4/GatewayScenarios.sol @@ -4,7 +4,12 @@ pragma solidity ^0.8.0; import 'forge-std/Test.sol'; import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; -import {ISpoke, IAaveOracle, INativeTokenGateway, ISignatureGateway} from 'aave-address-book/AaveV4.sol'; +import { + ISpoke, + IAaveOracle, + INativeTokenGateway, + ISignatureGateway +} from 'aave-address-book/AaveV4.sol'; import {IHubBase} from 'aave-v4/hub/interfaces/IHubBase.sol'; import {Types} from 'src/dependencies/v4/Types.sol'; import {Helpers} from 'src/dependencies/v4/Helpers.sol'; @@ -13,6 +18,7 @@ import {Helpers} from 'src/dependencies/v4/Helpers.sol'; /// @notice E2E test scenarios for NativeTokenGateway and SignatureGateway. abstract contract GatewayScenarios is Helpers { using SafeERC20 for IERC20; + using stdMath for uint256; // ------------------------------------------------------------------------- // Helpers @@ -61,7 +67,11 @@ abstract contract GatewayScenarios is Helpers { uint256 gatewaySnapshot = vm.snapshotState(); address user = vm.randomAddress(); - uint256 amount = _halfToken(wethInfo.decimals); + uint256 amount = _getTokenAmountByDollarValue( + spoke.ORACLE(), + wethInfo, + vm.randomUint(1_000, 10_000) + ); // Authorize gateway as position manager for user vm.prank(user); @@ -142,21 +152,18 @@ abstract contract GatewayScenarios is Helpers { Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, wethInfo, user); assertApproxEqAbs( - stdMath.delta(snapshotAfter.user.collateralAssets, snapshotBefore.user.collateralAssets), + snapshotAfter.user.collateralAssets.delta(snapshotBefore.user.collateralAssets), amountSupplied, 1, 'NATIVE_SUPPLY: user assets mismatch' ); assertEq( - stdMath.delta(snapshotAfter.user.collateralShares, snapshotBefore.user.collateralShares), + snapshotAfter.user.collateralShares.delta(snapshotBefore.user.collateralShares), sharesSupplied, 'NATIVE_SUPPLY: user shares mismatch' ); assertApproxEqAbs( - stdMath.delta( - snapshotAfter.spokeOnHub.collateralAssets, - snapshotBefore.spokeOnHub.collateralAssets - ), + snapshotAfter.spokeOnHub.collateralAssets.delta(snapshotBefore.spokeOnHub.collateralAssets), amountSupplied, 1, 'NATIVE_SUPPLY: hub assets mismatch' @@ -190,21 +197,18 @@ abstract contract GatewayScenarios is Helpers { Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, wethInfo, user); assertApproxEqAbs( - stdMath.delta(snapshotAfter.user.collateralAssets, snapshotBefore.user.collateralAssets), + snapshotAfter.user.collateralAssets.delta(snapshotBefore.user.collateralAssets), amountSupplied, 1, 'NATIVE_SUPPLY_AS_COLLATERAL: user assets mismatch' ); assertEq( - stdMath.delta(snapshotAfter.user.collateralShares, snapshotBefore.user.collateralShares), + snapshotAfter.user.collateralShares.delta(snapshotBefore.user.collateralShares), sharesSupplied, 'NATIVE_SUPPLY_AS_COLLATERAL: user shares mismatch' ); assertApproxEqAbs( - stdMath.delta( - snapshotAfter.spokeOnHub.collateralAssets, - snapshotBefore.spokeOnHub.collateralAssets - ), + snapshotAfter.spokeOnHub.collateralAssets.delta(snapshotBefore.spokeOnHub.collateralAssets), amountSupplied, 1, 'NATIVE_SUPPLY_AS_COLLATERAL: hub assets mismatch' @@ -273,7 +277,7 @@ abstract contract GatewayScenarios is Helpers { assertEq(amountWithdrawn, withdrawAmount, 'NATIVE_WITHDRAW: amount mismatch'); } assertEq( - stdMath.delta(user.balance, ethBefore), + user.balance.delta(ethBefore), expectedWithdrawnAmount, 'NATIVE_WITHDRAW: user ETH mismatch' ); @@ -282,30 +286,24 @@ abstract contract GatewayScenarios is Helpers { Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, wethInfo, user); assertApproxEqAbs( - stdMath.delta(snapshotBefore.user.collateralAssets, snapshotAfter.user.collateralAssets), + snapshotBefore.user.collateralAssets.delta(snapshotAfter.user.collateralAssets), expectedWithdrawnAmount, 1, 'NATIVE_WITHDRAW: user assets mismatch' ); assertEq( - stdMath.delta(snapshotBefore.user.collateralShares, snapshotAfter.user.collateralShares), + snapshotBefore.user.collateralShares.delta(snapshotAfter.user.collateralShares), sharesWithdrawn, 'NATIVE_WITHDRAW: user shares mismatch' ); assertApproxEqAbs( - stdMath.delta( - snapshotBefore.spokeOnHub.collateralAssets, - snapshotAfter.spokeOnHub.collateralAssets - ), + snapshotBefore.spokeOnHub.collateralAssets.delta(snapshotAfter.spokeOnHub.collateralAssets), expectedWithdrawnAmount, 1, 'NATIVE_WITHDRAW: hub assets mismatch' ); assertEq( - stdMath.delta( - snapshotBefore.spokeOnHub.collateralShares, - snapshotAfter.spokeOnHub.collateralShares - ), + snapshotBefore.spokeOnHub.collateralShares.delta(snapshotAfter.spokeOnHub.collateralShares), sharesWithdrawn, 'NATIVE_WITHDRAW: hub shares mismatch' ); @@ -389,33 +387,29 @@ abstract contract GatewayScenarios is Helpers { borrowAmount ); assertEq(amountBorrowed, borrowAmount, 'NATIVE_BORROW: amount mismatch'); - assertEq( - stdMath.delta(user.balance, ethBefore), - borrowAmount, - 'NATIVE_BORROW: user ETH mismatch' - ); + assertEq(user.balance.delta(ethBefore), borrowAmount, 'NATIVE_BORROW: user ETH mismatch'); } Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, wethInfo, user); assertEq( - stdMath.delta(snapshotAfter.user.drawnShares, snapshotBefore.user.drawnShares), + snapshotAfter.user.drawnShares.delta(snapshotBefore.user.drawnShares), sharesBorrowed, 'NATIVE_BORROW: user drawn shares mismatch' ); assertApproxEqAbs( - stdMath.delta(snapshotAfter.user.totalDebt, snapshotBefore.user.totalDebt), + snapshotAfter.user.totalDebt.delta(snapshotBefore.user.totalDebt), borrowAmount, 2, 'NATIVE_BORROW: user debt asset mismatch' ); assertApproxEqAbs( - stdMath.delta(snapshotAfter.spokeOnHub.totalDebt, snapshotBefore.spokeOnHub.totalDebt), + snapshotAfter.spokeOnHub.totalDebt.delta(snapshotBefore.spokeOnHub.totalDebt), borrowAmount, 2, 'NATIVE_BORROW: hub debt mismatch' ); assertEq( - stdMath.delta(snapshotAfter.spokeOnHub.drawnShares, snapshotBefore.spokeOnHub.drawnShares), + snapshotAfter.spokeOnHub.drawnShares.delta(snapshotBefore.spokeOnHub.drawnShares), sharesBorrowed, 'NATIVE_BORROW: hub drawn shares mismatch' ); @@ -443,33 +437,29 @@ abstract contract GatewayScenarios is Helpers { repayAmount ); assertEq(amountRepaid, repayAmount, 'NATIVE_REPAY: amount mismatch'); - assertEq( - stdMath.delta(user.balance, ethBefore), - repayAmount, - 'NATIVE_REPAY: user ETH mismatch' - ); + assertEq(user.balance.delta(ethBefore), repayAmount, 'NATIVE_REPAY: user ETH mismatch'); } Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, wethInfo, user); assertEq( - stdMath.delta(snapshotBefore.user.drawnShares, snapshotAfter.user.drawnShares), + snapshotBefore.user.drawnShares.delta(snapshotAfter.user.drawnShares), sharesRepaid, 'NATIVE_REPAY: user drawn shares mismatch' ); assertApproxEqAbs( - stdMath.delta(snapshotBefore.user.totalDebt, snapshotAfter.user.totalDebt), + snapshotBefore.user.totalDebt.delta(snapshotAfter.user.totalDebt), repayAmount, 2, 'NATIVE_REPAY: user debt mismatch' ); assertApproxEqAbs( - stdMath.delta(snapshotBefore.spokeOnHub.totalDebt, snapshotAfter.spokeOnHub.totalDebt), + snapshotBefore.spokeOnHub.totalDebt.delta(snapshotAfter.spokeOnHub.totalDebt), repayAmount, 2, 'NATIVE_REPAY: hub debt mismatch' ); assertEq( - stdMath.delta(snapshotBefore.spokeOnHub.drawnShares, snapshotAfter.spokeOnHub.drawnShares), + snapshotBefore.spokeOnHub.drawnShares.delta(snapshotAfter.spokeOnHub.drawnShares), sharesRepaid, 'NATIVE_REPAY: hub drawn shares mismatch' ); @@ -496,7 +486,11 @@ abstract contract GatewayScenarios is Helpers { ) internal { uint256 privateKey = vm.randomUint(1, type(uint248).max); address user = vm.addr(privateKey); - uint256 amount = _halfToken(reserveInfo.decimals); + uint256 amount = _getTokenAmountByDollarValue( + spoke.ORACLE(), + reserveInfo, + vm.randomUint(1_000, 10_000) + ); // Authorize gateway as position manager for user vm.prank(user); @@ -530,8 +524,7 @@ abstract contract GatewayScenarios is Helpers { reserveInfo: reserveInfo, collateralInfo: collateralInfo, privateKey: privateKey, - user: user, - amount: amount + user: user }); } } @@ -722,11 +715,19 @@ abstract contract GatewayScenarios is Helpers { Types.ReserveInfo memory reserveInfo, Types.ReserveInfo memory collateralInfo, uint256 privateKey, - address user, - uint256 amount + address user ) internal { + address oracleAddr = spoke.ORACLE(); + uint256 borrowDollars = vm.randomUint(1_000, 10_000); + uint256 borrowAmount = _getTokenAmountByDollarValue(oracleAddr, reserveInfo, borrowDollars); + // 3x collateral to ensure HF stays above 1 + uint256 collateralAmount = _getTokenAmountByDollarValue( + oracleAddr, + collateralInfo, + borrowDollars * 3 + ); + // Supply collateral + enable as collateral via sig - uint256 collateralAmount = _halfToken(collateralInfo.decimals); _sigSupply({ gateway: gateway, spoke: spoke, @@ -744,8 +745,7 @@ abstract contract GatewayScenarios is Helpers { }); // Ensure liquidity + borrow + repay - _ensureLiquidity({spoke: spoke, reserveInfo: reserveInfo, amount: amount}); - uint256 borrowAmount = amount / 4; + _ensureLiquidity({spoke: spoke, reserveInfo: reserveInfo, amount: borrowAmount}); _sigBorrow({ gateway: gateway, spoke: spoke, diff --git a/src/dependencies/v4/Helpers.sol b/src/dependencies/v4/Helpers.sol index 4f9fd2c7..dd5886d9 100644 --- a/src/dependencies/v4/Helpers.sol +++ b/src/dependencies/v4/Helpers.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IERC20Metadata} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import { + IERC20Metadata +} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; import {ISpoke, IHubConfigurator, IAaveOracle} from 'aave-address-book/AaveV4.sol'; import {AaveV4Ethereum} from 'aave-address-book/AaveV4Ethereum.sol'; import {Types} from 'src/dependencies/v4/Types.sol'; @@ -15,22 +17,20 @@ abstract contract Helpers is Actions { uint256 count = spoke.getReserveCount(); Types.ReserveInfo[] memory info = new Types.ReserveInfo[](count); - for (uint256 i; i < count; i++) { - ISpoke.Reserve memory reserve = spoke.getReserve(i); - ISpoke.ReserveConfig memory config = spoke.getReserveConfig(i); + for (uint256 reserveId; reserveId < count; reserveId++) { + ISpoke.Reserve memory reserve = spoke.getReserve(reserveId); + ISpoke.ReserveConfig memory config = spoke.getReserveConfig(reserveId); ISpoke.DynamicReserveConfig memory dynamicConfig = spoke.getDynamicReserveConfig( - i, + reserveId, reserve.dynamicConfigKey ); - string memory symbol = _safeSymbol(reserve.underlying); - - info[i] = Types.ReserveInfo({ - reserveId: i, + info[reserveId] = Types.ReserveInfo({ + reserveId: reserveId, underlying: reserve.underlying, hub: address(reserve.hub), assetId: reserve.assetId, - symbol: symbol, + symbol: _safeSymbol(reserve.underlying), decimals: reserve.decimals, paused: config.paused, frozen: config.frozen, @@ -377,17 +377,7 @@ abstract contract Helpers is Actions { vm.clearMockedCalls(); } - /// @notice Safely get the ERC20 symbol, fallback to "UNKNOWN". function _safeSymbol(address token) internal view returns (string memory) { - try IERC20Metadata(token).symbol() returns (string memory s) { - return s; - } catch { - return 'UNKNOWN'; - } - } - - /// @notice Half a token in the asset's native decimals. - function _halfToken(uint8 decimals) internal pure returns (uint256) { - return 10 ** decimals / 2; + return IERC20Metadata(token).symbol(); } } diff --git a/src/dependencies/v4/Scenarios.sol b/src/dependencies/v4/Scenarios.sol index 58d56e06..1cf2c080 100644 --- a/src/dependencies/v4/Scenarios.sol +++ b/src/dependencies/v4/Scenarios.sol @@ -445,12 +445,12 @@ abstract contract Scenarios is Helpers { // technically possible to liquidate less if premium debt exists, but serves as a basic check if ( spoke.getReserve(testAssetInfo.reserveId).hub.previewRestoreByAssets( - testAssetInfo.reserveId, + testAssetInfo.assetId, partialDebt ) == 0 ) { partialDebt = spoke.getReserve(testAssetInfo.reserveId).hub.previewRestoreByShares( - testAssetInfo.reserveId, + testAssetInfo.assetId, 1 ); } diff --git a/src/dependencies/v4/SnapshotV4.sol b/src/dependencies/v4/SnapshotV4.sol index 501f58fd..e7482cb5 100644 --- a/src/dependencies/v4/SnapshotV4.sol +++ b/src/dependencies/v4/SnapshotV4.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; import 'forge-std/Test.sol'; -import {IERC20Metadata} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; import {ISpoke, IHub, IAaveOracle} from 'aave-address-book/AaveV4.sol'; import {IAssetInterestRateStrategy} from 'aave-v4/hub/interfaces/IAssetInterestRateStrategy.sol'; import {Types} from 'src/dependencies/v4/Types.sol'; From 54145592a042c8187716db162366e62369d44b01 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:48:05 -0500 Subject: [PATCH 03/29] feat: ts based diffs --- ...it_payload_before_v4_emit_payload_after.md | 30 + diffs/v4_no_e2e_before_v4_no_e2e_after.md | 30 + ...orage_pass_before_v4_storage_pass_after.md | 30 + foundry.toml | 2 +- .../__tests__/protocol-diff-v4.spec.ts | 249 ++ packages/aave-helpers-js/cli.ts | 17 + packages/aave-helpers-js/formatters-v4.ts | 168 + packages/aave-helpers-js/index.ts | 8 + packages/aave-helpers-js/protocol-diff-v4.ts | 57 + .../aave-helpers-js/sections/hub-assets.ts | 104 + .../aave-helpers-js/sections/spoke-caps.ts | 109 + .../sections/spoke-liquidation.ts | 86 + .../sections/spoke-reserves.ts | 115 + packages/aave-helpers-js/snapshot-types-v4.ts | 83 + reports/v4_emit_payload_after.json | 2770 +++++++++++++++++ reports/v4_emit_payload_before.json | 2728 ++++++++++++++++ src/ProtocolV4TestBase.sol | 2 +- src/dependencies/v4/GatewayScenarios.sol | 2 +- src/dependencies/v4/SnapshotV4.sol | 59 +- src/dependencies/v4/V4DiffWriter.sol | 417 +-- tests/ProtocolV4TestBase.t.sol | 10 - 21 files changed, 6621 insertions(+), 455 deletions(-) create mode 100644 diffs/v4_emit_payload_before_v4_emit_payload_after.md create mode 100644 diffs/v4_no_e2e_before_v4_no_e2e_after.md create mode 100644 diffs/v4_storage_pass_before_v4_storage_pass_after.md create mode 100644 packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts create mode 100644 packages/aave-helpers-js/formatters-v4.ts create mode 100644 packages/aave-helpers-js/protocol-diff-v4.ts create mode 100644 packages/aave-helpers-js/sections/hub-assets.ts create mode 100644 packages/aave-helpers-js/sections/spoke-caps.ts create mode 100644 packages/aave-helpers-js/sections/spoke-liquidation.ts create mode 100644 packages/aave-helpers-js/sections/spoke-reserves.ts create mode 100644 packages/aave-helpers-js/snapshot-types-v4.ts create mode 100644 reports/v4_emit_payload_after.json create mode 100644 reports/v4_emit_payload_before.json diff --git a/diffs/v4_emit_payload_before_v4_emit_payload_after.md b/diffs/v4_emit_payload_before_v4_emit_payload_after.md new file mode 100644 index 00000000..318d7b79 --- /dev/null +++ b/diffs/v4_emit_payload_before_v4_emit_payload_after.md @@ -0,0 +1,30 @@ +## Event logs + +#### 0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A (AaveV2Ethereum.POOL_ADMIN, AaveV2EthereumAMM.POOL_ADMIN, AaveV3Ethereum.ACL_ADMIN, AaveV3EthereumEtherFi.ACL_ADMIN, AaveV3EthereumHorizon.ACL_ADMIN, AaveV3EthereumLido.ACL_ADMIN, GovernanceV3Ethereum.EXECUTOR_LVL_1) + +| index | event | +| --- | --- | +| 0 | topics: `0x24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b`, data: `0x` | +| 1 | ExecutedAction(target: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f, value: 0, signature: execute(), data: 0x, executionTime: 1775579243, withDelegatecall: true, resultData: 0x) | + +#### 0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5 (GovernanceV3Ethereum.PAYLOADS_CONTROLLER) + +| index | event | +| --- | --- | +| 2 | PayloadExecuted(payloadId: 426) | + +## Raw storage changes + +### 0xdabad81af85554e9ae636395611c58f7ec1aaec5 (GovernanceV3Ethereum.PAYLOADS_CONTROLLER) + +| slot | previous value | new value | +| --- | --- | --- | +| 0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c7 | 0x0069d5306a000000000002000000000000000000000000000000000000000000 | 0x0069d5306a000000000003000000000000000000000000000000000000000000 | +| 0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c8 | 0x000000000000000000093a800000000000006a0354eb00000000000000000000 | 0x000000000000000000093a800000000000006a0354eb00000000000069d5306b | + + +## Raw diff + +```json +{} +``` diff --git a/diffs/v4_no_e2e_before_v4_no_e2e_after.md b/diffs/v4_no_e2e_before_v4_no_e2e_after.md new file mode 100644 index 00000000..318d7b79 --- /dev/null +++ b/diffs/v4_no_e2e_before_v4_no_e2e_after.md @@ -0,0 +1,30 @@ +## Event logs + +#### 0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A (AaveV2Ethereum.POOL_ADMIN, AaveV2EthereumAMM.POOL_ADMIN, AaveV3Ethereum.ACL_ADMIN, AaveV3EthereumEtherFi.ACL_ADMIN, AaveV3EthereumHorizon.ACL_ADMIN, AaveV3EthereumLido.ACL_ADMIN, GovernanceV3Ethereum.EXECUTOR_LVL_1) + +| index | event | +| --- | --- | +| 0 | topics: `0x24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b`, data: `0x` | +| 1 | ExecutedAction(target: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f, value: 0, signature: execute(), data: 0x, executionTime: 1775579243, withDelegatecall: true, resultData: 0x) | + +#### 0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5 (GovernanceV3Ethereum.PAYLOADS_CONTROLLER) + +| index | event | +| --- | --- | +| 2 | PayloadExecuted(payloadId: 426) | + +## Raw storage changes + +### 0xdabad81af85554e9ae636395611c58f7ec1aaec5 (GovernanceV3Ethereum.PAYLOADS_CONTROLLER) + +| slot | previous value | new value | +| --- | --- | --- | +| 0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c7 | 0x0069d5306a000000000002000000000000000000000000000000000000000000 | 0x0069d5306a000000000003000000000000000000000000000000000000000000 | +| 0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c8 | 0x000000000000000000093a800000000000006a0354eb00000000000000000000 | 0x000000000000000000093a800000000000006a0354eb00000000000069d5306b | + + +## Raw diff + +```json +{} +``` diff --git a/diffs/v4_storage_pass_before_v4_storage_pass_after.md b/diffs/v4_storage_pass_before_v4_storage_pass_after.md new file mode 100644 index 00000000..318d7b79 --- /dev/null +++ b/diffs/v4_storage_pass_before_v4_storage_pass_after.md @@ -0,0 +1,30 @@ +## Event logs + +#### 0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A (AaveV2Ethereum.POOL_ADMIN, AaveV2EthereumAMM.POOL_ADMIN, AaveV3Ethereum.ACL_ADMIN, AaveV3EthereumEtherFi.ACL_ADMIN, AaveV3EthereumHorizon.ACL_ADMIN, AaveV3EthereumLido.ACL_ADMIN, GovernanceV3Ethereum.EXECUTOR_LVL_1) + +| index | event | +| --- | --- | +| 0 | topics: `0x24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b`, data: `0x` | +| 1 | ExecutedAction(target: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f, value: 0, signature: execute(), data: 0x, executionTime: 1775579243, withDelegatecall: true, resultData: 0x) | + +#### 0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5 (GovernanceV3Ethereum.PAYLOADS_CONTROLLER) + +| index | event | +| --- | --- | +| 2 | PayloadExecuted(payloadId: 426) | + +## Raw storage changes + +### 0xdabad81af85554e9ae636395611c58f7ec1aaec5 (GovernanceV3Ethereum.PAYLOADS_CONTROLLER) + +| slot | previous value | new value | +| --- | --- | --- | +| 0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c7 | 0x0069d5306a000000000002000000000000000000000000000000000000000000 | 0x0069d5306a000000000003000000000000000000000000000000000000000000 | +| 0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c8 | 0x000000000000000000093a800000000000006a0354eb00000000000000000000 | 0x000000000000000000093a800000000000006a0354eb00000000000069d5306b | + + +## Raw diff + +```json +{} +``` diff --git a/foundry.toml b/foundry.toml index ac22d79e..6d3c4ce4 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,7 +5,7 @@ script = 'scripts' out = 'out' libs = ['lib'] remappings = [] -fs_permissions = [{ access = "read-write", path = "./reports" }, { access = "read-write", path = "./diffs" }] +fs_permissions = [{ access = "read-write", path = "./reports" }] ffi = true evm_version = 'cancun' decode_external_storage = true diff --git a/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts new file mode 100644 index 00000000..aacbf246 --- /dev/null +++ b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts @@ -0,0 +1,249 @@ +import { describe, it, expect } from 'vitest'; +import { diffV4Snapshots } from '../protocol-diff-v4'; +import { aaveV4SnapshotSchema, type AaveV4Snapshot } from '../snapshot-types-v4'; +import { formatBps } from '../formatters-v4'; + +// --- Fixtures --- + +const SPOKE_ADDR = '0x1111111111111111111111111111111111111111'; +const HUB_ADDR = '0x2222222222222222222222222222222222222222'; +const ORACLE_ADDR = '0x3333333333333333333333333333333333333333'; +const PRICE_SRC = '0x4444444444444444444444444444444444444444'; +const UNDERLYING = '0x5555555555555555555555555555555555555555'; +const IR_STRATEGY = '0x6666666666666666666666666666666666666666'; +const FEE_RECV = '0x7777777777777777777777777777777777777777'; +const REINVEST = '0x8888888888888888888888888888888888888888'; + +function makeSnapshot(overrides?: Partial): AaveV4Snapshot { + return { + chainId: 1, + spokeReserves: { + [SPOKE_ADDR]: { + '0': { + symbol: 'WETH', + underlying: UNDERLYING, + hub: HUB_ADDR, + assetId: 0, + decimals: 18, + collateralRisk: 100, + paused: false, + frozen: false, + borrowable: true, + receiveSharesEnabled: true, + dynamicConfigKey: 0, + collateralFactor: 8000, + maxLiquidationBonus: 500, + liquidationFee: 100, + oracleAddress: ORACLE_ADDR, + priceSource: PRICE_SRC, + oraclePrice: '200000000000', + }, + }, + }, + spokeLiquidationConfigs: { + [SPOKE_ADDR]: { + targetHealthFactor: '1050000000000000000', + healthFactorForMaxBonus: '1000000000000000000', + liquidationBonusFactor: 500, + maxUserReservesLimit: 128, + }, + }, + hubAssets: { + [HUB_ADDR]: { + '0': { + symbol: 'WETH', + underlying: UNDERLYING, + decimals: 18, + liquidityFee: 1000, + irStrategy: IR_STRATEGY, + feeReceiver: FEE_RECV, + reinvestmentController: REINVEST, + optimalUsageRatio: 8000, + baseDrawnRate: 100, + rateGrowthBeforeOptimal: 400, + rateGrowthAfterOptimal: 6000, + maxDrawnRate: '10000', + }, + }, + }, + spokeCaps: { + [`${HUB_ADDR}_0_${SPOKE_ADDR}`]: { + assetSymbol: 'WETH', + addCap: '1000000', + drawCap: '500000', + riskPremiumThreshold: 100, + active: true, + halted: false, + }, + }, + ...overrides, + }; +} + +// --- Schema validation --- + +describe('V4 snapshot Zod schema', () => { + it('validates a well-formed snapshot', () => { + const result = aaveV4SnapshotSchema.safeParse(makeSnapshot()); + expect(result.success).toBe(true); + }); + + it('rejects a snapshot missing required fields', () => { + const result = aaveV4SnapshotSchema.safeParse({ chainId: 1 }); + expect(result.success).toBe(false); + }); + + it('accepts optional raw and logs', () => { + const snap = makeSnapshot({ raw: {}, logs: [] }); + const result = aaveV4SnapshotSchema.safeParse(snap); + expect(result.success).toBe(true); + }); +}); + +// --- Formatter --- + +describe('formatBps', () => { + it('formats 8000 as 80.00 %', () => { + expect(formatBps(8000)).toBe('80.00 % [8000]'); + }); + + it('formats 100 as 1.00 %', () => { + expect(formatBps(100)).toBe('1.00 % [100]'); + }); + + it('formats 50 as 0.50 %', () => { + expect(formatBps(50)).toBe('0.50 % [50]'); + }); + + it('formats 5 as 0.05 %', () => { + expect(formatBps(5)).toBe('0.05 % [5]'); + }); + + it('formats 0 as 0.00 %', () => { + expect(formatBps(0)).toBe('0.00 % [0]'); + }); +}); + +// --- Diff --- + +describe('diffV4Snapshots', () => { + it('returns no-change message for identical snapshots', async () => { + const snap = makeSnapshot(); + const result = await diffV4Snapshots(snap, snap); + expect(result).toBe('No configuration changes detected.\n'); + }); + + it('detects spoke reserve changes', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + after.spokeReserves[SPOKE_ADDR]['0'] = { + ...after.spokeReserves[SPOKE_ADDR]['0'], + collateralFactor: 7500, + frozen: true, + }; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('## Spoke Reserve Changes'); + expect(md).toContain('WETH'); + expect(md).toContain('collateralFactor'); + expect(md).toContain('80.00 % [8000]'); + expect(md).toContain('75.00 % [7500]'); + expect(md).toContain('frozen'); + }); + + it('detects new spoke reserve', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + after.spokeReserves[SPOKE_ADDR]['1'] = { + symbol: 'USDC', + underlying: '0x9999999999999999999999999999999999999999', + hub: HUB_ADDR, + assetId: 1, + decimals: 6, + collateralRisk: 50, + paused: false, + frozen: false, + borrowable: true, + receiveSharesEnabled: false, + dynamicConfigKey: 0, + collateralFactor: 8500, + maxLiquidationBonus: 400, + liquidationFee: 50, + oracleAddress: ORACLE_ADDR, + priceSource: PRICE_SRC, + oraclePrice: '100000000', + }; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('NEW RESERVE'); + expect(md).toContain('USDC'); + }); + + it('detects removed spoke reserve', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + delete after.spokeReserves[SPOKE_ADDR]['0']; + after.spokeReserves[SPOKE_ADDR] = {}; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('REMOVED'); + expect(md).toContain('WETH'); + }); + + it('detects hub asset changes', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + after.hubAssets[HUB_ADDR]['0'] = { + ...after.hubAssets[HUB_ADDR]['0'], + baseDrawnRate: 200, + optimalUsageRatio: 7500, + }; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('## Hub Asset Changes'); + expect(md).toContain('baseDrawnRate'); + expect(md).toContain('optimalUsageRatio'); + }); + + it('detects spoke cap changes', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + const capKey = `${HUB_ADDR}_0_${SPOKE_ADDR}`; + after.spokeCaps[capKey] = { + ...after.spokeCaps[capKey], + addCap: '2000000', + halted: true, + }; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('## Hub Spoke Cap Changes'); + expect(md).toContain('addCap'); + expect(md).toContain('halted'); + }); + + it('detects spoke liquidation config changes', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + after.spokeLiquidationConfigs[SPOKE_ADDR] = { + ...after.spokeLiquidationConfigs[SPOKE_ADDR], + liquidationBonusFactor: 600, + }; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('## Spoke Liquidation Config Changes'); + expect(md).toContain('liquidationBonusFactor'); + }); + + it('includes raw JSON diff section', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + after.spokeReserves[SPOKE_ADDR]['0'] = { + ...after.spokeReserves[SPOKE_ADDR]['0'], + collateralFactor: 7500, + }; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('## Raw diff'); + expect(md).toContain('```json'); + }); +}); diff --git a/packages/aave-helpers-js/cli.ts b/packages/aave-helpers-js/cli.ts index b7e5d7dc..c5ab92b3 100644 --- a/packages/aave-helpers-js/cli.ts +++ b/packages/aave-helpers-js/cli.ts @@ -16,6 +16,7 @@ import { } from '@bgd-labs/toolbox'; import { getAddressBookReferences } from '@aave-dao/aave-address-book/utils'; import { diffSnapshots } from './protocol-diff'; +import { diffV4Snapshots } from './protocol-diff-v4'; import { Address, encodeFunctionData, Hex, parseAbi, zeroAddress } from 'viem'; import { readContract } from 'viem/actions'; import { toAccount } from 'viem/accounts'; @@ -40,6 +41,22 @@ program writeFileSync(opts.out, md, 'utf-8'); }); +program + .command('diff-v4-snapshots') + .description('Diff two Aave V4 protocol snapshot JSON files and produce a markdown report') + .argument('', 'path to the before snapshot JSON') + .argument('', 'path to the after snapshot JSON') + .requiredOption('-o, --out ', 'output path for the markdown report') + .action(async (beforePath: string, afterPath: string, opts: { out: string }) => { + const before = JSON.parse(readFileSync(beforePath, 'utf-8')); + const after = JSON.parse(readFileSync(afterPath, 'utf-8')); + + const md = await diffV4Snapshots(before, after); + + mkdirSync(dirname(opts.out), { recursive: true }); + writeFileSync(opts.out, md, 'utf-8'); + }); + async function uploadToPinata(source: string) { const PINATA_KEY = process.env.PINATA_KEY; if (!PINATA_KEY) throw new Error('PINATA_KEY env must be set'); diff --git a/packages/aave-helpers-js/formatters-v4.ts b/packages/aave-helpers-js/formatters-v4.ts new file mode 100644 index 00000000..fae72bb5 --- /dev/null +++ b/packages/aave-helpers-js/formatters-v4.ts @@ -0,0 +1,168 @@ +import type { Hex } from 'viem'; +import { getClient } from '@bgd-labs/toolbox'; +import { toAddressLink, boolToMarkdown } from './utils/markdown'; +import type { + V4SpokeReserve, + V4HubAsset, + V4SpokeCap, + V4SpokeLiquidationConfig, +} from './snapshot-types-v4'; + +// --- Formatter context --- + +export interface V4FormatterContext { + chainId: number; +} + +export type FieldFormatter = (value: T, ctx: V4FormatterContext) => string; + +// --- Helpers --- + +function getExplorerClient(chainId: number) { + return getClient(chainId, {}); +} + +function addressLink(value: string, chainId: number): string { + return toAddressLink(value as Hex, true, getExplorerClient(chainId)); +} + +function isAddress(value: unknown): boolean { + return typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value); +} + +/** Format BPS value as percentage, matching Solidity _ps(): "W.FF % [bps]" */ +export function formatBps(bps: number): string { + const w = Math.floor(bps / 100); + const f = bps % 100; + const fs = f < 10 ? `0${f}` : `${f}`; + return `${w}.${fs} % [${bps}]`; +} + +// --- Spoke Reserve formatters --- + +type SpokeReserveKey = keyof V4SpokeReserve; + +const SPOKE_RESERVE_BPS_FIELDS: readonly SpokeReserveKey[] = [ + 'collateralFactor', + 'maxLiquidationBonus', + 'liquidationFee', +] as const; + +const SPOKE_RESERVE_BOOL_FIELDS: readonly SpokeReserveKey[] = [ + 'paused', + 'frozen', + 'borrowable', + 'receiveSharesEnabled', +] as const; + +const SPOKE_RESERVE_ADDRESS_FIELDS: readonly SpokeReserveKey[] = [ + 'underlying', + 'hub', + 'oracleAddress', + 'priceSource', +] as const; + +export const spokeReserveFormatters: Partial<{ + [K in SpokeReserveKey]: FieldFormatter; +}> = {}; + +for (const field of SPOKE_RESERVE_BPS_FIELDS) { + (spokeReserveFormatters[field] as FieldFormatter) = (value) => formatBps(value); +} + +for (const field of SPOKE_RESERVE_BOOL_FIELDS) { + (spokeReserveFormatters[field] as FieldFormatter) = (value) => boolToMarkdown(value); +} + +for (const field of SPOKE_RESERVE_ADDRESS_FIELDS) { + (spokeReserveFormatters[field] as FieldFormatter) = (value, ctx) => + addressLink(value, ctx.chainId); +} + +// --- Hub Asset formatters --- + +type HubAssetKey = keyof V4HubAsset; + +const HUB_ASSET_BPS_FIELDS: readonly HubAssetKey[] = [ + 'liquidityFee', + 'optimalUsageRatio', + 'baseDrawnRate', + 'rateGrowthBeforeOptimal', + 'rateGrowthAfterOptimal', +] as const; + +const HUB_ASSET_ADDRESS_FIELDS: readonly HubAssetKey[] = [ + 'underlying', + 'irStrategy', + 'feeReceiver', + 'reinvestmentController', +] as const; + +export const hubAssetFormatters: Partial<{ + [K in HubAssetKey]: FieldFormatter; +}> = {}; + +for (const field of HUB_ASSET_BPS_FIELDS) { + (hubAssetFormatters[field] as FieldFormatter) = (value) => formatBps(value); +} + +hubAssetFormatters['maxDrawnRate'] = (value) => formatBps(Number(value)); + +for (const field of HUB_ASSET_ADDRESS_FIELDS) { + (hubAssetFormatters[field] as FieldFormatter) = (value, ctx) => + addressLink(value, ctx.chainId); +} + +// --- Spoke Cap formatters --- + +type SpokeCapKey = keyof V4SpokeCap; + +const SPOKE_CAP_BOOL_FIELDS: readonly SpokeCapKey[] = ['active', 'halted'] as const; + +export const spokeCapFormatters: Partial<{ + [K in SpokeCapKey]: FieldFormatter; +}> = {}; + +for (const field of SPOKE_CAP_BOOL_FIELDS) { + (spokeCapFormatters[field] as FieldFormatter) = (value) => boolToMarkdown(value); +} + +// --- Spoke Liquidation Config formatters --- + +type SpokeLiqKey = keyof V4SpokeLiquidationConfig; + +export const spokeLiqFormatters: Partial<{ + [K in SpokeLiqKey]: FieldFormatter; +}> = {}; + +// --- Generic format function --- + +type V4SectionFormatters = { + spokeReserve: typeof spokeReserveFormatters; + hubAsset: typeof hubAssetFormatters; + spokeCap: typeof spokeCapFormatters; + spokeLiq: typeof spokeLiqFormatters; +}; + +const formattersMap: V4SectionFormatters = { + spokeReserve: spokeReserveFormatters, + hubAsset: hubAssetFormatters, + spokeCap: spokeCapFormatters, + spokeLiq: spokeLiqFormatters, +} as const; + +export function formatV4Value( + section: keyof V4SectionFormatters, + key: string, + value: unknown, + ctx: V4FormatterContext +): string { + const formatter = (formattersMap[section] as Record)[key]; + if (formatter) return formatter(value, ctx); + + // Default formatting + if (typeof value === 'boolean') return boolToMarkdown(value); + if (typeof value === 'number') return value.toLocaleString('en-US'); + if (isAddress(value)) return addressLink(value as string, ctx.chainId); + return String(value); +} diff --git a/packages/aave-helpers-js/index.ts b/packages/aave-helpers-js/index.ts index 784d68e0..9e2957f6 100644 --- a/packages/aave-helpers-js/index.ts +++ b/packages/aave-helpers-js/index.ts @@ -1,4 +1,5 @@ export { diffSnapshots } from './protocol-diff'; +export { diffV4Snapshots } from './protocol-diff-v4'; export { eventDb } from './utils/eventDb'; export { diff, isChange, hasChanges } from './diff'; export type { Change, DiffResult } from './diff'; @@ -14,3 +15,10 @@ export type { Log, CHAIN_ID, } from './snapshot-types'; +export type { + AaveV4Snapshot, + V4SpokeReserve, + V4HubAsset, + V4SpokeCap, + V4SpokeLiquidationConfig, +} from './snapshot-types-v4'; diff --git a/packages/aave-helpers-js/protocol-diff-v4.ts b/packages/aave-helpers-js/protocol-diff-v4.ts new file mode 100644 index 00000000..98b58bd5 --- /dev/null +++ b/packages/aave-helpers-js/protocol-diff-v4.ts @@ -0,0 +1,57 @@ +import { diff } from './diff'; +import type { AaveV4Snapshot } from './snapshot-types-v4'; +import type { RawStorage, Log } from './snapshot-types'; +import { renderSpokeReservesSection } from './sections/spoke-reserves'; +import { renderHubAssetsSection } from './sections/hub-assets'; +import { renderSpokeCapsSection } from './sections/spoke-caps'; +import { renderSpokeLiquidationSection } from './sections/spoke-liquidation'; +import { renderRawSection } from './sections/raw'; +import { renderLogsSection } from './sections/logs'; + +/** + * Diff two Aave V4 protocol snapshots and produce a formatted markdown report. + * + * The `raw` and `logs` sections only exist in the "after" snapshot and are + * rendered as-is (they already represent the diff / changes). + */ +export async function diffV4Snapshots( + before: AaveV4Snapshot, + after: AaveV4Snapshot +): Promise { + // Extract raw & logs from "after" — they don't participate in the structural diff + let raw: RawStorage | undefined; + let logs: Log[] | undefined; + + const postCopy: AaveV4Snapshot = { ...after }; + if (postCopy.raw) { + raw = postCopy.raw; + delete postCopy.raw; + } + if (postCopy.logs) { + logs = [...postCopy.logs]; + delete postCopy.logs; + } + + // Build the markdown report from each section + let md = ''; + + md += renderSpokeReservesSection(before, postCopy); + md += renderHubAssetsSection(before, postCopy); + md += renderSpokeCapsSection(before, postCopy); + md += renderSpokeLiquidationSection(before, postCopy); + md += await renderLogsSection(logs, after.chainId); + md += renderRawSection(raw, after.chainId); + + // Append raw JSON diff as fallback + const preCopy: Record = { ...before }; + delete preCopy.raw; + delete preCopy.logs; + const diffWithoutUnchanged = diff(preCopy as any, postCopy as any, true); + md += `## Raw diff\n\n\`\`\`json\n${JSON.stringify(diffWithoutUnchanged, null, 2)}\n\`\`\`\n`; + + if (!md.trim() || md.trim() === '## Raw diff\n\n```json\n{}\n```') { + return 'No configuration changes detected.\n'; + } + + return md; +} diff --git a/packages/aave-helpers-js/sections/hub-assets.ts b/packages/aave-helpers-js/sections/hub-assets.ts new file mode 100644 index 00000000..eda6d25e --- /dev/null +++ b/packages/aave-helpers-js/sections/hub-assets.ts @@ -0,0 +1,104 @@ +import type { Hex } from 'viem'; +import { getClient } from '@bgd-labs/toolbox'; +import type { AaveV4Snapshot, V4HubAsset } from '../snapshot-types-v4'; +import { formatV4Value, type V4FormatterContext } from '../formatters-v4'; +import { toAddressLink } from '../utils/markdown'; + +/** All fields in display order. Every field is compared — even identity fields + * that "shouldn't" change — so unexpected mutations are never silently missed. */ +const FIELD_ORDER: (keyof V4HubAsset)[] = [ + 'symbol', + 'underlying', + 'decimals', + 'liquidityFee', + 'irStrategy', + 'feeReceiver', + 'reinvestmentController', + 'optimalUsageRatio', + 'baseDrawnRate', + 'rateGrowthBeforeOptimal', + 'rateGrowthAfterOptimal', + 'maxDrawnRate', +]; + +function hubAssetHeader(asset: V4HubAsset, hubAddr: string, assetId: string, chainId: number): string { + const client = getClient(chainId, {}); + const hubLink = toAddressLink(hubAddr as Hex, true, client); + return `### ${asset.symbol} (assetId: ${assetId}) on Hub ${hubLink}\n\n`; +} + +function renderNewHubAsset( + asset: V4HubAsset, + hubAddr: string, + assetId: string, + ctx: V4FormatterContext +): string { + let md = hubAssetHeader(asset, hubAddr, assetId, ctx.chainId); + md += '**NEW ASSET**\n\n'; + md += '| description | value |\n| --- | --- |\n'; + for (const key of FIELD_ORDER) { + md += `| ${key} | ${formatV4Value('hubAsset', key, asset[key], ctx)} |\n`; + } + return md + '\n'; +} + +function renderHubAssetDiff( + before: V4HubAsset, + after: V4HubAsset, + hubAddr: string, + assetId: string, + ctx: V4FormatterContext +): string { + const rows: string[] = []; + for (const key of FIELD_ORDER) { + const bVal = before[key]; + const aVal = after[key]; + if (String(bVal) === String(aVal)) continue; + const fromFmt = formatV4Value('hubAsset', key, bVal, ctx); + const toFmt = formatV4Value('hubAsset', key, aVal, ctx); + rows.push(`| ${key} | ${fromFmt} | ${toFmt} |`); + } + if (rows.length === 0) return ''; + + let md = hubAssetHeader(after, hubAddr, assetId, ctx.chainId); + md += '| description | value before | value after |\n| --- | --- | --- |\n'; + md += rows.join('\n') + '\n'; + return md + '\n'; +} + +export function renderHubAssetsSection( + before: AaveV4Snapshot, + after: AaveV4Snapshot +): string { + const ctx: V4FormatterContext = { chainId: after.chainId }; + + const allHubAddrs = new Set([ + ...Object.keys(before.hubAssets), + ...Object.keys(after.hubAssets), + ]); + + let body = ''; + + for (const hubAddr of allHubAddrs) { + const beforeHub = before.hubAssets[hubAddr] ?? {}; + const afterHub = after.hubAssets[hubAddr] ?? {}; + + const allAssetIds = new Set([...Object.keys(beforeHub), ...Object.keys(afterHub)]); + + for (const assetId of allAssetIds) { + const bAsset = beforeHub[assetId]; + const aAsset = afterHub[assetId]; + + if (bAsset && aAsset) { + body += renderHubAssetDiff(bAsset, aAsset, hubAddr, assetId, ctx); + } else if (aAsset) { + body += renderNewHubAsset(aAsset, hubAddr, assetId, ctx); + } else if (bAsset) { + body += hubAssetHeader(bAsset, hubAddr, assetId, ctx.chainId) + '**REMOVED**\n\n'; + } + } + } + + if (!body) return ''; + return `## Hub Asset Changes\n\n${body}`; +} diff --git a/packages/aave-helpers-js/sections/spoke-caps.ts b/packages/aave-helpers-js/sections/spoke-caps.ts new file mode 100644 index 00000000..6bde843b --- /dev/null +++ b/packages/aave-helpers-js/sections/spoke-caps.ts @@ -0,0 +1,109 @@ +import type { Hex } from 'viem'; +import { getClient } from '@bgd-labs/toolbox'; +import type { AaveV4Snapshot, V4SpokeCap } from '../snapshot-types-v4'; +import { formatV4Value, type V4FormatterContext } from '../formatters-v4'; +import { toAddressLink } from '../utils/markdown'; + +/** All fields in display order. Every field is compared so unexpected mutations + * are never silently missed. */ +const FIELD_ORDER: (keyof V4SpokeCap)[] = [ + 'assetSymbol', + 'addCap', + 'drawCap', + 'riskPremiumThreshold', + 'active', + 'halted', +]; + +/** + * Parse the composite key "hubAddr_assetId_spokeAddr". + * Ethereum addresses are 42 chars (0x + 40 hex), so the split is unambiguous. + */ +function parseCapKey(key: string): { hubAddr: string; assetId: string; spokeAddr: string } { + const hubAddr = key.slice(0, 42); + // skip the underscore after hub address + const rest = key.slice(43); + const underscoreIdx = rest.indexOf('_'); + const assetId = rest.slice(0, underscoreIdx); + const spokeAddr = rest.slice(underscoreIdx + 1); + return { hubAddr, assetId, spokeAddr }; +} + +function capHeader(cap: V4SpokeCap, hubAddr: string, assetId: string, spokeAddr: string, chainId: number): string { + const client = getClient(chainId, {}); + const hubLink = toAddressLink(hubAddr as Hex, true, client); + const spokeLink = toAddressLink(spokeAddr as Hex, true, client); + return `### ${cap.assetSymbol} (assetId: ${assetId}) on Hub ${hubLink} / Spoke ${spokeLink}\n\n`; +} + +function renderNewCap( + cap: V4SpokeCap, + hubAddr: string, + assetId: string, + spokeAddr: string, + ctx: V4FormatterContext +): string { + let md = capHeader(cap, hubAddr, assetId, spokeAddr, ctx.chainId); + md += '**NEW SPOKE**\n\n'; + md += '| description | value |\n| --- | --- |\n'; + for (const key of FIELD_ORDER) { + md += `| ${key} | ${formatV4Value('spokeCap', key, cap[key], ctx)} |\n`; + } + return md + '\n'; +} + +function renderCapDiff( + before: V4SpokeCap, + after: V4SpokeCap, + hubAddr: string, + assetId: string, + spokeAddr: string, + ctx: V4FormatterContext +): string { + const rows: string[] = []; + for (const key of FIELD_ORDER) { + const bVal = before[key]; + const aVal = after[key]; + if (String(bVal) === String(aVal)) continue; + const fromFmt = formatV4Value('spokeCap', key, bVal, ctx); + const toFmt = formatV4Value('spokeCap', key, aVal, ctx); + rows.push(`| ${key} | ${fromFmt} | ${toFmt} |`); + } + if (rows.length === 0) return ''; + + let md = capHeader(after, hubAddr, assetId, spokeAddr, ctx.chainId); + md += '| description | value before | value after |\n| --- | --- | --- |\n'; + md += rows.join('\n') + '\n'; + return md + '\n'; +} + +export function renderSpokeCapsSection( + before: AaveV4Snapshot, + after: AaveV4Snapshot +): string { + const ctx: V4FormatterContext = { chainId: after.chainId }; + + const allKeys = new Set([ + ...Object.keys(before.spokeCaps), + ...Object.keys(after.spokeCaps), + ]); + + let body = ''; + + for (const key of allKeys) { + const { hubAddr, assetId, spokeAddr } = parseCapKey(key); + const bCap = before.spokeCaps[key]; + const aCap = after.spokeCaps[key]; + + if (bCap && aCap) { + body += renderCapDiff(bCap, aCap, hubAddr, assetId, spokeAddr, ctx); + } else if (aCap) { + body += renderNewCap(aCap, hubAddr, assetId, spokeAddr, ctx); + } else if (bCap) { + body += capHeader(bCap, hubAddr, assetId, spokeAddr, ctx.chainId) + '**REMOVED**\n\n'; + } + } + + if (!body) return ''; + return `## Hub Spoke Cap Changes\n\n${body}`; +} diff --git a/packages/aave-helpers-js/sections/spoke-liquidation.ts b/packages/aave-helpers-js/sections/spoke-liquidation.ts new file mode 100644 index 00000000..804470e9 --- /dev/null +++ b/packages/aave-helpers-js/sections/spoke-liquidation.ts @@ -0,0 +1,86 @@ +import type { Hex } from 'viem'; +import { getClient } from '@bgd-labs/toolbox'; +import type { AaveV4Snapshot, V4SpokeLiquidationConfig } from '../snapshot-types-v4'; +import { formatV4Value, type V4FormatterContext } from '../formatters-v4'; +import { toAddressLink } from '../utils/markdown'; + +/** All fields in display order (this section already covers every field). */ +const FIELD_ORDER: (keyof V4SpokeLiquidationConfig)[] = [ + 'targetHealthFactor', + 'healthFactorForMaxBonus', + 'liquidationBonusFactor', + 'maxUserReservesLimit', +]; + +function spokeHeader(spokeAddr: string, chainId: number): string { + const client = getClient(chainId, {}); + const spokeLink = toAddressLink(spokeAddr as Hex, true, client); + return `### Spoke ${spokeLink}\n\n`; +} + +function renderNewSpokeLiq( + config: V4SpokeLiquidationConfig, + spokeAddr: string, + ctx: V4FormatterContext +): string { + let md = spokeHeader(spokeAddr, ctx.chainId); + md += '**NEW**\n\n'; + md += '| description | value |\n| --- | --- |\n'; + for (const key of FIELD_ORDER) { + md += `| ${key} | ${formatV4Value('spokeLiq', key, config[key], ctx)} |\n`; + } + return md + '\n'; +} + +function renderSpokeLiqDiff( + before: V4SpokeLiquidationConfig, + after: V4SpokeLiquidationConfig, + spokeAddr: string, + ctx: V4FormatterContext +): string { + const rows: string[] = []; + for (const key of FIELD_ORDER) { + const bVal = before[key]; + const aVal = after[key]; + if (String(bVal) === String(aVal)) continue; + const fromFmt = formatV4Value('spokeLiq', key, bVal, ctx); + const toFmt = formatV4Value('spokeLiq', key, aVal, ctx); + rows.push(`| ${key} | ${fromFmt} | ${toFmt} |`); + } + if (rows.length === 0) return ''; + + let md = spokeHeader(spokeAddr, ctx.chainId); + md += '| description | value before | value after |\n| --- | --- | --- |\n'; + md += rows.join('\n') + '\n'; + return md + '\n'; +} + +export function renderSpokeLiquidationSection( + before: AaveV4Snapshot, + after: AaveV4Snapshot +): string { + const ctx: V4FormatterContext = { chainId: after.chainId }; + + const allSpokeAddrs = new Set([ + ...Object.keys(before.spokeLiquidationConfigs), + ...Object.keys(after.spokeLiquidationConfigs), + ]); + + let body = ''; + + for (const spokeAddr of allSpokeAddrs) { + const bConfig = before.spokeLiquidationConfigs[spokeAddr]; + const aConfig = after.spokeLiquidationConfigs[spokeAddr]; + + if (bConfig && aConfig) { + body += renderSpokeLiqDiff(bConfig, aConfig, spokeAddr, ctx); + } else if (aConfig) { + body += renderNewSpokeLiq(aConfig, spokeAddr, ctx); + } else if (bConfig) { + body += spokeHeader(spokeAddr, ctx.chainId) + '**REMOVED**\n\n'; + } + } + + if (!body) return ''; + return `## Spoke Liquidation Config Changes\n\n${body}`; +} diff --git a/packages/aave-helpers-js/sections/spoke-reserves.ts b/packages/aave-helpers-js/sections/spoke-reserves.ts new file mode 100644 index 00000000..f95b8e93 --- /dev/null +++ b/packages/aave-helpers-js/sections/spoke-reserves.ts @@ -0,0 +1,115 @@ +import type { Hex } from 'viem'; +import { getClient } from '@bgd-labs/toolbox'; +import type { AaveV4Snapshot, V4SpokeReserve } from '../snapshot-types-v4'; +import { formatV4Value, type V4FormatterContext } from '../formatters-v4'; +import { toAddressLink } from '../utils/markdown'; + +/** All fields in display order. Every field is compared — even identity fields + * that "shouldn't" change — so unexpected mutations are never silently missed. */ +const FIELD_ORDER: (keyof V4SpokeReserve)[] = [ + 'symbol', + 'underlying', + 'hub', + 'assetId', + 'decimals', + 'collateralRisk', + 'paused', + 'frozen', + 'borrowable', + 'receiveSharesEnabled', + 'dynamicConfigKey', + 'collateralFactor', + 'maxLiquidationBonus', + 'liquidationFee', + 'oracleAddress', + 'priceSource', + 'oraclePrice', +]; + +function reserveHeader( + reserve: V4SpokeReserve, + spokeAddr: string, + reserveId: string, + chainId: number +): string { + const client = getClient(chainId, {}); + const underlyingLink = toAddressLink(reserve.underlying as Hex, true, client); + const spokeLink = toAddressLink(spokeAddr as Hex, true, client); + return `### ${reserve.symbol} (${underlyingLink}) on Spoke ${spokeLink} [reserveId: ${reserveId}]\n\n`; +} + +function renderNewReserve( + reserve: V4SpokeReserve, + spokeAddr: string, + reserveId: string, + ctx: V4FormatterContext +): string { + let md = reserveHeader(reserve, spokeAddr, reserveId, ctx.chainId); + md += '**NEW RESERVE**\n\n'; + md += '| description | value |\n| --- | --- |\n'; + for (const key of FIELD_ORDER) { + md += `| ${key} | ${formatV4Value('spokeReserve', key, reserve[key], ctx)} |\n`; + } + return md + '\n'; +} + +function renderReserveDiff( + before: V4SpokeReserve, + after: V4SpokeReserve, + spokeAddr: string, + reserveId: string, + ctx: V4FormatterContext +): string { + const rows: string[] = []; + for (const key of FIELD_ORDER) { + const bVal = before[key]; + const aVal = after[key]; + if (String(bVal) === String(aVal)) continue; + const fromFmt = formatV4Value('spokeReserve', key, bVal, ctx); + const toFmt = formatV4Value('spokeReserve', key, aVal, ctx); + rows.push(`| ${key} | ${fromFmt} | ${toFmt} |`); + } + if (rows.length === 0) return ''; + + let md = reserveHeader(after, spokeAddr, reserveId, ctx.chainId); + md += '| description | value before | value after |\n| --- | --- | --- |\n'; + md += rows.join('\n') + '\n'; + return md + '\n'; +} + +export function renderSpokeReservesSection( + before: AaveV4Snapshot, + after: AaveV4Snapshot +): string { + const ctx: V4FormatterContext = { chainId: after.chainId }; + + const allSpokeAddrs = new Set([ + ...Object.keys(before.spokeReserves), + ...Object.keys(after.spokeReserves), + ]); + + let body = ''; + + for (const spokeAddr of allSpokeAddrs) { + const beforeSpoke = before.spokeReserves[spokeAddr] ?? {}; + const afterSpoke = after.spokeReserves[spokeAddr] ?? {}; + + const allReserveIds = new Set([...Object.keys(beforeSpoke), ...Object.keys(afterSpoke)]); + + for (const reserveId of allReserveIds) { + const bRes = beforeSpoke[reserveId]; + const aRes = afterSpoke[reserveId]; + + if (bRes && aRes) { + body += renderReserveDiff(bRes, aRes, spokeAddr, reserveId, ctx); + } else if (aRes) { + body += renderNewReserve(aRes, spokeAddr, reserveId, ctx); + } else if (bRes) { + body += reserveHeader(bRes, spokeAddr, reserveId, ctx.chainId) + '**REMOVED**\n\n'; + } + } + } + + if (!body) return ''; + return `## Spoke Reserve Changes\n\n${body}`; +} diff --git a/packages/aave-helpers-js/snapshot-types-v4.ts b/packages/aave-helpers-js/snapshot-types-v4.ts new file mode 100644 index 00000000..1661c0c5 --- /dev/null +++ b/packages/aave-helpers-js/snapshot-types-v4.ts @@ -0,0 +1,83 @@ +import { z } from 'zod'; +import { rawStorageSchema, logSchema } from './snapshot-types'; + +// --- Spoke Reserve --- + +export const v4SpokeReserveSchema = z.object({ + symbol: z.string(), + underlying: z.string(), + hub: z.string(), + assetId: z.number(), + decimals: z.number(), + collateralRisk: z.number(), + paused: z.boolean(), + frozen: z.boolean(), + borrowable: z.boolean(), + receiveSharesEnabled: z.boolean(), + dynamicConfigKey: z.number(), + collateralFactor: z.number(), + maxLiquidationBonus: z.number(), + liquidationFee: z.number(), + oracleAddress: z.string(), + priceSource: z.string(), + oraclePrice: z.string(), // uint256 serialized as string +}); + +export type V4SpokeReserve = z.infer; + +// --- Spoke Liquidation Config --- + +export const v4SpokeLiquidationConfigSchema = z.object({ + targetHealthFactor: z.string(), // uint128 serialized as string + healthFactorForMaxBonus: z.string(), // uint64 serialized as string + liquidationBonusFactor: z.number(), + maxUserReservesLimit: z.number(), +}); + +export type V4SpokeLiquidationConfig = z.infer; + +// --- Hub Asset --- + +export const v4HubAssetSchema = z.object({ + symbol: z.string(), + underlying: z.string(), + decimals: z.number(), + liquidityFee: z.number(), + irStrategy: z.string(), + feeReceiver: z.string(), + reinvestmentController: z.string(), + optimalUsageRatio: z.number(), + baseDrawnRate: z.number(), + rateGrowthBeforeOptimal: z.number(), + rateGrowthAfterOptimal: z.number(), + maxDrawnRate: z.string(), // uint256 serialized as string +}); + +export type V4HubAsset = z.infer; + +// --- Spoke Cap --- + +export const v4SpokeCapSchema = z.object({ + assetSymbol: z.string(), + addCap: z.string(), // uint40 serialized as string + drawCap: z.string(), // uint40 serialized as string + riskPremiumThreshold: z.number(), + active: z.boolean(), + halted: z.boolean(), +}); + +export type V4SpokeCap = z.infer; + +// --- Full V4 Snapshot --- + +export const aaveV4SnapshotSchema = z.object({ + chainId: z.number(), + spokeReserves: z.record(z.string(), z.record(z.string(), v4SpokeReserveSchema)), + spokeLiquidationConfigs: z.record(z.string(), v4SpokeLiquidationConfigSchema), + hubAssets: z.record(z.string(), z.record(z.string(), v4HubAssetSchema)), + spokeCaps: z.record(z.string(), v4SpokeCapSchema), + raw: rawStorageSchema.optional(), + logs: z.array(logSchema).optional(), +}); + +export type AaveV4Snapshot = z.infer; diff --git a/reports/v4_emit_payload_after.json b/reports/v4_emit_payload_after.json new file mode 100644 index 00000000..99d11c77 --- /dev/null +++ b/reports/v4_emit_payload_after.json @@ -0,0 +1,2770 @@ +{ + "chainId": 1, + "hubAssets": { + "0x06002e9c4412CB7814a791eA3666D905871E536A": { + "0": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "PT-sUSDE-7MAY2026", + "underlying": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49" + }, + "1": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "PT-USDe-7MAY2026", + "underlying": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0" + }, + "2": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "sUSDe", + "underlying": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497" + }, + "3": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 2500, + "maxDrawnRate": "3450", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3000, + "rateGrowthBeforeOptimal": 450, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDe", + "underlying": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3" + }, + "4": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 1500, + "maxDrawnRate": "2450", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 2000, + "rateGrowthBeforeOptimal": 450, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "5": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 1000, + "maxDrawnRate": "3450", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3000, + "rateGrowthBeforeOptimal": 450, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + }, + "6": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 1500, + "maxDrawnRate": "2450", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 2000, + "rateGrowthBeforeOptimal": 450, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + } + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931": { + "0": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, + "1": { + "baseDrawnRate": 0, + "decimals": 8, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "WBTC", + "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + }, + "2": { + "baseDrawnRate": 0, + "decimals": 8, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "cbBTC", + "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" + }, + "3": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "wstETH", + "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + }, + "4": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 1000, + "maxDrawnRate": "2400", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 2000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "5": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 1000, + "maxDrawnRate": "2400", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 2000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "6": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 1000, + "maxDrawnRate": "3400", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + } + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9": { + "0": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 1500, + "maxDrawnRate": "1635", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 1400, + "rateGrowthBeforeOptimal": 235, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, + "1": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "wstETH", + "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + }, + "10": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 1000, + "maxDrawnRate": "4050", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3500, + "rateGrowthBeforeOptimal": 550, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "EURC", + "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" + }, + "11": { + "baseDrawnRate": 25, + "decimals": 8, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 2000, + "maxDrawnRate": "6425", + "optimalUsageRatio": 8000, + "rateGrowthAfterOptimal": 6000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "WBTC", + "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + }, + "12": { + "baseDrawnRate": 25, + "decimals": 8, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 2000, + "maxDrawnRate": "6425", + "optimalUsageRatio": 8000, + "rateGrowthAfterOptimal": 6000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "cbBTC", + "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" + }, + "13": { + "baseDrawnRate": 0, + "decimals": 8, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "LBTC", + "underlying": "0x8236a87084f8B84306f72007F36F2618A5634494" + }, + "14": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "XAUt", + "underlying": "0x68749665FF8D2d112Fa859AA293F07A622782F38" + }, + "15": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "AAVE", + "underlying": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9" + }, + "16": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "LINK", + "underlying": "0x514910771AF9Ca656af840dff83E8264EcF986CA" + }, + "2": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "weETH", + "underlying": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" + }, + "3": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "rsETH", + "underlying": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7" + }, + "4": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 1000, + "maxDrawnRate": "2400", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 2000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "5": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 1000, + "maxDrawnRate": "2400", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 2000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "6": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 1000, + "maxDrawnRate": "3400", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + }, + "7": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 2000, + "maxDrawnRate": "3900", + "optimalUsageRatio": 8000, + "rateGrowthAfterOptimal": 3500, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "RLUSD", + "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" + }, + "8": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 2000, + "maxDrawnRate": "3900", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3500, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDG", + "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" + }, + "9": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 2000, + "maxDrawnRate": "3900", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3500, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "frxUSD", + "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" + } + } + }, + "spokeCaps": { + "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0x58131E79531caB1d52301228d1f7b842F26B9649": { + "active": true, + "addCap": "400000", + "assetSymbol": "PT-sUSDE-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0x90774889c22D2F2Adf44da1f04C7c95542590df4": { + "active": true, + "addCap": "0", + "assetSymbol": "PT-sUSDE-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "PT-sUSDE-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "1400000", + "assetSymbol": "PT-sUSDE-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0x58131E79531caB1d52301228d1f7b842F26B9649": { + "active": true, + "addCap": "50000", + "assetSymbol": "PT-USDe-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "PT-USDe-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "250000", + "assetSymbol": "PT-USDe-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0xdd2Eb78BF9e6aC5068B95aD2d451e8c9Af10ac81": { + "active": true, + "addCap": "0", + "assetSymbol": "PT-USDe-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0x24f8c062e1E0451736C1D6E023510DA262a41df4": { + "active": true, + "addCap": "0", + "assetSymbol": "sUSDe", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0x58131E79531caB1d52301228d1f7b842F26B9649": { + "active": true, + "addCap": "250000", + "assetSymbol": "sUSDe", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "sUSDe", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "375000", + "assetSymbol": "sUSDe", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0x502Cd81da6a8F1785eb2eEE72713B7388E16A854": { + "active": true, + "addCap": "78000", + "assetSymbol": "USDe", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0x58131E79531caB1d52301228d1f7b842F26B9649": { + "active": true, + "addCap": "312500", + "assetSymbol": "USDe", + "drawCap": "325000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDe", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "312500", + "assetSymbol": "USDe", + "drawCap": "300000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_4_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_4_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "150000", + "assetSymbol": "USDC", + "drawCap": "187500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_4_0xc94bdd83D2c7655C280655D60954e79E88D4F949": { + "active": true, + "addCap": "37500", + "assetSymbol": "USDC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_5_0xA54382db40EC602c0a173A08f9E86Ed40F9D4D10": { + "active": true, + "addCap": "125000", + "assetSymbol": "GHO", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_5_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "GHO", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_5_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "500000", + "assetSymbol": "GHO", + "drawCap": "562500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_6_0x80835EB50694EE0e519743f67e5401e6FD300006": { + "active": true, + "addCap": "37500", + "assetSymbol": "USDT", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_6_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDT", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_6_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "150000", + "assetSymbol": "USDT", + "drawCap": "187500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_0_0x2087513383330B961A3753B47627Bbf149F31c70": { + "active": true, + "addCap": "0", + "assetSymbol": "WETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_0_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "130", + "assetSymbol": "WETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_0_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "WETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_1_0x5AE3d87De89CA6Ce501e8317887F71EABED69E18": { + "active": true, + "addCap": "0", + "assetSymbol": "WBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_1_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "6", + "assetSymbol": "WBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_1_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "WBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_2_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "5", + "assetSymbol": "cbBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_2_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "cbBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_2_0xD38098faf52D8E915EdED84fBF30F81C17906938": { + "active": true, + "addCap": "0", + "assetSymbol": "cbBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_3_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "114", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_3_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_3_0xFCD3D3C69cd032DE0cc78fE529B7447D2fe7F666": { + "active": true, + "addCap": "0", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_4_0x486415fb1F8b062c89ED548f871cf64304AACb31": { + "active": true, + "addCap": "37500", + "assetSymbol": "USDC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_4_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "150000", + "assetSymbol": "USDC", + "drawCap": "175000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_4_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_5_0x46c588DD8453aC259c1f6a54b4C9A93C2aC3762D": { + "active": true, + "addCap": "37500", + "assetSymbol": "USDT", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_5_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "150000", + "assetSymbol": "USDT", + "drawCap": "187500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_5_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDT", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_6_0x900fD46d565d1ac8995928c0179052ec02a6D0E1": { + "active": true, + "addCap": "125000", + "assetSymbol": "GHO", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_6_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "500000", + "assetSymbol": "GHO", + "drawCap": "562500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_6_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "GHO", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { + "active": true, + "addCap": "0", + "assetSymbol": "WETH", + "drawCap": "588", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0x7320CF22Ac095bA2a2e0a652F77efB836c2E751b": { + "active": true, + "addCap": "250", + "assetSymbol": "WETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "1500", + "assetSymbol": "WETH", + "drawCap": "130", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "WETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { + "active": true, + "addCap": "0", + "assetSymbol": "WETH", + "drawCap": "530", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { + "active": true, + "addCap": "0", + "assetSymbol": "WETH", + "drawCap": "441", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "EURC", + "drawCap": "50000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x6D9e2Cdd61CaF69af99b275704B6e272C41c6718": { + "active": true, + "addCap": "112500", + "assetSymbol": "EURC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "225000", + "assetSymbol": "EURC", + "drawCap": "150000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "0", + "assetSymbol": "EURC", + "drawCap": "50000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "EURC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "300000", + "assetSymbol": "EURC", + "drawCap": "312500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { + "active": true, + "addCap": "0", + "assetSymbol": "WBTC", + "drawCap": "5", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0x82A9CC4656784E55Ef2E78F704028B5E1Bfc1732": { + "active": true, + "addCap": "0", + "assetSymbol": "WBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "16", + "assetSymbol": "WBTC", + "drawCap": "1", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "WBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0x33B41B74366F55327d959FfF6D6b6fBc2853dbB1": { + "active": true, + "addCap": "0", + "assetSymbol": "cbBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { + "active": true, + "addCap": "0", + "assetSymbol": "cbBTC", + "drawCap": "3", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "13", + "assetSymbol": "cbBTC", + "drawCap": "1", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "cbBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_13_0x7961F140B570490849DB878AE222570ea838799d": { + "active": true, + "addCap": "0", + "assetSymbol": "LBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_13_0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { + "active": true, + "addCap": "9", + "assetSymbol": "LBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_13_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "LBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_14_0x4E712562fcb5337011398B6C630f55b60641cd5e": { + "active": true, + "addCap": "0", + "assetSymbol": "XAUt", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_14_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "125", + "assetSymbol": "XAUt", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_14_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "XAUt", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_15_0x0A65197b16C5969F92672051c9C9C0C75B369135": { + "active": true, + "addCap": "0", + "assetSymbol": "AAVE", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_15_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "5000", + "assetSymbol": "AAVE", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_15_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "AAVE", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_16_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "31250", + "assetSymbol": "LINK", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_16_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "LINK", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_16_0xE69C2045095C8Ab3E2a7d77de2328faE5baF797c": { + "active": true, + "addCap": "0", + "assetSymbol": "LINK", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "229", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0xcb0E7dA9c635628f6d4827355AeCa75aB8d3560f": { + "active": true, + "addCap": "0", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { + "active": true, + "addCap": "406", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0x559cEc2C840D9DBB18936Afc5E5341D78bfC7Cbe": { + "active": true, + "addCap": "0", + "assetSymbol": "weETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "58", + "assetSymbol": "weETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "weETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { + "active": true, + "addCap": "500", + "assetSymbol": "weETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_3_0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { + "active": true, + "addCap": "563", + "assetSymbol": "rsETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_3_0x45a04Ca1A5cbEeA4B44356c75EDd29b33eB2527a": { + "active": true, + "addCap": "0", + "assetSymbol": "rsETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_3_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "rsETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x5eC44a70F309854fe04d495cFE1B5dA63DD1cc73": { + "active": true, + "addCap": "312500", + "assetSymbol": "USDT", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "USDT", + "drawCap": "125000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "1250000", + "assetSymbol": "USDT", + "drawCap": "1250000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "0", + "assetSymbol": "USDT", + "drawCap": "125000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDT", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "200000", + "assetSymbol": "USDT", + "drawCap": "50000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "0", + "assetSymbol": "USDT", + "drawCap": "125000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x531E90a2376902DE8915789Fcc1075e3B0c153E7": { + "active": true, + "addCap": "312500", + "assetSymbol": "USDC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "USDC", + "drawCap": "125000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "1250000", + "assetSymbol": "USDC", + "drawCap": "1250000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "0", + "assetSymbol": "USDC", + "drawCap": "125000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "187500", + "assetSymbol": "USDC", + "drawCap": "50000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "0", + "assetSymbol": "USDC", + "drawCap": "125000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0x58C14a5E061c9bC6926c5b853445290F296C2F7B": { + "active": true, + "addCap": "125000", + "assetSymbol": "GHO", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "GHO", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "500000", + "assetSymbol": "GHO", + "drawCap": "500000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "GHO", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "0", + "assetSymbol": "GHO", + "drawCap": "12500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "RLUSD", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "500000", + "assetSymbol": "RLUSD", + "drawCap": "340000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "RLUSD", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0xC8a125AE4275a78AADc53B46Ca10566Bc9B249E0": { + "active": true, + "addCap": "125000", + "assetSymbol": "RLUSD", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "0", + "assetSymbol": "RLUSD", + "drawCap": "90000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "USDG", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "500000", + "assetSymbol": "USDG", + "drawCap": "340000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0xAC2435E3C25e8246870D33ce0a26988A46d5DB68": { + "active": true, + "addCap": "125000", + "assetSymbol": "USDG", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDG", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "0", + "assetSymbol": "USDG", + "drawCap": "90000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x2226749630775ee20230Ad65214fB339087eF30D": { + "active": true, + "addCap": "125000", + "assetSymbol": "frxUSD", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "frxUSD", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "500000", + "assetSymbol": "frxUSD", + "drawCap": "312500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "0", + "assetSymbol": "frxUSD", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "frxUSD", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "0", + "assetSymbol": "frxUSD", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "0", + "assetSymbol": "frxUSD", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + } + }, + "spokeLiquidationConfigs": { + "0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1021800000000000000" + }, + "0x58131E79531caB1d52301228d1f7b842F26B9649": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1027700000000000000" + }, + "0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "healthFactorForMaxBonus": "900000000000000000", + "liquidationBonusFactor": 9000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1307500000000000000" + }, + "0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1061500000000000000" + }, + "0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "healthFactorForMaxBonus": "900000000000000000", + "liquidationBonusFactor": 9000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1240000000000000000" + }, + "0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "healthFactorForMaxBonus": "900000000000000000", + "liquidationBonusFactor": 9000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1174000000000000000" + }, + "0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1044200000000000000" + }, + "0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1019100000000000000" + }, + "0xba1B3D55D249692b669A164024A838309B7508AF": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1033200000000000000" + }, + "0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1013700000000000000" + } + }, + "spokeReserves": { + "0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { + "0": { + "assetId": 3, + "borrowable": false, + "collateralFactor": 9500, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10100, + "oracleAddress": "0x37C316996C714Bf906743071e04E62220b3271ac", + "oraclePrice": "222136416823", + "paused": false, + "priceSource": "0x47F52B2e43D0386cF161e001835b03Ad49889e3b", + "receiveSharesEnabled": true, + "symbol": "rsETH", + "underlying": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7" + }, + "1": { + "assetId": 0, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x37C316996C714Bf906743071e04E62220b3271ac", + "oraclePrice": "207813165287", + "paused": false, + "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", + "receiveSharesEnabled": true, + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + } + }, + "0x58131E79531caB1d52301228d1f7b842F26B9649": { + "0": { + "assetId": 1, + "borrowable": false, + "collateralFactor": 9580, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10200, + "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", + "oraclePrice": "99689155", + "paused": false, + "priceSource": "0x0a72df02CE3E4185b6CEDf561f0AE651E9BeE235", + "receiveSharesEnabled": true, + "symbol": "PT-USDe-7MAY2026", + "underlying": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0" + }, + "1": { + "assetId": 0, + "borrowable": false, + "collateralFactor": 9400, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10300, + "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", + "oraclePrice": "99683533", + "paused": false, + "priceSource": "0xa0dc0249c32fa79e8B9b17c735908a60b1141B40", + "receiveSharesEnabled": true, + "symbol": "PT-sUSDE-7MAY2026", + "underlying": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49" + }, + "2": { + "assetId": 2, + "borrowable": false, + "collateralFactor": 9200, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10300, + "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", + "oraclePrice": "122650671", + "paused": false, + "priceSource": "0x42bc86f2f08419280a99d8fbEa4672e7c30a86ec", + "receiveSharesEnabled": true, + "symbol": "sUSDe", + "underlying": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497" + }, + "3": { + "assetId": 3, + "borrowable": true, + "collateralFactor": 9300, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10200, + "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0xC26D4a1c46d884cfF6dE9800B6aE7A8Cf48B4Ff8", + "receiveSharesEnabled": true, + "symbol": "USDe", + "underlying": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3" + } + }, + "0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "0": { + "assetId": 14, + "borrowable": false, + "collateralFactor": 7500, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10666, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "465498700000", + "paused": false, + "priceSource": "0x214eD9Da11D2fbe465a6fc601a91E62EbEc1a0D6", + "receiveSharesEnabled": true, + "symbol": "XAUt", + "underlying": "0x68749665FF8D2d112Fa859AA293F07A622782F38" + }, + "1": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "2": { + "assetId": 7, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "100001110", + "paused": false, + "priceSource": "0xf0eaC18E908B34770FDEe46d069c846bDa866759", + "receiveSharesEnabled": true, + "symbol": "RLUSD", + "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" + }, + "3": { + "assetId": 8, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xF29b1e3b68Fd59DD0a413811fD5d0AbaE653216d", + "receiveSharesEnabled": true, + "symbol": "USDG", + "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" + }, + "4": { + "assetId": 9, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "99992090", + "paused": false, + "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", + "receiveSharesEnabled": true, + "symbol": "frxUSD", + "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" + }, + "5": { + "assetId": 10, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "115424199", + "paused": false, + "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", + "receiveSharesEnabled": true, + "symbol": "EURC", + "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" + }, + "6": { + "assetId": 6, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", + "receiveSharesEnabled": true, + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + }, + "7": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + } + }, + "0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { + "0": { + "assetId": 13, + "borrowable": false, + "collateralFactor": 8600, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10300, + "oracleAddress": "0x198Cac7f54FFc7d709Ac0FEc4B6454CE73e21D3D", + "oraclePrice": "6841743291729", + "paused": false, + "priceSource": "0x5C1771583dbbAE5AFEd71ACD2BfC0eA4029EBB04", + "receiveSharesEnabled": true, + "symbol": "LBTC", + "underlying": "0x8236a87084f8B84306f72007F36F2618A5634494" + }, + "1": { + "assetId": 11, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x198Cac7f54FFc7d709Ac0FEc4B6454CE73e21D3D", + "oraclePrice": "6817396697793", + "paused": false, + "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", + "receiveSharesEnabled": true, + "symbol": "WBTC", + "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + }, + "2": { + "assetId": 12, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x198Cac7f54FFc7d709Ac0FEc4B6454CE73e21D3D", + "oraclePrice": "6817396697793", + "paused": false, + "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", + "receiveSharesEnabled": true, + "symbol": "cbBTC", + "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" + } + }, + "0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "0": { + "assetId": 0, + "borrowable": true, + "collateralFactor": 8300, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10555, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "207813165287", + "paused": false, + "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", + "receiveSharesEnabled": true, + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, + "1": { + "assetId": 1, + "borrowable": false, + "collateralFactor": 8000, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10666, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "255881582629", + "paused": false, + "priceSource": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C", + "receiveSharesEnabled": true, + "symbol": "wstETH", + "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + }, + "10": { + "assetId": 7, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "100001110", + "paused": false, + "priceSource": "0xf0eaC18E908B34770FDEe46d069c846bDa866759", + "receiveSharesEnabled": true, + "symbol": "RLUSD", + "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" + }, + "11": { + "assetId": 8, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xF29b1e3b68Fd59DD0a413811fD5d0AbaE653216d", + "receiveSharesEnabled": true, + "symbol": "USDG", + "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" + }, + "12": { + "assetId": 9, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "99992090", + "paused": false, + "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", + "receiveSharesEnabled": true, + "symbol": "frxUSD", + "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" + }, + "13": { + "assetId": 6, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", + "receiveSharesEnabled": true, + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + }, + "2": { + "assetId": 2, + "borrowable": false, + "collateralFactor": 8000, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10777, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "226950761571", + "paused": false, + "priceSource": "0xf112aF6F0A332B815fbEf3Ff932c057E570b62d3", + "receiveSharesEnabled": true, + "symbol": "weETH", + "underlying": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" + }, + "3": { + "assetId": 11, + "borrowable": true, + "collateralFactor": 7800, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10555, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "6817396697793", + "paused": false, + "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", + "receiveSharesEnabled": true, + "symbol": "WBTC", + "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + }, + "4": { + "assetId": 12, + "borrowable": true, + "collateralFactor": 7800, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10555, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "6817396697793", + "paused": false, + "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", + "receiveSharesEnabled": true, + "symbol": "cbBTC", + "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" + }, + "5": { + "assetId": 15, + "borrowable": false, + "collateralFactor": 7600, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10833, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "8613353000", + "paused": false, + "priceSource": "0x547a514d5e3769680Ce22B2361c10Ea13619e8a9", + "receiveSharesEnabled": true, + "symbol": "AAVE", + "underlying": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9" + }, + "6": { + "assetId": 16, + "borrowable": false, + "collateralFactor": 7100, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10777, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "863312586", + "paused": false, + "priceSource": "0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c", + "receiveSharesEnabled": true, + "symbol": "LINK", + "underlying": "0x514910771AF9Ca656af840dff83E8264EcF986CA" + }, + "7": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 7800, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10500, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "8": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 7800, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10500, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "9": { + "assetId": 10, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "115424199", + "paused": false, + "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", + "receiveSharesEnabled": true, + "symbol": "EURC", + "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" + } + }, + "0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "0": { + "assetId": 0, + "borrowable": false, + "collateralFactor": 8600, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 1000, + "maxLiquidationBonus": 10444, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "207813165287", + "paused": false, + "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", + "receiveSharesEnabled": true, + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, + "1": { + "assetId": 1, + "borrowable": false, + "collateralFactor": 8450, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 1000, + "maxLiquidationBonus": 10444, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "6817396697793", + "paused": false, + "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", + "receiveSharesEnabled": true, + "symbol": "WBTC", + "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + }, + "10": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "2": { + "assetId": 2, + "borrowable": false, + "collateralFactor": 8450, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 1000, + "maxLiquidationBonus": 10444, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "6817396697793", + "paused": false, + "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", + "receiveSharesEnabled": true, + "symbol": "cbBTC", + "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" + }, + "3": { + "assetId": 3, + "borrowable": false, + "collateralFactor": 8550, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 1000, + "maxLiquidationBonus": 10444, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "255881582629", + "paused": false, + "priceSource": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C", + "receiveSharesEnabled": true, + "symbol": "wstETH", + "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + }, + "4": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "5": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "6": { + "assetId": 6, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", + "receiveSharesEnabled": true, + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + }, + "7": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "8": { + "assetId": 9, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "99992090", + "paused": false, + "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", + "receiveSharesEnabled": true, + "symbol": "frxUSD", + "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" + }, + "9": { + "assetId": 10, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "115424199", + "paused": false, + "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", + "receiveSharesEnabled": true, + "symbol": "EURC", + "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" + } + }, + "0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "0": { + "assetId": 10, + "borrowable": true, + "collateralFactor": 9000, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10200, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "115424199", + "paused": false, + "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", + "receiveSharesEnabled": true, + "symbol": "EURC", + "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" + }, + "1": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 9000, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10200, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "2": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 9000, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10200, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "3": { + "assetId": 7, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "100001110", + "paused": false, + "priceSource": "0xf0eaC18E908B34770FDEe46d069c846bDa866759", + "receiveSharesEnabled": true, + "symbol": "RLUSD", + "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" + }, + "4": { + "assetId": 8, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xF29b1e3b68Fd59DD0a413811fD5d0AbaE653216d", + "receiveSharesEnabled": true, + "symbol": "USDG", + "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" + }, + "5": { + "assetId": 9, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "99992090", + "paused": false, + "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", + "receiveSharesEnabled": true, + "symbol": "frxUSD", + "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" + }, + "6": { + "assetId": 6, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", + "receiveSharesEnabled": true, + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + } + }, + "0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { + "0": { + "assetId": 2, + "borrowable": false, + "collateralFactor": 9550, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10100, + "oracleAddress": "0xd8B153FaAA8f2b1bC774916FEd333A4F3dE48792", + "oraclePrice": "226950761571", + "paused": false, + "priceSource": "0xf112aF6F0A332B815fbEf3Ff932c057E570b62d3", + "receiveSharesEnabled": true, + "symbol": "weETH", + "underlying": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" + }, + "1": { + "assetId": 0, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xd8B153FaAA8f2b1bC774916FEd333A4F3dE48792", + "oraclePrice": "207813165287", + "paused": false, + "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", + "receiveSharesEnabled": true, + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + } + }, + "0xba1B3D55D249692b669A164024A838309B7508AF": { + "0": { + "assetId": 1, + "borrowable": false, + "collateralFactor": 9300, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10300, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "99689155", + "paused": false, + "priceSource": "0x0a72df02CE3E4185b6CEDf561f0AE651E9BeE235", + "receiveSharesEnabled": true, + "symbol": "PT-USDe-7MAY2026", + "underlying": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0" + }, + "1": { + "assetId": 0, + "borrowable": false, + "collateralFactor": 9200, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10400, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "99683533", + "paused": false, + "priceSource": "0xa0dc0249c32fa79e8B9b17c735908a60b1141B40", + "receiveSharesEnabled": true, + "symbol": "PT-sUSDE-7MAY2026", + "underlying": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49" + }, + "2": { + "assetId": 2, + "borrowable": false, + "collateralFactor": 9200, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10300, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "122650671", + "paused": false, + "priceSource": "0x42bc86f2f08419280a99d8fbEa4672e7c30a86ec", + "receiveSharesEnabled": true, + "symbol": "sUSDe", + "underlying": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497" + }, + "3": { + "assetId": 3, + "borrowable": true, + "collateralFactor": 9300, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10200, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0xC26D4a1c46d884cfF6dE9800B6aE7A8Cf48B4Ff8", + "receiveSharesEnabled": true, + "symbol": "USDe", + "underlying": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3" + }, + "4": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "5": { + "assetId": 6, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "6": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", + "receiveSharesEnabled": true, + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + }, + "7": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "8": { + "assetId": 9, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "99992090", + "paused": false, + "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", + "receiveSharesEnabled": true, + "symbol": "frxUSD", + "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" + }, + "9": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + } + }, + "0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { + "0": { + "assetId": 1, + "borrowable": false, + "collateralFactor": 9550, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10100, + "oracleAddress": "0x664D73b6C3591333Fd79510f7ce9ef81228824F5", + "oraclePrice": "255881582629", + "paused": false, + "priceSource": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C", + "receiveSharesEnabled": true, + "symbol": "wstETH", + "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + }, + "1": { + "assetId": 0, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x664D73b6C3591333Fd79510f7ce9ef81228824F5", + "oraclePrice": "207813165287", + "paused": false, + "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", + "receiveSharesEnabled": true, + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + } + } + }, + "raw": { + "0xdabad81af85554e9ae636395611c58f7ec1aaec5": { + "label": null, + "contract": null, + "balanceDiff": null, + "nonceDiff": null, + "stateDiff": { + "0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c7": { + "previousValue": "0x0069d5306a000000000002000000000000000000000000000000000000000000", + "newValue": "0x0069d5306a000000000003000000000000000000000000000000000000000000" + }, + "0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c8": { + "previousValue": "0x000000000000000000093a800000000000006a0354eb00000000000000000000", + "newValue": "0x000000000000000000093a800000000000006a0354eb00000000000069d5306b" + } + } + } + }, + "logs": [ + { + "topics": [ + "0x24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b" + ], + "data": "0x", + "emitter": "0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A" + }, + { + "topics": [ + "0x528c26f4cc05f95dc8bad30284946548f08ec44f7dd536473f28b08c65334cdd", + "0x0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f" + ], + "data": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000069d5306b000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000009657865637574652829000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "emitter": "0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A" + }, + { + "topics": [ + "0xda6084bb0aa902a7f6da10ba185d4aa129414651c90772417eff02a52112af2a" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000001aa", + "emitter": "0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5" + } + ] +} \ No newline at end of file diff --git a/reports/v4_emit_payload_before.json b/reports/v4_emit_payload_before.json new file mode 100644 index 00000000..fe218f14 --- /dev/null +++ b/reports/v4_emit_payload_before.json @@ -0,0 +1,2728 @@ +{ + "chainId": 1, + "hubAssets": { + "0x06002e9c4412CB7814a791eA3666D905871E536A": { + "0": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "PT-sUSDE-7MAY2026", + "underlying": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49" + }, + "1": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "PT-USDe-7MAY2026", + "underlying": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0" + }, + "2": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "sUSDe", + "underlying": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497" + }, + "3": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 2500, + "maxDrawnRate": "3450", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3000, + "rateGrowthBeforeOptimal": 450, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDe", + "underlying": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3" + }, + "4": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 1500, + "maxDrawnRate": "2450", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 2000, + "rateGrowthBeforeOptimal": 450, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "5": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 1000, + "maxDrawnRate": "3450", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3000, + "rateGrowthBeforeOptimal": 450, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + }, + "6": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", + "liquidityFee": 1500, + "maxDrawnRate": "2450", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 2000, + "rateGrowthBeforeOptimal": 450, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + } + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931": { + "0": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, + "1": { + "baseDrawnRate": 0, + "decimals": 8, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "WBTC", + "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + }, + "2": { + "baseDrawnRate": 0, + "decimals": 8, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "cbBTC", + "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" + }, + "3": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "wstETH", + "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + }, + "4": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 1000, + "maxDrawnRate": "2400", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 2000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "5": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 1000, + "maxDrawnRate": "2400", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 2000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "6": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", + "liquidityFee": 1000, + "maxDrawnRate": "3400", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + } + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9": { + "0": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 1500, + "maxDrawnRate": "1635", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 1400, + "rateGrowthBeforeOptimal": 235, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, + "1": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "wstETH", + "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + }, + "10": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 1000, + "maxDrawnRate": "4050", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3500, + "rateGrowthBeforeOptimal": 550, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "EURC", + "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" + }, + "11": { + "baseDrawnRate": 25, + "decimals": 8, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 2000, + "maxDrawnRate": "6425", + "optimalUsageRatio": 8000, + "rateGrowthAfterOptimal": 6000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "WBTC", + "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + }, + "12": { + "baseDrawnRate": 25, + "decimals": 8, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 2000, + "maxDrawnRate": "6425", + "optimalUsageRatio": 8000, + "rateGrowthAfterOptimal": 6000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "cbBTC", + "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" + }, + "13": { + "baseDrawnRate": 0, + "decimals": 8, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "LBTC", + "underlying": "0x8236a87084f8B84306f72007F36F2618A5634494" + }, + "14": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "XAUt", + "underlying": "0x68749665FF8D2d112Fa859AA293F07A622782F38" + }, + "15": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "AAVE", + "underlying": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9" + }, + "16": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "LINK", + "underlying": "0x514910771AF9Ca656af840dff83E8264EcF986CA" + }, + "2": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "weETH", + "underlying": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" + }, + "3": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 0, + "maxDrawnRate": "0", + "optimalUsageRatio": 9900, + "rateGrowthAfterOptimal": 0, + "rateGrowthBeforeOptimal": 0, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "rsETH", + "underlying": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7" + }, + "4": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 1000, + "maxDrawnRate": "2400", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 2000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "5": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 1000, + "maxDrawnRate": "2400", + "optimalUsageRatio": 9200, + "rateGrowthAfterOptimal": 2000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "6": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 1000, + "maxDrawnRate": "3400", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3000, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + }, + "7": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 2000, + "maxDrawnRate": "3900", + "optimalUsageRatio": 8000, + "rateGrowthAfterOptimal": 3500, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "RLUSD", + "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" + }, + "8": { + "baseDrawnRate": 0, + "decimals": 6, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 2000, + "maxDrawnRate": "3900", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3500, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "USDG", + "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" + }, + "9": { + "baseDrawnRate": 0, + "decimals": 18, + "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", + "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", + "liquidityFee": 2000, + "maxDrawnRate": "3900", + "optimalUsageRatio": 9000, + "rateGrowthAfterOptimal": 3500, + "rateGrowthBeforeOptimal": 400, + "reinvestmentController": "0x0000000000000000000000000000000000000000", + "symbol": "frxUSD", + "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" + } + } + }, + "spokeCaps": { + "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0x58131E79531caB1d52301228d1f7b842F26B9649": { + "active": true, + "addCap": "400000", + "assetSymbol": "PT-sUSDE-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0x90774889c22D2F2Adf44da1f04C7c95542590df4": { + "active": true, + "addCap": "0", + "assetSymbol": "PT-sUSDE-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "PT-sUSDE-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "1400000", + "assetSymbol": "PT-sUSDE-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0x58131E79531caB1d52301228d1f7b842F26B9649": { + "active": true, + "addCap": "50000", + "assetSymbol": "PT-USDe-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "PT-USDe-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "250000", + "assetSymbol": "PT-USDe-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0xdd2Eb78BF9e6aC5068B95aD2d451e8c9Af10ac81": { + "active": true, + "addCap": "0", + "assetSymbol": "PT-USDe-7MAY2026", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0x24f8c062e1E0451736C1D6E023510DA262a41df4": { + "active": true, + "addCap": "0", + "assetSymbol": "sUSDe", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0x58131E79531caB1d52301228d1f7b842F26B9649": { + "active": true, + "addCap": "250000", + "assetSymbol": "sUSDe", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "sUSDe", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "375000", + "assetSymbol": "sUSDe", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0x502Cd81da6a8F1785eb2eEE72713B7388E16A854": { + "active": true, + "addCap": "78000", + "assetSymbol": "USDe", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0x58131E79531caB1d52301228d1f7b842F26B9649": { + "active": true, + "addCap": "312500", + "assetSymbol": "USDe", + "drawCap": "325000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDe", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "312500", + "assetSymbol": "USDe", + "drawCap": "300000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_4_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_4_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "150000", + "assetSymbol": "USDC", + "drawCap": "187500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_4_0xc94bdd83D2c7655C280655D60954e79E88D4F949": { + "active": true, + "addCap": "37500", + "assetSymbol": "USDC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_5_0xA54382db40EC602c0a173A08f9E86Ed40F9D4D10": { + "active": true, + "addCap": "125000", + "assetSymbol": "GHO", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_5_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "GHO", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_5_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "500000", + "assetSymbol": "GHO", + "drawCap": "562500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_6_0x80835EB50694EE0e519743f67e5401e6FD300006": { + "active": true, + "addCap": "37500", + "assetSymbol": "USDT", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_6_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDT", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x06002e9c4412CB7814a791eA3666D905871E536A_6_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "150000", + "assetSymbol": "USDT", + "drawCap": "187500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_0_0x2087513383330B961A3753B47627Bbf149F31c70": { + "active": true, + "addCap": "0", + "assetSymbol": "WETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_0_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "130", + "assetSymbol": "WETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_0_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "WETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_1_0x5AE3d87De89CA6Ce501e8317887F71EABED69E18": { + "active": true, + "addCap": "0", + "assetSymbol": "WBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_1_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "6", + "assetSymbol": "WBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_1_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "WBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_2_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "5", + "assetSymbol": "cbBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_2_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "cbBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_2_0xD38098faf52D8E915EdED84fBF30F81C17906938": { + "active": true, + "addCap": "0", + "assetSymbol": "cbBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_3_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "114", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_3_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_3_0xFCD3D3C69cd032DE0cc78fE529B7447D2fe7F666": { + "active": true, + "addCap": "0", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_4_0x486415fb1F8b062c89ED548f871cf64304AACb31": { + "active": true, + "addCap": "37500", + "assetSymbol": "USDC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_4_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "150000", + "assetSymbol": "USDC", + "drawCap": "175000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_4_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_5_0x46c588DD8453aC259c1f6a54b4C9A93C2aC3762D": { + "active": true, + "addCap": "37500", + "assetSymbol": "USDT", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_5_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "150000", + "assetSymbol": "USDT", + "drawCap": "187500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_5_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDT", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_6_0x900fD46d565d1ac8995928c0179052ec02a6D0E1": { + "active": true, + "addCap": "125000", + "assetSymbol": "GHO", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_6_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "500000", + "assetSymbol": "GHO", + "drawCap": "562500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_6_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "GHO", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { + "active": true, + "addCap": "0", + "assetSymbol": "WETH", + "drawCap": "588", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0x7320CF22Ac095bA2a2e0a652F77efB836c2E751b": { + "active": true, + "addCap": "250", + "assetSymbol": "WETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "1500", + "assetSymbol": "WETH", + "drawCap": "130", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "WETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { + "active": true, + "addCap": "0", + "assetSymbol": "WETH", + "drawCap": "530", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { + "active": true, + "addCap": "0", + "assetSymbol": "WETH", + "drawCap": "441", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "EURC", + "drawCap": "50000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x6D9e2Cdd61CaF69af99b275704B6e272C41c6718": { + "active": true, + "addCap": "112500", + "assetSymbol": "EURC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "225000", + "assetSymbol": "EURC", + "drawCap": "150000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "0", + "assetSymbol": "EURC", + "drawCap": "50000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "EURC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "300000", + "assetSymbol": "EURC", + "drawCap": "312500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { + "active": true, + "addCap": "0", + "assetSymbol": "WBTC", + "drawCap": "5", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0x82A9CC4656784E55Ef2E78F704028B5E1Bfc1732": { + "active": true, + "addCap": "0", + "assetSymbol": "WBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "16", + "assetSymbol": "WBTC", + "drawCap": "1", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "WBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0x33B41B74366F55327d959FfF6D6b6fBc2853dbB1": { + "active": true, + "addCap": "0", + "assetSymbol": "cbBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { + "active": true, + "addCap": "0", + "assetSymbol": "cbBTC", + "drawCap": "3", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "13", + "assetSymbol": "cbBTC", + "drawCap": "1", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "cbBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_13_0x7961F140B570490849DB878AE222570ea838799d": { + "active": true, + "addCap": "0", + "assetSymbol": "LBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_13_0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { + "active": true, + "addCap": "9", + "assetSymbol": "LBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_13_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "LBTC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_14_0x4E712562fcb5337011398B6C630f55b60641cd5e": { + "active": true, + "addCap": "0", + "assetSymbol": "XAUt", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_14_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "125", + "assetSymbol": "XAUt", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_14_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "XAUt", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_15_0x0A65197b16C5969F92672051c9C9C0C75B369135": { + "active": true, + "addCap": "0", + "assetSymbol": "AAVE", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_15_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "5000", + "assetSymbol": "AAVE", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_15_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "AAVE", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_16_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "31250", + "assetSymbol": "LINK", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_16_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "LINK", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_16_0xE69C2045095C8Ab3E2a7d77de2328faE5baF797c": { + "active": true, + "addCap": "0", + "assetSymbol": "LINK", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "229", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0xcb0E7dA9c635628f6d4827355AeCa75aB8d3560f": { + "active": true, + "addCap": "0", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { + "active": true, + "addCap": "406", + "assetSymbol": "wstETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0x559cEc2C840D9DBB18936Afc5E5341D78bfC7Cbe": { + "active": true, + "addCap": "0", + "assetSymbol": "weETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "58", + "assetSymbol": "weETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "weETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { + "active": true, + "addCap": "500", + "assetSymbol": "weETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_3_0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { + "active": true, + "addCap": "563", + "assetSymbol": "rsETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_3_0x45a04Ca1A5cbEeA4B44356c75EDd29b33eB2527a": { + "active": true, + "addCap": "0", + "assetSymbol": "rsETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_3_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "rsETH", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x5eC44a70F309854fe04d495cFE1B5dA63DD1cc73": { + "active": true, + "addCap": "312500", + "assetSymbol": "USDT", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "USDT", + "drawCap": "125000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "1250000", + "assetSymbol": "USDT", + "drawCap": "1250000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "0", + "assetSymbol": "USDT", + "drawCap": "125000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDT", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "200000", + "assetSymbol": "USDT", + "drawCap": "50000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "0", + "assetSymbol": "USDT", + "drawCap": "125000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x531E90a2376902DE8915789Fcc1075e3B0c153E7": { + "active": true, + "addCap": "312500", + "assetSymbol": "USDC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "USDC", + "drawCap": "125000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "1250000", + "assetSymbol": "USDC", + "drawCap": "1250000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "0", + "assetSymbol": "USDC", + "drawCap": "125000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDC", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "187500", + "assetSymbol": "USDC", + "drawCap": "50000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "0", + "assetSymbol": "USDC", + "drawCap": "125000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0x58C14a5E061c9bC6926c5b853445290F296C2F7B": { + "active": true, + "addCap": "125000", + "assetSymbol": "GHO", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "GHO", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "500000", + "assetSymbol": "GHO", + "drawCap": "500000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "GHO", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "0", + "assetSymbol": "GHO", + "drawCap": "12500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "RLUSD", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "500000", + "assetSymbol": "RLUSD", + "drawCap": "340000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "RLUSD", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0xC8a125AE4275a78AADc53B46Ca10566Bc9B249E0": { + "active": true, + "addCap": "125000", + "assetSymbol": "RLUSD", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "0", + "assetSymbol": "RLUSD", + "drawCap": "90000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "USDG", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "500000", + "assetSymbol": "USDG", + "drawCap": "340000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0xAC2435E3C25e8246870D33ce0a26988A46d5DB68": { + "active": true, + "addCap": "125000", + "assetSymbol": "USDG", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "USDG", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "0", + "assetSymbol": "USDG", + "drawCap": "90000", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x2226749630775ee20230Ad65214fB339087eF30D": { + "active": true, + "addCap": "125000", + "assetSymbol": "frxUSD", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "active": true, + "addCap": "0", + "assetSymbol": "frxUSD", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "active": true, + "addCap": "500000", + "assetSymbol": "frxUSD", + "drawCap": "312500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "active": true, + "addCap": "0", + "assetSymbol": "frxUSD", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { + "active": true, + "addCap": "1099511627775", + "assetSymbol": "frxUSD", + "drawCap": "0", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "active": true, + "addCap": "0", + "assetSymbol": "frxUSD", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + }, + "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0xba1B3D55D249692b669A164024A838309B7508AF": { + "active": true, + "addCap": "0", + "assetSymbol": "frxUSD", + "drawCap": "62500", + "halted": false, + "riskPremiumThreshold": 0 + } + }, + "spokeLiquidationConfigs": { + "0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1021800000000000000" + }, + "0x58131E79531caB1d52301228d1f7b842F26B9649": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1027700000000000000" + }, + "0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "healthFactorForMaxBonus": "900000000000000000", + "liquidationBonusFactor": 9000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1307500000000000000" + }, + "0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1061500000000000000" + }, + "0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "healthFactorForMaxBonus": "900000000000000000", + "liquidationBonusFactor": 9000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1240000000000000000" + }, + "0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "healthFactorForMaxBonus": "900000000000000000", + "liquidationBonusFactor": 9000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1174000000000000000" + }, + "0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1044200000000000000" + }, + "0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1019100000000000000" + }, + "0xba1B3D55D249692b669A164024A838309B7508AF": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1033200000000000000" + }, + "0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { + "healthFactorForMaxBonus": "990000000000000000", + "liquidationBonusFactor": 10000, + "maxUserReservesLimit": 65535, + "targetHealthFactor": "1013700000000000000" + } + }, + "spokeReserves": { + "0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { + "0": { + "assetId": 3, + "borrowable": false, + "collateralFactor": 9500, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10100, + "oracleAddress": "0x37C316996C714Bf906743071e04E62220b3271ac", + "oraclePrice": "222136416823", + "paused": false, + "priceSource": "0x47F52B2e43D0386cF161e001835b03Ad49889e3b", + "receiveSharesEnabled": true, + "symbol": "rsETH", + "underlying": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7" + }, + "1": { + "assetId": 0, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x37C316996C714Bf906743071e04E62220b3271ac", + "oraclePrice": "207813165287", + "paused": false, + "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", + "receiveSharesEnabled": true, + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + } + }, + "0x58131E79531caB1d52301228d1f7b842F26B9649": { + "0": { + "assetId": 1, + "borrowable": false, + "collateralFactor": 9580, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10200, + "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", + "oraclePrice": "99689155", + "paused": false, + "priceSource": "0x0a72df02CE3E4185b6CEDf561f0AE651E9BeE235", + "receiveSharesEnabled": true, + "symbol": "PT-USDe-7MAY2026", + "underlying": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0" + }, + "1": { + "assetId": 0, + "borrowable": false, + "collateralFactor": 9400, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10300, + "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", + "oraclePrice": "99683533", + "paused": false, + "priceSource": "0xa0dc0249c32fa79e8B9b17c735908a60b1141B40", + "receiveSharesEnabled": true, + "symbol": "PT-sUSDE-7MAY2026", + "underlying": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49" + }, + "2": { + "assetId": 2, + "borrowable": false, + "collateralFactor": 9200, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10300, + "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", + "oraclePrice": "122650671", + "paused": false, + "priceSource": "0x42bc86f2f08419280a99d8fbEa4672e7c30a86ec", + "receiveSharesEnabled": true, + "symbol": "sUSDe", + "underlying": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497" + }, + "3": { + "assetId": 3, + "borrowable": true, + "collateralFactor": 9300, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10200, + "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0xC26D4a1c46d884cfF6dE9800B6aE7A8Cf48B4Ff8", + "receiveSharesEnabled": true, + "symbol": "USDe", + "underlying": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3" + } + }, + "0x65407b940966954b23dfA3caA5C0702bB42984DC": { + "0": { + "assetId": 14, + "borrowable": false, + "collateralFactor": 7500, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10666, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "465498700000", + "paused": false, + "priceSource": "0x214eD9Da11D2fbe465a6fc601a91E62EbEc1a0D6", + "receiveSharesEnabled": true, + "symbol": "XAUt", + "underlying": "0x68749665FF8D2d112Fa859AA293F07A622782F38" + }, + "1": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "2": { + "assetId": 7, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "100001110", + "paused": false, + "priceSource": "0xf0eaC18E908B34770FDEe46d069c846bDa866759", + "receiveSharesEnabled": true, + "symbol": "RLUSD", + "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" + }, + "3": { + "assetId": 8, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xF29b1e3b68Fd59DD0a413811fD5d0AbaE653216d", + "receiveSharesEnabled": true, + "symbol": "USDG", + "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" + }, + "4": { + "assetId": 9, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "99992090", + "paused": false, + "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", + "receiveSharesEnabled": true, + "symbol": "frxUSD", + "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" + }, + "5": { + "assetId": 10, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "115424199", + "paused": false, + "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", + "receiveSharesEnabled": true, + "symbol": "EURC", + "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" + }, + "6": { + "assetId": 6, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", + "receiveSharesEnabled": true, + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + }, + "7": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + } + }, + "0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { + "0": { + "assetId": 13, + "borrowable": false, + "collateralFactor": 8600, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10300, + "oracleAddress": "0x198Cac7f54FFc7d709Ac0FEc4B6454CE73e21D3D", + "oraclePrice": "6841743291729", + "paused": false, + "priceSource": "0x5C1771583dbbAE5AFEd71ACD2BfC0eA4029EBB04", + "receiveSharesEnabled": true, + "symbol": "LBTC", + "underlying": "0x8236a87084f8B84306f72007F36F2618A5634494" + }, + "1": { + "assetId": 11, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x198Cac7f54FFc7d709Ac0FEc4B6454CE73e21D3D", + "oraclePrice": "6817396697793", + "paused": false, + "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", + "receiveSharesEnabled": true, + "symbol": "WBTC", + "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + }, + "2": { + "assetId": 12, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x198Cac7f54FFc7d709Ac0FEc4B6454CE73e21D3D", + "oraclePrice": "6817396697793", + "paused": false, + "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", + "receiveSharesEnabled": true, + "symbol": "cbBTC", + "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" + } + }, + "0x94e7A5dCbE816e498b89aB752661904E2F56c485": { + "0": { + "assetId": 0, + "borrowable": true, + "collateralFactor": 8300, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10555, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "207813165287", + "paused": false, + "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", + "receiveSharesEnabled": true, + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, + "1": { + "assetId": 1, + "borrowable": false, + "collateralFactor": 8000, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10666, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "255881582629", + "paused": false, + "priceSource": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C", + "receiveSharesEnabled": true, + "symbol": "wstETH", + "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + }, + "10": { + "assetId": 7, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "100001110", + "paused": false, + "priceSource": "0xf0eaC18E908B34770FDEe46d069c846bDa866759", + "receiveSharesEnabled": true, + "symbol": "RLUSD", + "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" + }, + "11": { + "assetId": 8, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xF29b1e3b68Fd59DD0a413811fD5d0AbaE653216d", + "receiveSharesEnabled": true, + "symbol": "USDG", + "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" + }, + "12": { + "assetId": 9, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "99992090", + "paused": false, + "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", + "receiveSharesEnabled": true, + "symbol": "frxUSD", + "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" + }, + "13": { + "assetId": 6, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", + "receiveSharesEnabled": true, + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + }, + "2": { + "assetId": 2, + "borrowable": false, + "collateralFactor": 8000, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10777, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "226950761571", + "paused": false, + "priceSource": "0xf112aF6F0A332B815fbEf3Ff932c057E570b62d3", + "receiveSharesEnabled": true, + "symbol": "weETH", + "underlying": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" + }, + "3": { + "assetId": 11, + "borrowable": true, + "collateralFactor": 7800, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10555, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "6817396697793", + "paused": false, + "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", + "receiveSharesEnabled": true, + "symbol": "WBTC", + "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + }, + "4": { + "assetId": 12, + "borrowable": true, + "collateralFactor": 7800, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10555, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "6817396697793", + "paused": false, + "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", + "receiveSharesEnabled": true, + "symbol": "cbBTC", + "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" + }, + "5": { + "assetId": 15, + "borrowable": false, + "collateralFactor": 7600, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10833, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "8613353000", + "paused": false, + "priceSource": "0x547a514d5e3769680Ce22B2361c10Ea13619e8a9", + "receiveSharesEnabled": true, + "symbol": "AAVE", + "underlying": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9" + }, + "6": { + "assetId": 16, + "borrowable": false, + "collateralFactor": 7100, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10777, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "863312586", + "paused": false, + "priceSource": "0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c", + "receiveSharesEnabled": true, + "symbol": "LINK", + "underlying": "0x514910771AF9Ca656af840dff83E8264EcF986CA" + }, + "7": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 7800, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10500, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "8": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 7800, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10500, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "9": { + "assetId": 10, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", + "oraclePrice": "115424199", + "paused": false, + "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", + "receiveSharesEnabled": true, + "symbol": "EURC", + "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" + } + }, + "0x973a023A77420ba610f06b3858aD991Df6d85A08": { + "0": { + "assetId": 0, + "borrowable": false, + "collateralFactor": 8600, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 1000, + "maxLiquidationBonus": 10444, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "207813165287", + "paused": false, + "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", + "receiveSharesEnabled": true, + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, + "1": { + "assetId": 1, + "borrowable": false, + "collateralFactor": 8450, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 1000, + "maxLiquidationBonus": 10444, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "6817396697793", + "paused": false, + "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", + "receiveSharesEnabled": true, + "symbol": "WBTC", + "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + }, + "10": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "2": { + "assetId": 2, + "borrowable": false, + "collateralFactor": 8450, + "collateralRisk": 0, + "decimals": 8, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 1000, + "maxLiquidationBonus": 10444, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "6817396697793", + "paused": false, + "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", + "receiveSharesEnabled": true, + "symbol": "cbBTC", + "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" + }, + "3": { + "assetId": 3, + "borrowable": false, + "collateralFactor": 8550, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 1000, + "maxLiquidationBonus": 10444, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "255881582629", + "paused": false, + "priceSource": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C", + "receiveSharesEnabled": true, + "symbol": "wstETH", + "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + }, + "4": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "5": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "6": { + "assetId": 6, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", + "receiveSharesEnabled": true, + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + }, + "7": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "8": { + "assetId": 9, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "99992090", + "paused": false, + "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", + "receiveSharesEnabled": true, + "symbol": "frxUSD", + "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" + }, + "9": { + "assetId": 10, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", + "oraclePrice": "115424199", + "paused": false, + "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", + "receiveSharesEnabled": true, + "symbol": "EURC", + "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" + } + }, + "0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { + "0": { + "assetId": 10, + "borrowable": true, + "collateralFactor": 9000, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10200, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "115424199", + "paused": false, + "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", + "receiveSharesEnabled": true, + "symbol": "EURC", + "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" + }, + "1": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 9000, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10200, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "2": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 9000, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10200, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "3": { + "assetId": 7, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "100001110", + "paused": false, + "priceSource": "0xf0eaC18E908B34770FDEe46d069c846bDa866759", + "receiveSharesEnabled": true, + "symbol": "RLUSD", + "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" + }, + "4": { + "assetId": 8, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xF29b1e3b68Fd59DD0a413811fD5d0AbaE653216d", + "receiveSharesEnabled": true, + "symbol": "USDG", + "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" + }, + "5": { + "assetId": 9, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "99992090", + "paused": false, + "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", + "receiveSharesEnabled": true, + "symbol": "frxUSD", + "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" + }, + "6": { + "assetId": 6, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", + "receiveSharesEnabled": true, + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + } + }, + "0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { + "0": { + "assetId": 2, + "borrowable": false, + "collateralFactor": 9550, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10100, + "oracleAddress": "0xd8B153FaAA8f2b1bC774916FEd333A4F3dE48792", + "oraclePrice": "226950761571", + "paused": false, + "priceSource": "0xf112aF6F0A332B815fbEf3Ff932c057E570b62d3", + "receiveSharesEnabled": true, + "symbol": "weETH", + "underlying": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" + }, + "1": { + "assetId": 0, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xd8B153FaAA8f2b1bC774916FEd333A4F3dE48792", + "oraclePrice": "207813165287", + "paused": false, + "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", + "receiveSharesEnabled": true, + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + } + }, + "0xba1B3D55D249692b669A164024A838309B7508AF": { + "0": { + "assetId": 1, + "borrowable": false, + "collateralFactor": 9300, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10300, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "99689155", + "paused": false, + "priceSource": "0x0a72df02CE3E4185b6CEDf561f0AE651E9BeE235", + "receiveSharesEnabled": true, + "symbol": "PT-USDe-7MAY2026", + "underlying": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0" + }, + "1": { + "assetId": 0, + "borrowable": false, + "collateralFactor": 9200, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10400, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "99683533", + "paused": false, + "priceSource": "0xa0dc0249c32fa79e8B9b17c735908a60b1141B40", + "receiveSharesEnabled": true, + "symbol": "PT-sUSDE-7MAY2026", + "underlying": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49" + }, + "2": { + "assetId": 2, + "borrowable": false, + "collateralFactor": 9200, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10300, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "122650671", + "paused": false, + "priceSource": "0x42bc86f2f08419280a99d8fbEa4672e7c30a86ec", + "receiveSharesEnabled": true, + "symbol": "sUSDe", + "underlying": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497" + }, + "3": { + "assetId": 3, + "borrowable": true, + "collateralFactor": 9300, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 1000, + "maxLiquidationBonus": 10200, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0xC26D4a1c46d884cfF6dE9800B6aE7A8Cf48B4Ff8", + "receiveSharesEnabled": true, + "symbol": "USDe", + "underlying": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3" + }, + "4": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "5": { + "assetId": 6, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "6": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "100000000", + "paused": false, + "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", + "receiveSharesEnabled": true, + "symbol": "GHO", + "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + }, + "7": { + "assetId": 5, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "99986000", + "paused": false, + "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", + "receiveSharesEnabled": true, + "symbol": "USDC", + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "8": { + "assetId": 9, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "99992090", + "paused": false, + "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", + "receiveSharesEnabled": true, + "symbol": "frxUSD", + "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" + }, + "9": { + "assetId": 4, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 6, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", + "oraclePrice": "100006413", + "paused": false, + "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", + "receiveSharesEnabled": true, + "symbol": "USDT", + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + } + }, + "0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { + "0": { + "assetId": 1, + "borrowable": false, + "collateralFactor": 9550, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 1000, + "maxLiquidationBonus": 10100, + "oracleAddress": "0x664D73b6C3591333Fd79510f7ce9ef81228824F5", + "oraclePrice": "255881582629", + "paused": false, + "priceSource": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C", + "receiveSharesEnabled": true, + "symbol": "wstETH", + "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + }, + "1": { + "assetId": 0, + "borrowable": true, + "collateralFactor": 0, + "collateralRisk": 0, + "decimals": 18, + "dynamicConfigKey": 0, + "frozen": false, + "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", + "liquidationFee": 0, + "maxLiquidationBonus": 10000, + "oracleAddress": "0x664D73b6C3591333Fd79510f7ce9ef81228824F5", + "oraclePrice": "207813165287", + "paused": false, + "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", + "receiveSharesEnabled": true, + "symbol": "WETH", + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + } + } + } +} \ No newline at end of file diff --git a/src/ProtocolV4TestBase.sol b/src/ProtocolV4TestBase.sol index e02f1212..c1eae45b 100644 --- a/src/ProtocolV4TestBase.sol +++ b/src/ProtocolV4TestBase.sol @@ -96,7 +96,7 @@ contract ProtocolV4TestBase is SnapshotV4, Scenarios, TokenizationScenarios, Gat vm.writeJson(rawDiff, afterPath, '$.raw'); vm.writeJson(logsJson, afterPath, '$.logs'); - diffV4Snapshots(reportName, snapshotBefore, snapshotAfter); + diffV4Snapshots(reportName); } function _executePayloadWithRecording( diff --git a/src/dependencies/v4/GatewayScenarios.sol b/src/dependencies/v4/GatewayScenarios.sol index 8c332eb7..844d665f 100644 --- a/src/dependencies/v4/GatewayScenarios.sol +++ b/src/dependencies/v4/GatewayScenarios.sol @@ -745,7 +745,7 @@ abstract contract GatewayScenarios is Helpers { }); // Ensure liquidity + borrow + repay - _ensureLiquidity({spoke: spoke, reserveInfo: reserveInfo, amount: borrowAmount}); + _ensureLiquidity(spoke, reserveInfo, borrowAmount); _sigBorrow({ gateway: gateway, spoke: spoke, diff --git a/src/dependencies/v4/SnapshotV4.sol b/src/dependencies/v4/SnapshotV4.sol index e7482cb5..634b9f00 100644 --- a/src/dependencies/v4/SnapshotV4.sol +++ b/src/dependencies/v4/SnapshotV4.sol @@ -9,7 +9,7 @@ import {V4DiffWriter} from 'src/dependencies/v4/V4DiffWriter.sol'; import {Helpers} from 'src/dependencies/v4/Helpers.sol'; /// @title SnapshotV4 -/// @notice Snapshot capture for Aave V4. JSON + markdown diff delegated to V4DiffWriter. +/// @notice Snapshot capture for Aave V4. JSON serialization via V4DiffWriter, diff via TypeScript FFI. abstract contract SnapshotV4 is Helpers { /// @notice Capture a full V4 configuration snapshot from the given spokes and hubs. function createV4Snapshot( @@ -27,13 +27,27 @@ abstract contract SnapshotV4 is Helpers { V4DiffWriter.writeSnapshotJson(name, snap); } - /// @notice Generate markdown diff between two snapshots. - function diffV4Snapshots( - string memory reportName, - Types.V4Snapshot memory snapBefore, - Types.V4Snapshot memory snapAfter - ) internal { - V4DiffWriter.writeDiff(reportName, snapBefore, snapAfter); + /// @notice Generate markdown diff between two snapshots via TypeScript CLI (FFI). + function diffV4Snapshots(string memory reportName) internal { + string memory beforePath = string.concat('./reports/', reportName, '_before.json'); + string memory afterPath = string.concat('./reports/', reportName, '_after.json'); + string memory outPath = string.concat( + './diffs/', + reportName, + '_before_', + reportName, + '_after.md' + ); + + string[] memory inputs = new string[](7); + inputs[0] = 'npx'; + inputs[1] = '@aave-dao/aave-helpers-js@^1.0.1'; + inputs[2] = 'diff-v4-snapshots'; + inputs[3] = beforePath; + inputs[4] = afterPath; + inputs[5] = '-o'; + inputs[6] = outPath; + vm.ffi(inputs); } // Spoke reserves @@ -85,12 +99,8 @@ abstract contract SnapshotV4 is Helpers { address oracleAddr = spoke.ORACLE(); snap.oracleAddress = oracleAddr; - try IAaveOracle(oracleAddr).getReserveSource(reserveId) returns (address src) { - snap.priceSource = src; - } catch {} - try IAaveOracle(oracleAddr).getReservePrice(reserveId) returns (uint256 price) { - snap.oraclePrice = price; - } catch {} + snap.priceSource = IAaveOracle(oracleAddr).getReserveSource(reserveId); + snap.oraclePrice = IAaveOracle(oracleAddr).getReservePrice(reserveId); } // Spoke liquidation configs @@ -151,19 +161,14 @@ abstract contract SnapshotV4 is Helpers { snap.reinvestmentController = config.reinvestmentController; if (config.irStrategy != address(0)) { - try IAssetInterestRateStrategy(config.irStrategy).getInterestRateData(assetId) returns ( - IAssetInterestRateStrategy.InterestRateData memory irData - ) { - snap.optimalUsageRatio = irData.optimalUsageRatio; - snap.baseDrawnRate = irData.baseDrawnRate; - snap.rateGrowthBeforeOptimal = irData.rateGrowthBeforeOptimal; - snap.rateGrowthAfterOptimal = irData.rateGrowthAfterOptimal; - } catch {} - try IAssetInterestRateStrategy(config.irStrategy).getMaxDrawnRate(assetId) returns ( - uint256 rate - ) { - snap.maxDrawnRate = rate; - } catch {} + IAssetInterestRateStrategy.InterestRateData memory irData = IAssetInterestRateStrategy( + config.irStrategy + ).getInterestRateData(assetId); + snap.optimalUsageRatio = irData.optimalUsageRatio; + snap.baseDrawnRate = irData.baseDrawnRate; + snap.rateGrowthBeforeOptimal = irData.rateGrowthBeforeOptimal; + snap.rateGrowthAfterOptimal = irData.rateGrowthAfterOptimal; + snap.maxDrawnRate = IAssetInterestRateStrategy(config.irStrategy).getMaxDrawnRate(assetId); } } diff --git a/src/dependencies/v4/V4DiffWriter.sol b/src/dependencies/v4/V4DiffWriter.sol index b73393d7..8c5194fd 100644 --- a/src/dependencies/v4/V4DiffWriter.sol +++ b/src/dependencies/v4/V4DiffWriter.sol @@ -5,7 +5,8 @@ import {Vm} from 'forge-std/Vm.sol'; import {Types} from 'src/dependencies/v4/Types.sol'; /// @title V4DiffWriter -/// @notice Internal library for V4 JSON serialization and markdown diff generation. +/// @notice Internal library for V4 JSON serialization. +/// Markdown diff generation is handled by the TypeScript CLI (aave-helpers-js). /// Using an internal library means functions are inlined via delegatecall context, /// keeping cheatcodes working while avoiding stack-too-deep in the inheritance chain. library V4DiffWriter { @@ -181,418 +182,4 @@ library V4DiffWriter { } vm.writeJson(vm.serializeString('root', 'spokeCaps', content), path); } - - function writeDiff( - string memory reportName, - Types.V4Snapshot memory snapBefore, - Types.V4Snapshot memory snapAfter - ) internal { - string memory md = ''; - md = string.concat(md, _diffSpokeReserves(snapBefore.spokeReserves, snapAfter.spokeReserves)); - md = string.concat(md, _diffHubAssets(snapBefore.hubAssets, snapAfter.hubAssets)); - md = string.concat(md, _diffSpokeCaps(snapBefore.spokeCaps, snapAfter.spokeCaps)); - md = string.concat( - md, - _diffSpokeLiq(snapBefore.spokeLiquidationConfigs, snapAfter.spokeLiquidationConfigs) - ); - - if (bytes(md).length == 0) md = 'No configuration changes detected.\n'; - - vm.writeFile(string.concat('./diffs/', reportName, '_before_', reportName, '_after.md'), md); - } - - function _diffSpokeReserves( - Types.SpokeReserveSnapshot[] memory arrB, - Types.SpokeReserveSnapshot[] memory arrA - ) private pure returns (string memory section) { - string memory body = ''; - for (uint256 i; i < arrA.length; i++) { - (bool found, uint256 bi) = _findRes(arrB, arrA[i].spokeAddress, arrA[i].reserveId); - if (found) { - string memory rows = _cmpRes(arrB[bi], arrA[i]); - if (bytes(rows).length > 0) { - body = string.concat(body, _resHdr(arrA[i]), _header(), rows, '\n'); - } - } else { - body = string.concat(body, _newRes(arrA[i])); - } - } - for (uint256 i; i < arrB.length; i++) { - (bool f, ) = _findRes(arrA, arrB[i].spokeAddress, arrB[i].reserveId); - if (!f) body = string.concat(body, _resHdr(arrB[i]), '**REMOVED**\n\n'); - } - if (bytes(body).length > 0) section = string.concat('## Spoke Reserve Changes\n\n', body); - } - - function _resHdr(Types.SpokeReserveSnapshot memory r) private pure returns (string memory) { - return - string.concat( - '### ', - r.symbol, - ' (', - vm.toString(r.underlying), - ') on Spoke ', - vm.toString(r.spokeAddress), - ' [reserveId: ', - vm.toString(r.reserveId), - ']\n\n' - ); - } - - function _cmpRes( - Types.SpokeReserveSnapshot memory b, - Types.SpokeReserveSnapshot memory a - ) private pure returns (string memory rows) { - rows = string.concat( - _dU('collateralRisk', b.collateralRisk, a.collateralRisk), - _dB('paused', b.paused, a.paused), - _dB('frozen', b.frozen, a.frozen), - _dB('borrowable', b.borrowable, a.borrowable), - _dB('receiveSharesEnabled', b.receiveSharesEnabled, a.receiveSharesEnabled), - _dU('dynamicConfigKey', b.dynamicConfigKey, a.dynamicConfigKey) - ); - rows = string.concat( - rows, - _dP('collateralFactor', b.collateralFactor, a.collateralFactor), - _dU('maxLiquidationBonus', b.maxLiquidationBonus, a.maxLiquidationBonus), - _dP('liquidationFee', b.liquidationFee, a.liquidationFee), - _dA('priceSource', b.priceSource, a.priceSource), - _dU('oraclePrice', b.oraclePrice, a.oraclePrice) - ); - } - - function _newRes(Types.SpokeReserveSnapshot memory r) private pure returns (string memory) { - string memory p1 = string.concat( - _resHdr(r), - '**NEW RESERVE**\n\n', - _header(), - _row('collateralRisk', vm.toString(uint256(r.collateralRisk))), - _row('paused', _bs(r.paused)), - _row('frozen', _bs(r.frozen)), - _row('borrowable', _bs(r.borrowable)), - _row('receiveSharesEnabled', _bs(r.receiveSharesEnabled)) - ); - return - string.concat( - p1, - _row('collateralFactor', _ps(r.collateralFactor)), - _row('maxLiquidationBonus', vm.toString(uint256(r.maxLiquidationBonus))), - _row('liquidationFee', _ps(r.liquidationFee)), - _row('priceSource', vm.toString(r.priceSource)), - _row('oraclePrice', vm.toString(r.oraclePrice)), - '\n' - ); - } - - function _findRes( - Types.SpokeReserveSnapshot[] memory a, - address s, - uint256 id - ) private pure returns (bool, uint256) { - for (uint256 i; i < a.length; i++) { - if (a[i].spokeAddress == s && a[i].reserveId == id) return (true, i); - } - return (false, 0); - } - - function _diffHubAssets( - Types.HubAssetSnapshot[] memory arrB, - Types.HubAssetSnapshot[] memory arrA - ) private pure returns (string memory section) { - string memory body = ''; - for (uint256 i; i < arrA.length; i++) { - (bool found, uint256 bi) = _findHA(arrB, arrA[i].hubAddress, arrA[i].assetId); - if (found) { - string memory rows = _cmpHA(arrB[bi], arrA[i]); - if (bytes(rows).length > 0) { - body = string.concat(body, _haHdr(arrA[i]), _header(), rows, '\n'); - } - } else { - body = string.concat(body, _newHA(arrA[i])); - } - } - for (uint256 i; i < arrB.length; i++) { - (bool f, ) = _findHA(arrA, arrB[i].hubAddress, arrB[i].assetId); - if (!f) body = string.concat(body, _haHdr(arrB[i]), '**REMOVED**\n\n'); - } - if (bytes(body).length > 0) section = string.concat('## Hub Asset Changes\n\n', body); - } - - function _haHdr(Types.HubAssetSnapshot memory a) private pure returns (string memory) { - return - string.concat( - '### ', - a.symbol, - ' (assetId: ', - vm.toString(a.assetId), - ') on Hub ', - vm.toString(a.hubAddress), - '\n\n' - ); - } - - function _cmpHA( - Types.HubAssetSnapshot memory b, - Types.HubAssetSnapshot memory a - ) private pure returns (string memory rows) { - rows = string.concat( - _dP('liquidityFee', b.liquidityFee, a.liquidityFee), - _dA('irStrategy', b.irStrategy, a.irStrategy), - _dA('feeReceiver', b.feeReceiver, a.feeReceiver), - _dA('reinvestmentController', b.reinvestmentController, a.reinvestmentController), - _dP('optimalUsageRatio', b.optimalUsageRatio, a.optimalUsageRatio) - ); - rows = string.concat( - rows, - _dP('baseDrawnRate', uint256(b.baseDrawnRate), uint256(a.baseDrawnRate)), - _dP( - 'rateGrowthBeforeOptimal', - uint256(b.rateGrowthBeforeOptimal), - uint256(a.rateGrowthBeforeOptimal) - ), - _dP( - 'rateGrowthAfterOptimal', - uint256(b.rateGrowthAfterOptimal), - uint256(a.rateGrowthAfterOptimal) - ), - _dP('maxDrawnRate', b.maxDrawnRate, a.maxDrawnRate) - ); - } - - function _newHA(Types.HubAssetSnapshot memory a) private pure returns (string memory) { - string memory p1 = string.concat( - _haHdr(a), - '**NEW ASSET**\n\n', - _header(), - _row('liquidityFee', _ps(a.liquidityFee)), - _row('irStrategy', vm.toString(a.irStrategy)), - _row('feeReceiver', vm.toString(a.feeReceiver)), - _row('reinvestmentController', vm.toString(a.reinvestmentController)) - ); - return - string.concat( - p1, - _row('optimalUsageRatio', _ps(a.optimalUsageRatio)), - _row('baseDrawnRate', _ps(uint256(a.baseDrawnRate))), - _row('rateGrowthBeforeOptimal', _ps(uint256(a.rateGrowthBeforeOptimal))), - _row('rateGrowthAfterOptimal', _ps(uint256(a.rateGrowthAfterOptimal))), - _row('maxDrawnRate', _ps(a.maxDrawnRate)), - '\n' - ); - } - - function _findHA( - Types.HubAssetSnapshot[] memory a, - address h, - uint256 id - ) private pure returns (bool, uint256) { - for (uint256 i; i < a.length; i++) { - if (a[i].hubAddress == h && a[i].assetId == id) return (true, i); - } - return (false, 0); - } - - // --- hub spoke caps diff --- - - function _diffSpokeCaps( - Types.SpokeCapSnapshot[] memory arrB, - Types.SpokeCapSnapshot[] memory arrA - ) private pure returns (string memory section) { - string memory body = ''; - for (uint256 i; i < arrA.length; i++) { - (bool found, uint256 bi) = _findSC( - arrB, - arrA[i].hubAddress, - arrA[i].assetId, - arrA[i].spokeAddress - ); - if (found) { - string memory rows = _cmpSC(arrB[bi], arrA[i]); - if (bytes(rows).length > 0) { - body = string.concat(body, _scHdr(arrA[i]), _header(), rows, '\n'); - } - } else { - body = string.concat(body, _newSC(arrA[i])); - } - } - for (uint256 i; i < arrB.length; i++) { - (bool f, ) = _findSC(arrA, arrB[i].hubAddress, arrB[i].assetId, arrB[i].spokeAddress); - if (!f) body = string.concat(body, _scHdr(arrB[i]), '**REMOVED**\n\n'); - } - if (bytes(body).length > 0) section = string.concat('## Hub Spoke Cap Changes\n\n', body); - } - - function _scHdr(Types.SpokeCapSnapshot memory c) private pure returns (string memory) { - return - string.concat( - '### ', - c.assetSymbol, - ' (assetId: ', - vm.toString(c.assetId), - ') on Hub ', - vm.toString(c.hubAddress), - ' / Spoke ', - vm.toString(c.spokeAddress), - '\n\n' - ); - } - - function _cmpSC( - Types.SpokeCapSnapshot memory b, - Types.SpokeCapSnapshot memory a - ) private pure returns (string memory) { - return - string.concat( - _dU('addCap', uint256(b.addCap), uint256(a.addCap)), - _dU('drawCap', uint256(b.drawCap), uint256(a.drawCap)), - _dU( - 'riskPremiumThreshold', - uint256(b.riskPremiumThreshold), - uint256(a.riskPremiumThreshold) - ), - _dB('active', b.active, a.active), - _dB('halted', b.halted, a.halted) - ); - } - - function _newSC(Types.SpokeCapSnapshot memory c) private pure returns (string memory) { - return - string.concat( - _scHdr(c), - '**NEW SPOKE**\n\n', - _header(), - _row('addCap', vm.toString(uint256(c.addCap))), - _row('drawCap', vm.toString(uint256(c.drawCap))), - _row('riskPremiumThreshold', vm.toString(uint256(c.riskPremiumThreshold))), - _row('active', _bs(c.active)), - _row('halted', _bs(c.halted)), - '\n' - ); - } - - function _findSC( - Types.SpokeCapSnapshot[] memory a, - address h, - uint256 id, - address s - ) private pure returns (bool, uint256) { - for (uint256 i; i < a.length; i++) { - if (a[i].hubAddress == h && a[i].assetId == id && a[i].spokeAddress == s) return (true, i); - } - return (false, 0); - } - - function _diffSpokeLiq( - Types.SpokeLiquidationSnapshot[] memory arrB, - Types.SpokeLiquidationSnapshot[] memory arrA - ) private pure returns (string memory section) { - string memory body = ''; - for (uint256 i; i < arrA.length; i++) { - (bool found, uint256 bi) = _findSL(arrB, arrA[i].spokeAddress); - if (found) { - string memory rows = _cmpSL(arrB[bi], arrA[i]); - if (bytes(rows).length > 0) { - body = string.concat( - body, - '### Spoke ', - vm.toString(arrA[i].spokeAddress), - '\n\n', - _header(), - rows, - '\n' - ); - } - } else { - body = string.concat( - body, - '### Spoke ', - vm.toString(arrA[i].spokeAddress), - ' **NEW**\n\n', - _header(), - _row('targetHealthFactor', vm.toString(uint256(arrA[i].targetHealthFactor))), - _row('healthFactorForMaxBonus', vm.toString(uint256(arrA[i].healthFactorForMaxBonus))), - _row('liquidationBonusFactor', vm.toString(uint256(arrA[i].liquidationBonusFactor))), - _row('maxUserReservesLimit', vm.toString(uint256(arrA[i].maxUserReservesLimit))), - '\n' - ); - } - } - if (bytes(body).length > 0) { - section = string.concat('## Spoke Liquidation Config Changes\n\n', body); - } - } - - function _cmpSL( - Types.SpokeLiquidationSnapshot memory b, - Types.SpokeLiquidationSnapshot memory a - ) private pure returns (string memory) { - return - string.concat( - _dU('targetHealthFactor', uint256(b.targetHealthFactor), uint256(a.targetHealthFactor)), - _dU( - 'healthFactorForMaxBonus', - uint256(b.healthFactorForMaxBonus), - uint256(a.healthFactorForMaxBonus) - ), - _dU( - 'liquidationBonusFactor', - uint256(b.liquidationBonusFactor), - uint256(a.liquidationBonusFactor) - ), - _dU( - 'maxUserReservesLimit', - uint256(b.maxUserReservesLimit), - uint256(a.maxUserReservesLimit) - ) - ); - } - - function _findSL( - Types.SpokeLiquidationSnapshot[] memory a, - address s - ) private pure returns (bool, uint256) { - for (uint256 i; i < a.length; i++) { - if (a[i].spokeAddress == s) return (true, i); - } - return (false, 0); - } - - function _header() private pure returns (string memory) { - return '| description | value before | value after |\n| --- | --- | --- |\n'; - } - - function _row(string memory n, string memory v) private pure returns (string memory) { - return string.concat('| ', n, ' | - | ', v, ' |\n'); - } - - function _dU(string memory n, uint256 b, uint256 a) private pure returns (string memory) { - if (b == a) return ''; - return string.concat('| ', n, ' | ', vm.toString(b), ' | ', vm.toString(a), ' |\n'); - } - - function _dP(string memory n, uint256 b, uint256 a) private pure returns (string memory) { - if (b == a) return ''; - return string.concat('| ', n, ' | ', _ps(b), ' | ', _ps(a), ' |\n'); - } - - function _dB(string memory n, bool b, bool a) private pure returns (string memory) { - if (b == a) return ''; - return string.concat('| ', n, ' | ', _bs(b), ' | ', _bs(a), ' |\n'); - } - - function _dA(string memory n, address b, address a) private pure returns (string memory) { - if (b == a) return ''; - return string.concat('| ', n, ' | ', vm.toString(b), ' | ', vm.toString(a), ' |\n'); - } - - function _bs(bool v) private pure returns (string memory) { - return v ? 'true' : 'false'; - } - - function _ps(uint256 bps) private pure returns (string memory) { - uint256 w = bps / 100; - uint256 f = bps % 100; - string memory fs = f < 10 ? string.concat('0', vm.toString(f)) : vm.toString(f); - return string.concat(vm.toString(w), '.', fs, ' % [', vm.toString(bps), ']'); - } } diff --git a/tests/ProtocolV4TestBase.t.sol b/tests/ProtocolV4TestBase.t.sol index 9caa2d44..6ef09d08 100644 --- a/tests/ProtocolV4TestBase.t.sol +++ b/tests/ProtocolV4TestBase.t.sol @@ -54,22 +54,12 @@ contract ProtocolV4TestBaseTest is ProtocolV4TestBase { function _cleanupArtifacts(string memory reportName) internal { string memory beforePath = string.concat('./reports/', reportName, '_before.json'); string memory afterPath = string.concat('./reports/', reportName, '_after.json'); - string memory diffPath = string.concat( - './diffs/', - reportName, - '_before_', - reportName, - '_after.md' - ); if (vm.exists(beforePath)) { vm.removeFile(beforePath); } if (vm.exists(afterPath)) { vm.removeFile(afterPath); } - if (vm.exists(diffPath)) { - vm.removeFile(diffPath); - } } } From ad0c6c9cb793812e66a07a94bf5605f7a31d8ead Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:14:39 -0500 Subject: [PATCH 04/29] fix: gitignore cleanup; add more ts tests --- .gitignore | 3 + ...it_payload_before_v4_emit_payload_after.md | 30 - diffs/v4_no_e2e_before_v4_no_e2e_after.md | 30 - ...orage_pass_before_v4_storage_pass_after.md | 30 - .../__tests__/protocol-diff-v4.spec.ts | 215 +- packages/aave-helpers-js/formatters-v4.ts | 30 +- .../aave-helpers-js/sections/hub-assets.ts | 5 + packages/aave-helpers-js/snapshot-types-v4.ts | 5 + reports/v4_emit_payload_after.json | 2770 ----------------- reports/v4_emit_payload_before.json | 2728 ---------------- src/dependencies/v4/SnapshotV4.sol | 7 + src/dependencies/v4/Types.sol | 5 + src/dependencies/v4/V4DiffWriter.sol | 7 +- 13 files changed, 263 insertions(+), 5602 deletions(-) delete mode 100644 diffs/v4_emit_payload_before_v4_emit_payload_after.md delete mode 100644 diffs/v4_no_e2e_before_v4_no_e2e_after.md delete mode 100644 diffs/v4_storage_pass_before_v4_storage_pass_after.md delete mode 100644 reports/v4_emit_payload_after.json delete mode 100644 reports/v4_emit_payload_before.json diff --git a/.gitignore b/.gitignore index dfed2b22..68686938 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ yarn-error.log .vscode/ dist + +reports/v4* +diffs/v4* diff --git a/diffs/v4_emit_payload_before_v4_emit_payload_after.md b/diffs/v4_emit_payload_before_v4_emit_payload_after.md deleted file mode 100644 index 318d7b79..00000000 --- a/diffs/v4_emit_payload_before_v4_emit_payload_after.md +++ /dev/null @@ -1,30 +0,0 @@ -## Event logs - -#### 0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A (AaveV2Ethereum.POOL_ADMIN, AaveV2EthereumAMM.POOL_ADMIN, AaveV3Ethereum.ACL_ADMIN, AaveV3EthereumEtherFi.ACL_ADMIN, AaveV3EthereumHorizon.ACL_ADMIN, AaveV3EthereumLido.ACL_ADMIN, GovernanceV3Ethereum.EXECUTOR_LVL_1) - -| index | event | -| --- | --- | -| 0 | topics: `0x24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b`, data: `0x` | -| 1 | ExecutedAction(target: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f, value: 0, signature: execute(), data: 0x, executionTime: 1775579243, withDelegatecall: true, resultData: 0x) | - -#### 0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5 (GovernanceV3Ethereum.PAYLOADS_CONTROLLER) - -| index | event | -| --- | --- | -| 2 | PayloadExecuted(payloadId: 426) | - -## Raw storage changes - -### 0xdabad81af85554e9ae636395611c58f7ec1aaec5 (GovernanceV3Ethereum.PAYLOADS_CONTROLLER) - -| slot | previous value | new value | -| --- | --- | --- | -| 0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c7 | 0x0069d5306a000000000002000000000000000000000000000000000000000000 | 0x0069d5306a000000000003000000000000000000000000000000000000000000 | -| 0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c8 | 0x000000000000000000093a800000000000006a0354eb00000000000000000000 | 0x000000000000000000093a800000000000006a0354eb00000000000069d5306b | - - -## Raw diff - -```json -{} -``` diff --git a/diffs/v4_no_e2e_before_v4_no_e2e_after.md b/diffs/v4_no_e2e_before_v4_no_e2e_after.md deleted file mode 100644 index 318d7b79..00000000 --- a/diffs/v4_no_e2e_before_v4_no_e2e_after.md +++ /dev/null @@ -1,30 +0,0 @@ -## Event logs - -#### 0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A (AaveV2Ethereum.POOL_ADMIN, AaveV2EthereumAMM.POOL_ADMIN, AaveV3Ethereum.ACL_ADMIN, AaveV3EthereumEtherFi.ACL_ADMIN, AaveV3EthereumHorizon.ACL_ADMIN, AaveV3EthereumLido.ACL_ADMIN, GovernanceV3Ethereum.EXECUTOR_LVL_1) - -| index | event | -| --- | --- | -| 0 | topics: `0x24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b`, data: `0x` | -| 1 | ExecutedAction(target: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f, value: 0, signature: execute(), data: 0x, executionTime: 1775579243, withDelegatecall: true, resultData: 0x) | - -#### 0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5 (GovernanceV3Ethereum.PAYLOADS_CONTROLLER) - -| index | event | -| --- | --- | -| 2 | PayloadExecuted(payloadId: 426) | - -## Raw storage changes - -### 0xdabad81af85554e9ae636395611c58f7ec1aaec5 (GovernanceV3Ethereum.PAYLOADS_CONTROLLER) - -| slot | previous value | new value | -| --- | --- | --- | -| 0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c7 | 0x0069d5306a000000000002000000000000000000000000000000000000000000 | 0x0069d5306a000000000003000000000000000000000000000000000000000000 | -| 0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c8 | 0x000000000000000000093a800000000000006a0354eb00000000000000000000 | 0x000000000000000000093a800000000000006a0354eb00000000000069d5306b | - - -## Raw diff - -```json -{} -``` diff --git a/diffs/v4_storage_pass_before_v4_storage_pass_after.md b/diffs/v4_storage_pass_before_v4_storage_pass_after.md deleted file mode 100644 index 318d7b79..00000000 --- a/diffs/v4_storage_pass_before_v4_storage_pass_after.md +++ /dev/null @@ -1,30 +0,0 @@ -## Event logs - -#### 0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A (AaveV2Ethereum.POOL_ADMIN, AaveV2EthereumAMM.POOL_ADMIN, AaveV3Ethereum.ACL_ADMIN, AaveV3EthereumEtherFi.ACL_ADMIN, AaveV3EthereumHorizon.ACL_ADMIN, AaveV3EthereumLido.ACL_ADMIN, GovernanceV3Ethereum.EXECUTOR_LVL_1) - -| index | event | -| --- | --- | -| 0 | topics: `0x24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b`, data: `0x` | -| 1 | ExecutedAction(target: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f, value: 0, signature: execute(), data: 0x, executionTime: 1775579243, withDelegatecall: true, resultData: 0x) | - -#### 0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5 (GovernanceV3Ethereum.PAYLOADS_CONTROLLER) - -| index | event | -| --- | --- | -| 2 | PayloadExecuted(payloadId: 426) | - -## Raw storage changes - -### 0xdabad81af85554e9ae636395611c58f7ec1aaec5 (GovernanceV3Ethereum.PAYLOADS_CONTROLLER) - -| slot | previous value | new value | -| --- | --- | --- | -| 0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c7 | 0x0069d5306a000000000002000000000000000000000000000000000000000000 | 0x0069d5306a000000000003000000000000000000000000000000000000000000 | -| 0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c8 | 0x000000000000000000093a800000000000006a0354eb00000000000000000000 | 0x000000000000000000093a800000000000006a0354eb00000000000069d5306b | - - -## Raw diff - -```json -{} -``` diff --git a/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts index aacbf246..ae82cf91 100644 --- a/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts +++ b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { diffV4Snapshots } from '../protocol-diff-v4'; import { aaveV4SnapshotSchema, type AaveV4Snapshot } from '../snapshot-types-v4'; -import { formatBps } from '../formatters-v4'; +import { formatV4Value } from '../formatters-v4'; // --- Fixtures --- @@ -63,6 +63,10 @@ function makeSnapshot(overrides?: Partial): AaveV4Snapshot { rateGrowthBeforeOptimal: 400, rateGrowthAfterOptimal: 6000, maxDrawnRate: '10000', + deficitRay: '0', + swept: '0', + premiumShares: '0', + premiumOffsetRay: '0', }, }, }, @@ -102,25 +106,83 @@ describe('V4 snapshot Zod schema', () => { // --- Formatter --- -describe('formatBps', () => { +describe('BPS formatting via formatV4Value', () => { + const ctx = { chainId: 1 }; + it('formats 8000 as 80.00 %', () => { - expect(formatBps(8000)).toBe('80.00 % [8000]'); + expect(formatV4Value('spokeReserve', 'collateralFactor', 8000, ctx)).toBe('80.00 % [8000]'); }); it('formats 100 as 1.00 %', () => { - expect(formatBps(100)).toBe('1.00 % [100]'); + expect(formatV4Value('spokeReserve', 'liquidationFee', 100, ctx)).toBe('1.00 % [100]'); }); it('formats 50 as 0.50 %', () => { - expect(formatBps(50)).toBe('0.50 % [50]'); + expect(formatV4Value('spokeReserve', 'collateralFactor', 50, ctx)).toBe('0.50 % [50]'); }); - it('formats 5 as 0.05 %', () => { - expect(formatBps(5)).toBe('0.05 % [5]'); + it('formats 0 as 0.00 %', () => { + expect(formatV4Value('spokeReserve', 'collateralFactor', 0, ctx)).toBe('0.00 % [0]'); }); - it('formats 0 as 0.00 %', () => { - expect(formatBps(0)).toBe('0.00 % [0]'); + it('formats collateralRisk as BPS', () => { + expect(formatV4Value('spokeReserve', 'collateralRisk', 5000, ctx)).toBe('50.00 % [5000]'); + }); + + it('formats maxLiquidationBonus as BPS', () => { + expect(formatV4Value('spokeReserve', 'maxLiquidationBonus', 10100, ctx)).toBe('101.00 % [10100]'); + }); + + it('formats hub asset IR strategy fields as BPS', () => { + expect(formatV4Value('hubAsset', 'optimalUsageRatio', 9200, ctx)).toBe('92.00 % [9200]'); + expect(formatV4Value('hubAsset', 'baseDrawnRate', 25, ctx)).toBe('0.25 % [25]'); + expect(formatV4Value('hubAsset', 'rateGrowthBeforeOptimal', 450, ctx)).toBe('4.50 % [450]'); + expect(formatV4Value('hubAsset', 'rateGrowthAfterOptimal', 3000, ctx)).toBe('30.00 % [3000]'); + expect(formatV4Value('hubAsset', 'maxDrawnRate', '3450', ctx)).toBe('34.50 % [3450]'); + }); + + it('formats hub asset liquidityFee as BPS', () => { + expect(formatV4Value('hubAsset', 'liquidityFee', 1500, ctx)).toBe('15.00 % [1500]'); + }); + + it('formats WAD fields for spoke liquidation', () => { + expect(formatV4Value('spokeLiq', 'targetHealthFactor', '1050000000000000000', ctx)).toBe( + '1.05 [1050000000000000000]' + ); + expect(formatV4Value('spokeLiq', 'healthFactorForMaxBonus', '1000000000000000000', ctx)).toBe( + '1 [1000000000000000000]' + ); + }); + + it('formats spokeLiq liquidationBonusFactor as BPS', () => { + expect(formatV4Value('spokeLiq', 'liquidationBonusFactor', 500, ctx)).toBe('5.00 % [500]'); + }); + + it('formats RAY fields for hub asset state', () => { + expect(formatV4Value('hubAsset', 'deficitRay', '1000000000000000000000000000', ctx)).toBe( + '1 [1000000000000000000000000000]' + ); + expect(formatV4Value('hubAsset', 'premiumOffsetRay', '-500000000000000000000000000', ctx)).toBe( + '-0.5 [-500000000000000000000000000]' + ); + }); + + it('formats booleans as checkmarks', () => { + expect(formatV4Value('spokeReserve', 'paused', true, ctx)).toBe(':white_check_mark:'); + expect(formatV4Value('spokeReserve', 'frozen', false, ctx)).toBe(':x:'); + expect(formatV4Value('spokeCap', 'active', true, ctx)).toBe(':white_check_mark:'); + expect(formatV4Value('spokeCap', 'halted', false, ctx)).toBe(':x:'); + }); + + it('formats addresses as explorer links', () => { + const result = formatV4Value('spokeReserve', 'underlying', UNDERLYING, ctx); + expect(result).toContain(UNDERLYING); + expect(result).toContain(']('); // markdown link + }); + + it('falls back to raw string for unformatted fields', () => { + expect(formatV4Value('spokeLiq', 'maxUserReservesLimit', 128, ctx)).toBe('128'); + expect(formatV4Value('spokeCap', 'addCap', '1000000', ctx)).toBe('1000000'); }); }); @@ -246,4 +308,139 @@ describe('diffV4Snapshots', () => { expect(md).toContain('## Raw diff'); expect(md).toContain('```json'); }); + + // --- New/removed for all section types --- + + it('detects new hub asset', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + after.hubAssets[HUB_ADDR]['1'] = { + symbol: 'USDC', + underlying: '0x9999999999999999999999999999999999999999', + decimals: 6, + liquidityFee: 1500, + irStrategy: IR_STRATEGY, + feeReceiver: FEE_RECV, + reinvestmentController: REINVEST, + optimalUsageRatio: 9200, + baseDrawnRate: 0, + rateGrowthBeforeOptimal: 450, + rateGrowthAfterOptimal: 2000, + maxDrawnRate: '2450', + deficitRay: '0', + swept: '0', + premiumShares: '0', + premiumOffsetRay: '0', + }; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('## Hub Asset Changes'); + expect(md).toContain('NEW ASSET'); + expect(md).toContain('USDC'); + }); + + it('detects removed hub asset', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + after.hubAssets[HUB_ADDR] = {}; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('## Hub Asset Changes'); + expect(md).toContain('REMOVED'); + }); + + it('detects new spoke cap', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + const newCapKey = `${HUB_ADDR}_1_${SPOKE_ADDR}`; + after.spokeCaps[newCapKey] = { + assetSymbol: 'USDC', + addCap: '500000', + drawCap: '250000', + riskPremiumThreshold: 50, + active: true, + halted: false, + }; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('## Hub Spoke Cap Changes'); + expect(md).toContain('NEW SPOKE'); + expect(md).toContain('USDC'); + }); + + it('detects removed spoke cap', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + const capKey = `${HUB_ADDR}_0_${SPOKE_ADDR}`; + delete after.spokeCaps[capKey]; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('## Hub Spoke Cap Changes'); + expect(md).toContain('REMOVED'); + }); + + it('detects new spoke liquidation config', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + const newSpoke = '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + after.spokeLiquidationConfigs[newSpoke] = { + targetHealthFactor: '1100000000000000000', + healthFactorForMaxBonus: '1050000000000000000', + liquidationBonusFactor: 400, + maxUserReservesLimit: 64, + }; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('## Spoke Liquidation Config Changes'); + expect(md).toContain('NEW'); + }); + + it('detects removed spoke liquidation config', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + delete after.spokeLiquidationConfigs[SPOKE_ADDR]; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('## Spoke Liquidation Config Changes'); + expect(md).toContain('REMOVED'); + }); + + // --- Hub asset state fields --- + + it('detects hub asset state changes (deficit, swept, premiumShares)', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + after.hubAssets[HUB_ADDR]['0'] = { + ...after.hubAssets[HUB_ADDR]['0'], + deficitRay: '1000000000000000000000000000', + swept: '5000000', + premiumShares: '100000', + premiumOffsetRay: '-500000000000000000000000000', + }; + + const md = await diffV4Snapshots(before, after); + expect(md).toContain('## Hub Asset Changes'); + expect(md).toContain('deficitRay'); + expect(md).toContain('swept'); + expect(md).toContain('premiumShares'); + expect(md).toContain('premiumOffsetRay'); + }); + + // --- Spoke cap composite key parsing --- + + it('parses spoke cap composite key correctly in headers', async () => { + const before = makeSnapshot(); + const after = makeSnapshot(); + const capKey = `${HUB_ADDR}_0_${SPOKE_ADDR}`; + after.spokeCaps[capKey] = { + ...after.spokeCaps[capKey], + addCap: '9999999', + }; + + const md = await diffV4Snapshots(before, after); + // Header should contain the hub and spoke addresses from the parsed key + expect(md).toContain(HUB_ADDR); + expect(md).toContain(SPOKE_ADDR); + expect(md).toContain('assetId: 0'); + }); }); diff --git a/packages/aave-helpers-js/formatters-v4.ts b/packages/aave-helpers-js/formatters-v4.ts index fae72bb5..1bc06f99 100644 --- a/packages/aave-helpers-js/formatters-v4.ts +++ b/packages/aave-helpers-js/formatters-v4.ts @@ -1,4 +1,4 @@ -import type { Hex } from 'viem'; +import { type Hex, formatUnits } from 'viem'; import { getClient } from '@bgd-labs/toolbox'; import { toAddressLink, boolToMarkdown } from './utils/markdown'; import type { @@ -31,7 +31,7 @@ function isAddress(value: unknown): boolean { } /** Format BPS value as percentage, matching Solidity _ps(): "W.FF % [bps]" */ -export function formatBps(bps: number): string { +function formatBps(bps: number): string { const w = Math.floor(bps / 100); const f = bps % 100; const fs = f < 10 ? `0${f}` : `${f}`; @@ -43,6 +43,7 @@ export function formatBps(bps: number): string { type SpokeReserveKey = keyof V4SpokeReserve; const SPOKE_RESERVE_BPS_FIELDS: readonly SpokeReserveKey[] = [ + 'collateralRisk', 'collateralFactor', 'maxLiquidationBonus', 'liquidationFee', @@ -83,8 +84,10 @@ for (const field of SPOKE_RESERVE_ADDRESS_FIELDS) { type HubAssetKey = keyof V4HubAsset; -const HUB_ASSET_BPS_FIELDS: readonly HubAssetKey[] = [ - 'liquidityFee', +const HUB_ASSET_BPS_FIELDS: readonly HubAssetKey[] = ['liquidityFee'] as const; + +/** IR strategy fields — per-asset on V4, BPS scale (unlike V3 RAY). */ +const HUB_ASSET_IR_STRATEGY_BPS_FIELDS: readonly HubAssetKey[] = [ 'optimalUsageRatio', 'baseDrawnRate', 'rateGrowthBeforeOptimal', @@ -106,6 +109,10 @@ for (const field of HUB_ASSET_BPS_FIELDS) { (hubAssetFormatters[field] as FieldFormatter) = (value) => formatBps(value); } +for (const field of HUB_ASSET_IR_STRATEGY_BPS_FIELDS) { + (hubAssetFormatters[field] as FieldFormatter) = (value) => formatBps(value); +} + hubAssetFormatters['maxDrawnRate'] = (value) => formatBps(Number(value)); for (const field of HUB_ASSET_ADDRESS_FIELDS) { @@ -113,6 +120,12 @@ for (const field of HUB_ASSET_ADDRESS_FIELDS) { addressLink(value, ctx.chainId); } +// Asset state — RAY fields (1e27) +hubAssetFormatters['deficitRay'] = (value) => + `${formatUnits(BigInt(value), 27)} [${value}]`; +hubAssetFormatters['premiumOffsetRay'] = (value) => + `${formatUnits(BigInt(value), 27)} [${value}]`; + // --- Spoke Cap formatters --- type SpokeCapKey = keyof V4SpokeCap; @@ -135,6 +148,15 @@ export const spokeLiqFormatters: Partial<{ [K in SpokeLiqKey]: FieldFormatter; }> = {}; +// WAD fields (1e18) — serialized as strings +spokeLiqFormatters['targetHealthFactor'] = (value) => + `${formatUnits(BigInt(value), 18)} [${value}]`; +spokeLiqFormatters['healthFactorForMaxBonus'] = (value) => + `${formatUnits(BigInt(value), 18)} [${value}]`; + +// BPS field +spokeLiqFormatters['liquidationBonusFactor'] = (value) => formatBps(value); + // --- Generic format function --- type V4SectionFormatters = { diff --git a/packages/aave-helpers-js/sections/hub-assets.ts b/packages/aave-helpers-js/sections/hub-assets.ts index eda6d25e..0a6cf3ba 100644 --- a/packages/aave-helpers-js/sections/hub-assets.ts +++ b/packages/aave-helpers-js/sections/hub-assets.ts @@ -19,6 +19,11 @@ const FIELD_ORDER: (keyof V4HubAsset)[] = [ 'rateGrowthBeforeOptimal', 'rateGrowthAfterOptimal', 'maxDrawnRate', + // Asset state + 'deficitRay', + 'swept', + 'premiumShares', + 'premiumOffsetRay', ]; function hubAssetHeader(asset: V4HubAsset, hubAddr: string, assetId: string, chainId: number): string { diff --git a/packages/aave-helpers-js/snapshot-types-v4.ts b/packages/aave-helpers-js/snapshot-types-v4.ts index 1661c0c5..493538b5 100644 --- a/packages/aave-helpers-js/snapshot-types-v4.ts +++ b/packages/aave-helpers-js/snapshot-types-v4.ts @@ -51,6 +51,11 @@ export const v4HubAssetSchema = z.object({ rateGrowthBeforeOptimal: z.number(), rateGrowthAfterOptimal: z.number(), maxDrawnRate: z.string(), // uint256 serialized as string + // Asset state + deficitRay: z.string(), // uint200 serialized as string (RAY) + swept: z.string(), // uint120 serialized as string + premiumShares: z.string(), // uint120 serialized as string + premiumOffsetRay: z.string(), // int200 serialized as string (RAY, signed) }); export type V4HubAsset = z.infer; diff --git a/reports/v4_emit_payload_after.json b/reports/v4_emit_payload_after.json deleted file mode 100644 index 99d11c77..00000000 --- a/reports/v4_emit_payload_after.json +++ /dev/null @@ -1,2770 +0,0 @@ -{ - "chainId": 1, - "hubAssets": { - "0x06002e9c4412CB7814a791eA3666D905871E536A": { - "0": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "PT-sUSDE-7MAY2026", - "underlying": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49" - }, - "1": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "PT-USDe-7MAY2026", - "underlying": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0" - }, - "2": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "sUSDe", - "underlying": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497" - }, - "3": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 2500, - "maxDrawnRate": "3450", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3000, - "rateGrowthBeforeOptimal": 450, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDe", - "underlying": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3" - }, - "4": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 1500, - "maxDrawnRate": "2450", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 2000, - "rateGrowthBeforeOptimal": 450, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "5": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 1000, - "maxDrawnRate": "3450", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3000, - "rateGrowthBeforeOptimal": 450, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - }, - "6": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 1500, - "maxDrawnRate": "2450", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 2000, - "rateGrowthBeforeOptimal": 450, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - } - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931": { - "0": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - }, - "1": { - "baseDrawnRate": 0, - "decimals": 8, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "WBTC", - "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" - }, - "2": { - "baseDrawnRate": 0, - "decimals": 8, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "cbBTC", - "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" - }, - "3": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "wstETH", - "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" - }, - "4": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 1000, - "maxDrawnRate": "2400", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 2000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "5": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 1000, - "maxDrawnRate": "2400", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 2000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "6": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 1000, - "maxDrawnRate": "3400", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - } - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9": { - "0": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 1500, - "maxDrawnRate": "1635", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 1400, - "rateGrowthBeforeOptimal": 235, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - }, - "1": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "wstETH", - "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" - }, - "10": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 1000, - "maxDrawnRate": "4050", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3500, - "rateGrowthBeforeOptimal": 550, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "EURC", - "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" - }, - "11": { - "baseDrawnRate": 25, - "decimals": 8, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 2000, - "maxDrawnRate": "6425", - "optimalUsageRatio": 8000, - "rateGrowthAfterOptimal": 6000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "WBTC", - "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" - }, - "12": { - "baseDrawnRate": 25, - "decimals": 8, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 2000, - "maxDrawnRate": "6425", - "optimalUsageRatio": 8000, - "rateGrowthAfterOptimal": 6000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "cbBTC", - "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" - }, - "13": { - "baseDrawnRate": 0, - "decimals": 8, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "LBTC", - "underlying": "0x8236a87084f8B84306f72007F36F2618A5634494" - }, - "14": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "XAUt", - "underlying": "0x68749665FF8D2d112Fa859AA293F07A622782F38" - }, - "15": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "AAVE", - "underlying": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9" - }, - "16": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "LINK", - "underlying": "0x514910771AF9Ca656af840dff83E8264EcF986CA" - }, - "2": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "weETH", - "underlying": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" - }, - "3": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "rsETH", - "underlying": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7" - }, - "4": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 1000, - "maxDrawnRate": "2400", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 2000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "5": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 1000, - "maxDrawnRate": "2400", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 2000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "6": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 1000, - "maxDrawnRate": "3400", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - }, - "7": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 2000, - "maxDrawnRate": "3900", - "optimalUsageRatio": 8000, - "rateGrowthAfterOptimal": 3500, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "RLUSD", - "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" - }, - "8": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 2000, - "maxDrawnRate": "3900", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3500, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDG", - "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" - }, - "9": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 2000, - "maxDrawnRate": "3900", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3500, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "frxUSD", - "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" - } - } - }, - "spokeCaps": { - "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0x58131E79531caB1d52301228d1f7b842F26B9649": { - "active": true, - "addCap": "400000", - "assetSymbol": "PT-sUSDE-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0x90774889c22D2F2Adf44da1f04C7c95542590df4": { - "active": true, - "addCap": "0", - "assetSymbol": "PT-sUSDE-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "PT-sUSDE-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "1400000", - "assetSymbol": "PT-sUSDE-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0x58131E79531caB1d52301228d1f7b842F26B9649": { - "active": true, - "addCap": "50000", - "assetSymbol": "PT-USDe-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "PT-USDe-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "250000", - "assetSymbol": "PT-USDe-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0xdd2Eb78BF9e6aC5068B95aD2d451e8c9Af10ac81": { - "active": true, - "addCap": "0", - "assetSymbol": "PT-USDe-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0x24f8c062e1E0451736C1D6E023510DA262a41df4": { - "active": true, - "addCap": "0", - "assetSymbol": "sUSDe", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0x58131E79531caB1d52301228d1f7b842F26B9649": { - "active": true, - "addCap": "250000", - "assetSymbol": "sUSDe", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "sUSDe", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "375000", - "assetSymbol": "sUSDe", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0x502Cd81da6a8F1785eb2eEE72713B7388E16A854": { - "active": true, - "addCap": "78000", - "assetSymbol": "USDe", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0x58131E79531caB1d52301228d1f7b842F26B9649": { - "active": true, - "addCap": "312500", - "assetSymbol": "USDe", - "drawCap": "325000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDe", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "312500", - "assetSymbol": "USDe", - "drawCap": "300000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_4_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_4_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "150000", - "assetSymbol": "USDC", - "drawCap": "187500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_4_0xc94bdd83D2c7655C280655D60954e79E88D4F949": { - "active": true, - "addCap": "37500", - "assetSymbol": "USDC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_5_0xA54382db40EC602c0a173A08f9E86Ed40F9D4D10": { - "active": true, - "addCap": "125000", - "assetSymbol": "GHO", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_5_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "GHO", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_5_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "500000", - "assetSymbol": "GHO", - "drawCap": "562500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_6_0x80835EB50694EE0e519743f67e5401e6FD300006": { - "active": true, - "addCap": "37500", - "assetSymbol": "USDT", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_6_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDT", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_6_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "150000", - "assetSymbol": "USDT", - "drawCap": "187500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_0_0x2087513383330B961A3753B47627Bbf149F31c70": { - "active": true, - "addCap": "0", - "assetSymbol": "WETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_0_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "130", - "assetSymbol": "WETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_0_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "WETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_1_0x5AE3d87De89CA6Ce501e8317887F71EABED69E18": { - "active": true, - "addCap": "0", - "assetSymbol": "WBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_1_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "6", - "assetSymbol": "WBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_1_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "WBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_2_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "5", - "assetSymbol": "cbBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_2_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "cbBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_2_0xD38098faf52D8E915EdED84fBF30F81C17906938": { - "active": true, - "addCap": "0", - "assetSymbol": "cbBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_3_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "114", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_3_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_3_0xFCD3D3C69cd032DE0cc78fE529B7447D2fe7F666": { - "active": true, - "addCap": "0", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_4_0x486415fb1F8b062c89ED548f871cf64304AACb31": { - "active": true, - "addCap": "37500", - "assetSymbol": "USDC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_4_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "150000", - "assetSymbol": "USDC", - "drawCap": "175000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_4_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_5_0x46c588DD8453aC259c1f6a54b4C9A93C2aC3762D": { - "active": true, - "addCap": "37500", - "assetSymbol": "USDT", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_5_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "150000", - "assetSymbol": "USDT", - "drawCap": "187500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_5_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDT", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_6_0x900fD46d565d1ac8995928c0179052ec02a6D0E1": { - "active": true, - "addCap": "125000", - "assetSymbol": "GHO", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_6_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "500000", - "assetSymbol": "GHO", - "drawCap": "562500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_6_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "GHO", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { - "active": true, - "addCap": "0", - "assetSymbol": "WETH", - "drawCap": "588", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0x7320CF22Ac095bA2a2e0a652F77efB836c2E751b": { - "active": true, - "addCap": "250", - "assetSymbol": "WETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "1500", - "assetSymbol": "WETH", - "drawCap": "130", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "WETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { - "active": true, - "addCap": "0", - "assetSymbol": "WETH", - "drawCap": "530", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { - "active": true, - "addCap": "0", - "assetSymbol": "WETH", - "drawCap": "441", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "EURC", - "drawCap": "50000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x6D9e2Cdd61CaF69af99b275704B6e272C41c6718": { - "active": true, - "addCap": "112500", - "assetSymbol": "EURC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "225000", - "assetSymbol": "EURC", - "drawCap": "150000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "0", - "assetSymbol": "EURC", - "drawCap": "50000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "EURC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "300000", - "assetSymbol": "EURC", - "drawCap": "312500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { - "active": true, - "addCap": "0", - "assetSymbol": "WBTC", - "drawCap": "5", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0x82A9CC4656784E55Ef2E78F704028B5E1Bfc1732": { - "active": true, - "addCap": "0", - "assetSymbol": "WBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "16", - "assetSymbol": "WBTC", - "drawCap": "1", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "WBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0x33B41B74366F55327d959FfF6D6b6fBc2853dbB1": { - "active": true, - "addCap": "0", - "assetSymbol": "cbBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { - "active": true, - "addCap": "0", - "assetSymbol": "cbBTC", - "drawCap": "3", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "13", - "assetSymbol": "cbBTC", - "drawCap": "1", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "cbBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_13_0x7961F140B570490849DB878AE222570ea838799d": { - "active": true, - "addCap": "0", - "assetSymbol": "LBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_13_0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { - "active": true, - "addCap": "9", - "assetSymbol": "LBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_13_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "LBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_14_0x4E712562fcb5337011398B6C630f55b60641cd5e": { - "active": true, - "addCap": "0", - "assetSymbol": "XAUt", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_14_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "125", - "assetSymbol": "XAUt", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_14_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "XAUt", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_15_0x0A65197b16C5969F92672051c9C9C0C75B369135": { - "active": true, - "addCap": "0", - "assetSymbol": "AAVE", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_15_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "5000", - "assetSymbol": "AAVE", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_15_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "AAVE", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_16_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "31250", - "assetSymbol": "LINK", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_16_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "LINK", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_16_0xE69C2045095C8Ab3E2a7d77de2328faE5baF797c": { - "active": true, - "addCap": "0", - "assetSymbol": "LINK", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "229", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0xcb0E7dA9c635628f6d4827355AeCa75aB8d3560f": { - "active": true, - "addCap": "0", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { - "active": true, - "addCap": "406", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0x559cEc2C840D9DBB18936Afc5E5341D78bfC7Cbe": { - "active": true, - "addCap": "0", - "assetSymbol": "weETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "58", - "assetSymbol": "weETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "weETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { - "active": true, - "addCap": "500", - "assetSymbol": "weETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_3_0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { - "active": true, - "addCap": "563", - "assetSymbol": "rsETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_3_0x45a04Ca1A5cbEeA4B44356c75EDd29b33eB2527a": { - "active": true, - "addCap": "0", - "assetSymbol": "rsETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_3_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "rsETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x5eC44a70F309854fe04d495cFE1B5dA63DD1cc73": { - "active": true, - "addCap": "312500", - "assetSymbol": "USDT", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "USDT", - "drawCap": "125000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "1250000", - "assetSymbol": "USDT", - "drawCap": "1250000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "0", - "assetSymbol": "USDT", - "drawCap": "125000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDT", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "200000", - "assetSymbol": "USDT", - "drawCap": "50000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "0", - "assetSymbol": "USDT", - "drawCap": "125000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x531E90a2376902DE8915789Fcc1075e3B0c153E7": { - "active": true, - "addCap": "312500", - "assetSymbol": "USDC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "USDC", - "drawCap": "125000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "1250000", - "assetSymbol": "USDC", - "drawCap": "1250000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "0", - "assetSymbol": "USDC", - "drawCap": "125000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "187500", - "assetSymbol": "USDC", - "drawCap": "50000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "0", - "assetSymbol": "USDC", - "drawCap": "125000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0x58C14a5E061c9bC6926c5b853445290F296C2F7B": { - "active": true, - "addCap": "125000", - "assetSymbol": "GHO", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "GHO", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "500000", - "assetSymbol": "GHO", - "drawCap": "500000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "GHO", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "0", - "assetSymbol": "GHO", - "drawCap": "12500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "RLUSD", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "500000", - "assetSymbol": "RLUSD", - "drawCap": "340000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "RLUSD", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0xC8a125AE4275a78AADc53B46Ca10566Bc9B249E0": { - "active": true, - "addCap": "125000", - "assetSymbol": "RLUSD", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "0", - "assetSymbol": "RLUSD", - "drawCap": "90000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "USDG", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "500000", - "assetSymbol": "USDG", - "drawCap": "340000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0xAC2435E3C25e8246870D33ce0a26988A46d5DB68": { - "active": true, - "addCap": "125000", - "assetSymbol": "USDG", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDG", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "0", - "assetSymbol": "USDG", - "drawCap": "90000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x2226749630775ee20230Ad65214fB339087eF30D": { - "active": true, - "addCap": "125000", - "assetSymbol": "frxUSD", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "frxUSD", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "500000", - "assetSymbol": "frxUSD", - "drawCap": "312500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "0", - "assetSymbol": "frxUSD", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "frxUSD", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "0", - "assetSymbol": "frxUSD", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "0", - "assetSymbol": "frxUSD", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - } - }, - "spokeLiquidationConfigs": { - "0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1021800000000000000" - }, - "0x58131E79531caB1d52301228d1f7b842F26B9649": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1027700000000000000" - }, - "0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "healthFactorForMaxBonus": "900000000000000000", - "liquidationBonusFactor": 9000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1307500000000000000" - }, - "0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1061500000000000000" - }, - "0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "healthFactorForMaxBonus": "900000000000000000", - "liquidationBonusFactor": 9000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1240000000000000000" - }, - "0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "healthFactorForMaxBonus": "900000000000000000", - "liquidationBonusFactor": 9000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1174000000000000000" - }, - "0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1044200000000000000" - }, - "0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1019100000000000000" - }, - "0xba1B3D55D249692b669A164024A838309B7508AF": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1033200000000000000" - }, - "0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1013700000000000000" - } - }, - "spokeReserves": { - "0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { - "0": { - "assetId": 3, - "borrowable": false, - "collateralFactor": 9500, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10100, - "oracleAddress": "0x37C316996C714Bf906743071e04E62220b3271ac", - "oraclePrice": "222136416823", - "paused": false, - "priceSource": "0x47F52B2e43D0386cF161e001835b03Ad49889e3b", - "receiveSharesEnabled": true, - "symbol": "rsETH", - "underlying": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7" - }, - "1": { - "assetId": 0, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x37C316996C714Bf906743071e04E62220b3271ac", - "oraclePrice": "207813165287", - "paused": false, - "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", - "receiveSharesEnabled": true, - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - } - }, - "0x58131E79531caB1d52301228d1f7b842F26B9649": { - "0": { - "assetId": 1, - "borrowable": false, - "collateralFactor": 9580, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10200, - "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", - "oraclePrice": "99689155", - "paused": false, - "priceSource": "0x0a72df02CE3E4185b6CEDf561f0AE651E9BeE235", - "receiveSharesEnabled": true, - "symbol": "PT-USDe-7MAY2026", - "underlying": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0" - }, - "1": { - "assetId": 0, - "borrowable": false, - "collateralFactor": 9400, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10300, - "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", - "oraclePrice": "99683533", - "paused": false, - "priceSource": "0xa0dc0249c32fa79e8B9b17c735908a60b1141B40", - "receiveSharesEnabled": true, - "symbol": "PT-sUSDE-7MAY2026", - "underlying": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49" - }, - "2": { - "assetId": 2, - "borrowable": false, - "collateralFactor": 9200, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10300, - "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", - "oraclePrice": "122650671", - "paused": false, - "priceSource": "0x42bc86f2f08419280a99d8fbEa4672e7c30a86ec", - "receiveSharesEnabled": true, - "symbol": "sUSDe", - "underlying": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497" - }, - "3": { - "assetId": 3, - "borrowable": true, - "collateralFactor": 9300, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10200, - "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0xC26D4a1c46d884cfF6dE9800B6aE7A8Cf48B4Ff8", - "receiveSharesEnabled": true, - "symbol": "USDe", - "underlying": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3" - } - }, - "0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "0": { - "assetId": 14, - "borrowable": false, - "collateralFactor": 7500, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10666, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "465498700000", - "paused": false, - "priceSource": "0x214eD9Da11D2fbe465a6fc601a91E62EbEc1a0D6", - "receiveSharesEnabled": true, - "symbol": "XAUt", - "underlying": "0x68749665FF8D2d112Fa859AA293F07A622782F38" - }, - "1": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "2": { - "assetId": 7, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "100001110", - "paused": false, - "priceSource": "0xf0eaC18E908B34770FDEe46d069c846bDa866759", - "receiveSharesEnabled": true, - "symbol": "RLUSD", - "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" - }, - "3": { - "assetId": 8, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xF29b1e3b68Fd59DD0a413811fD5d0AbaE653216d", - "receiveSharesEnabled": true, - "symbol": "USDG", - "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" - }, - "4": { - "assetId": 9, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "99992090", - "paused": false, - "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", - "receiveSharesEnabled": true, - "symbol": "frxUSD", - "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" - }, - "5": { - "assetId": 10, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "115424199", - "paused": false, - "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", - "receiveSharesEnabled": true, - "symbol": "EURC", - "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" - }, - "6": { - "assetId": 6, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", - "receiveSharesEnabled": true, - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - }, - "7": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - } - }, - "0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { - "0": { - "assetId": 13, - "borrowable": false, - "collateralFactor": 8600, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10300, - "oracleAddress": "0x198Cac7f54FFc7d709Ac0FEc4B6454CE73e21D3D", - "oraclePrice": "6841743291729", - "paused": false, - "priceSource": "0x5C1771583dbbAE5AFEd71ACD2BfC0eA4029EBB04", - "receiveSharesEnabled": true, - "symbol": "LBTC", - "underlying": "0x8236a87084f8B84306f72007F36F2618A5634494" - }, - "1": { - "assetId": 11, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x198Cac7f54FFc7d709Ac0FEc4B6454CE73e21D3D", - "oraclePrice": "6817396697793", - "paused": false, - "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", - "receiveSharesEnabled": true, - "symbol": "WBTC", - "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" - }, - "2": { - "assetId": 12, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x198Cac7f54FFc7d709Ac0FEc4B6454CE73e21D3D", - "oraclePrice": "6817396697793", - "paused": false, - "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", - "receiveSharesEnabled": true, - "symbol": "cbBTC", - "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" - } - }, - "0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "0": { - "assetId": 0, - "borrowable": true, - "collateralFactor": 8300, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10555, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "207813165287", - "paused": false, - "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", - "receiveSharesEnabled": true, - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - }, - "1": { - "assetId": 1, - "borrowable": false, - "collateralFactor": 8000, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10666, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "255881582629", - "paused": false, - "priceSource": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C", - "receiveSharesEnabled": true, - "symbol": "wstETH", - "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" - }, - "10": { - "assetId": 7, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "100001110", - "paused": false, - "priceSource": "0xf0eaC18E908B34770FDEe46d069c846bDa866759", - "receiveSharesEnabled": true, - "symbol": "RLUSD", - "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" - }, - "11": { - "assetId": 8, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xF29b1e3b68Fd59DD0a413811fD5d0AbaE653216d", - "receiveSharesEnabled": true, - "symbol": "USDG", - "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" - }, - "12": { - "assetId": 9, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "99992090", - "paused": false, - "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", - "receiveSharesEnabled": true, - "symbol": "frxUSD", - "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" - }, - "13": { - "assetId": 6, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", - "receiveSharesEnabled": true, - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - }, - "2": { - "assetId": 2, - "borrowable": false, - "collateralFactor": 8000, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10777, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "226950761571", - "paused": false, - "priceSource": "0xf112aF6F0A332B815fbEf3Ff932c057E570b62d3", - "receiveSharesEnabled": true, - "symbol": "weETH", - "underlying": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" - }, - "3": { - "assetId": 11, - "borrowable": true, - "collateralFactor": 7800, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10555, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "6817396697793", - "paused": false, - "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", - "receiveSharesEnabled": true, - "symbol": "WBTC", - "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" - }, - "4": { - "assetId": 12, - "borrowable": true, - "collateralFactor": 7800, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10555, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "6817396697793", - "paused": false, - "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", - "receiveSharesEnabled": true, - "symbol": "cbBTC", - "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" - }, - "5": { - "assetId": 15, - "borrowable": false, - "collateralFactor": 7600, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10833, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "8613353000", - "paused": false, - "priceSource": "0x547a514d5e3769680Ce22B2361c10Ea13619e8a9", - "receiveSharesEnabled": true, - "symbol": "AAVE", - "underlying": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9" - }, - "6": { - "assetId": 16, - "borrowable": false, - "collateralFactor": 7100, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10777, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "863312586", - "paused": false, - "priceSource": "0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c", - "receiveSharesEnabled": true, - "symbol": "LINK", - "underlying": "0x514910771AF9Ca656af840dff83E8264EcF986CA" - }, - "7": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 7800, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10500, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "8": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 7800, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10500, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "9": { - "assetId": 10, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "115424199", - "paused": false, - "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", - "receiveSharesEnabled": true, - "symbol": "EURC", - "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" - } - }, - "0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "0": { - "assetId": 0, - "borrowable": false, - "collateralFactor": 8600, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 1000, - "maxLiquidationBonus": 10444, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "207813165287", - "paused": false, - "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", - "receiveSharesEnabled": true, - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - }, - "1": { - "assetId": 1, - "borrowable": false, - "collateralFactor": 8450, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 1000, - "maxLiquidationBonus": 10444, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "6817396697793", - "paused": false, - "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", - "receiveSharesEnabled": true, - "symbol": "WBTC", - "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" - }, - "10": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "2": { - "assetId": 2, - "borrowable": false, - "collateralFactor": 8450, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 1000, - "maxLiquidationBonus": 10444, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "6817396697793", - "paused": false, - "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", - "receiveSharesEnabled": true, - "symbol": "cbBTC", - "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" - }, - "3": { - "assetId": 3, - "borrowable": false, - "collateralFactor": 8550, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 1000, - "maxLiquidationBonus": 10444, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "255881582629", - "paused": false, - "priceSource": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C", - "receiveSharesEnabled": true, - "symbol": "wstETH", - "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" - }, - "4": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "5": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "6": { - "assetId": 6, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", - "receiveSharesEnabled": true, - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - }, - "7": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "8": { - "assetId": 9, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "99992090", - "paused": false, - "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", - "receiveSharesEnabled": true, - "symbol": "frxUSD", - "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" - }, - "9": { - "assetId": 10, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "115424199", - "paused": false, - "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", - "receiveSharesEnabled": true, - "symbol": "EURC", - "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" - } - }, - "0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "0": { - "assetId": 10, - "borrowable": true, - "collateralFactor": 9000, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10200, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "115424199", - "paused": false, - "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", - "receiveSharesEnabled": true, - "symbol": "EURC", - "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" - }, - "1": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 9000, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10200, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "2": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 9000, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10200, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "3": { - "assetId": 7, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "100001110", - "paused": false, - "priceSource": "0xf0eaC18E908B34770FDEe46d069c846bDa866759", - "receiveSharesEnabled": true, - "symbol": "RLUSD", - "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" - }, - "4": { - "assetId": 8, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xF29b1e3b68Fd59DD0a413811fD5d0AbaE653216d", - "receiveSharesEnabled": true, - "symbol": "USDG", - "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" - }, - "5": { - "assetId": 9, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "99992090", - "paused": false, - "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", - "receiveSharesEnabled": true, - "symbol": "frxUSD", - "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" - }, - "6": { - "assetId": 6, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", - "receiveSharesEnabled": true, - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - } - }, - "0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { - "0": { - "assetId": 2, - "borrowable": false, - "collateralFactor": 9550, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10100, - "oracleAddress": "0xd8B153FaAA8f2b1bC774916FEd333A4F3dE48792", - "oraclePrice": "226950761571", - "paused": false, - "priceSource": "0xf112aF6F0A332B815fbEf3Ff932c057E570b62d3", - "receiveSharesEnabled": true, - "symbol": "weETH", - "underlying": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" - }, - "1": { - "assetId": 0, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xd8B153FaAA8f2b1bC774916FEd333A4F3dE48792", - "oraclePrice": "207813165287", - "paused": false, - "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", - "receiveSharesEnabled": true, - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - } - }, - "0xba1B3D55D249692b669A164024A838309B7508AF": { - "0": { - "assetId": 1, - "borrowable": false, - "collateralFactor": 9300, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10300, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "99689155", - "paused": false, - "priceSource": "0x0a72df02CE3E4185b6CEDf561f0AE651E9BeE235", - "receiveSharesEnabled": true, - "symbol": "PT-USDe-7MAY2026", - "underlying": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0" - }, - "1": { - "assetId": 0, - "borrowable": false, - "collateralFactor": 9200, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10400, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "99683533", - "paused": false, - "priceSource": "0xa0dc0249c32fa79e8B9b17c735908a60b1141B40", - "receiveSharesEnabled": true, - "symbol": "PT-sUSDE-7MAY2026", - "underlying": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49" - }, - "2": { - "assetId": 2, - "borrowable": false, - "collateralFactor": 9200, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10300, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "122650671", - "paused": false, - "priceSource": "0x42bc86f2f08419280a99d8fbEa4672e7c30a86ec", - "receiveSharesEnabled": true, - "symbol": "sUSDe", - "underlying": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497" - }, - "3": { - "assetId": 3, - "borrowable": true, - "collateralFactor": 9300, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10200, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0xC26D4a1c46d884cfF6dE9800B6aE7A8Cf48B4Ff8", - "receiveSharesEnabled": true, - "symbol": "USDe", - "underlying": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3" - }, - "4": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "5": { - "assetId": 6, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "6": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", - "receiveSharesEnabled": true, - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - }, - "7": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "8": { - "assetId": 9, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "99992090", - "paused": false, - "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", - "receiveSharesEnabled": true, - "symbol": "frxUSD", - "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" - }, - "9": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - } - }, - "0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { - "0": { - "assetId": 1, - "borrowable": false, - "collateralFactor": 9550, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10100, - "oracleAddress": "0x664D73b6C3591333Fd79510f7ce9ef81228824F5", - "oraclePrice": "255881582629", - "paused": false, - "priceSource": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C", - "receiveSharesEnabled": true, - "symbol": "wstETH", - "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" - }, - "1": { - "assetId": 0, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x664D73b6C3591333Fd79510f7ce9ef81228824F5", - "oraclePrice": "207813165287", - "paused": false, - "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", - "receiveSharesEnabled": true, - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - } - } - }, - "raw": { - "0xdabad81af85554e9ae636395611c58f7ec1aaec5": { - "label": null, - "contract": null, - "balanceDiff": null, - "nonceDiff": null, - "stateDiff": { - "0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c7": { - "previousValue": "0x0069d5306a000000000002000000000000000000000000000000000000000000", - "newValue": "0x0069d5306a000000000003000000000000000000000000000000000000000000" - }, - "0xfd549f7710b1aea5af966d00429c36e9dcaaea8e95b089868750b19f493a82c8": { - "previousValue": "0x000000000000000000093a800000000000006a0354eb00000000000000000000", - "newValue": "0x000000000000000000093a800000000000006a0354eb00000000000069d5306b" - } - } - } - }, - "logs": [ - { - "topics": [ - "0x24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b" - ], - "data": "0x", - "emitter": "0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A" - }, - { - "topics": [ - "0x528c26f4cc05f95dc8bad30284946548f08ec44f7dd536473f28b08c65334cdd", - "0x0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f" - ], - "data": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000069d5306b000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000009657865637574652829000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "emitter": "0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A" - }, - { - "topics": [ - "0xda6084bb0aa902a7f6da10ba185d4aa129414651c90772417eff02a52112af2a" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000001aa", - "emitter": "0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5" - } - ] -} \ No newline at end of file diff --git a/reports/v4_emit_payload_before.json b/reports/v4_emit_payload_before.json deleted file mode 100644 index fe218f14..00000000 --- a/reports/v4_emit_payload_before.json +++ /dev/null @@ -1,2728 +0,0 @@ -{ - "chainId": 1, - "hubAssets": { - "0x06002e9c4412CB7814a791eA3666D905871E536A": { - "0": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "PT-sUSDE-7MAY2026", - "underlying": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49" - }, - "1": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "PT-USDe-7MAY2026", - "underlying": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0" - }, - "2": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "sUSDe", - "underlying": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497" - }, - "3": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 2500, - "maxDrawnRate": "3450", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3000, - "rateGrowthBeforeOptimal": 450, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDe", - "underlying": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3" - }, - "4": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 1500, - "maxDrawnRate": "2450", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 2000, - "rateGrowthBeforeOptimal": 450, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "5": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 1000, - "maxDrawnRate": "3450", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3000, - "rateGrowthBeforeOptimal": 450, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - }, - "6": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0x31280650661b8443723fa9739b3A164E3696af48", - "liquidityFee": 1500, - "maxDrawnRate": "2450", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 2000, - "rateGrowthBeforeOptimal": 450, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - } - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931": { - "0": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - }, - "1": { - "baseDrawnRate": 0, - "decimals": 8, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "WBTC", - "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" - }, - "2": { - "baseDrawnRate": 0, - "decimals": 8, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "cbBTC", - "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" - }, - "3": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "wstETH", - "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" - }, - "4": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 1000, - "maxDrawnRate": "2400", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 2000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "5": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 1000, - "maxDrawnRate": "2400", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 2000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "6": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xDCd924047a4bDBFef9CCDDe845E5D45373Ad276D", - "liquidityFee": 1000, - "maxDrawnRate": "3400", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - } - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9": { - "0": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 1500, - "maxDrawnRate": "1635", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 1400, - "rateGrowthBeforeOptimal": 235, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - }, - "1": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "wstETH", - "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" - }, - "10": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 1000, - "maxDrawnRate": "4050", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3500, - "rateGrowthBeforeOptimal": 550, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "EURC", - "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" - }, - "11": { - "baseDrawnRate": 25, - "decimals": 8, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 2000, - "maxDrawnRate": "6425", - "optimalUsageRatio": 8000, - "rateGrowthAfterOptimal": 6000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "WBTC", - "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" - }, - "12": { - "baseDrawnRate": 25, - "decimals": 8, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 2000, - "maxDrawnRate": "6425", - "optimalUsageRatio": 8000, - "rateGrowthAfterOptimal": 6000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "cbBTC", - "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" - }, - "13": { - "baseDrawnRate": 0, - "decimals": 8, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "LBTC", - "underlying": "0x8236a87084f8B84306f72007F36F2618A5634494" - }, - "14": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "XAUt", - "underlying": "0x68749665FF8D2d112Fa859AA293F07A622782F38" - }, - "15": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "AAVE", - "underlying": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9" - }, - "16": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "LINK", - "underlying": "0x514910771AF9Ca656af840dff83E8264EcF986CA" - }, - "2": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "weETH", - "underlying": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" - }, - "3": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 0, - "maxDrawnRate": "0", - "optimalUsageRatio": 9900, - "rateGrowthAfterOptimal": 0, - "rateGrowthBeforeOptimal": 0, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "rsETH", - "underlying": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7" - }, - "4": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 1000, - "maxDrawnRate": "2400", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 2000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "5": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 1000, - "maxDrawnRate": "2400", - "optimalUsageRatio": 9200, - "rateGrowthAfterOptimal": 2000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "6": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 1000, - "maxDrawnRate": "3400", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3000, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - }, - "7": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 2000, - "maxDrawnRate": "3900", - "optimalUsageRatio": 8000, - "rateGrowthAfterOptimal": 3500, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "RLUSD", - "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" - }, - "8": { - "baseDrawnRate": 0, - "decimals": 6, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 2000, - "maxDrawnRate": "3900", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3500, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "USDG", - "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" - }, - "9": { - "baseDrawnRate": 0, - "decimals": 18, - "feeReceiver": "0xB9B0b8616f6Bf6841972a52058132BE08d723155", - "irStrategy": "0xAD88791B0F81D1FA242f637eB05bee0cbc53fe2f", - "liquidityFee": 2000, - "maxDrawnRate": "3900", - "optimalUsageRatio": 9000, - "rateGrowthAfterOptimal": 3500, - "rateGrowthBeforeOptimal": 400, - "reinvestmentController": "0x0000000000000000000000000000000000000000", - "symbol": "frxUSD", - "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" - } - } - }, - "spokeCaps": { - "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0x58131E79531caB1d52301228d1f7b842F26B9649": { - "active": true, - "addCap": "400000", - "assetSymbol": "PT-sUSDE-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0x90774889c22D2F2Adf44da1f04C7c95542590df4": { - "active": true, - "addCap": "0", - "assetSymbol": "PT-sUSDE-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "PT-sUSDE-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_0_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "1400000", - "assetSymbol": "PT-sUSDE-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0x58131E79531caB1d52301228d1f7b842F26B9649": { - "active": true, - "addCap": "50000", - "assetSymbol": "PT-USDe-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "PT-USDe-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "250000", - "assetSymbol": "PT-USDe-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_1_0xdd2Eb78BF9e6aC5068B95aD2d451e8c9Af10ac81": { - "active": true, - "addCap": "0", - "assetSymbol": "PT-USDe-7MAY2026", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0x24f8c062e1E0451736C1D6E023510DA262a41df4": { - "active": true, - "addCap": "0", - "assetSymbol": "sUSDe", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0x58131E79531caB1d52301228d1f7b842F26B9649": { - "active": true, - "addCap": "250000", - "assetSymbol": "sUSDe", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "sUSDe", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_2_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "375000", - "assetSymbol": "sUSDe", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0x502Cd81da6a8F1785eb2eEE72713B7388E16A854": { - "active": true, - "addCap": "78000", - "assetSymbol": "USDe", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0x58131E79531caB1d52301228d1f7b842F26B9649": { - "active": true, - "addCap": "312500", - "assetSymbol": "USDe", - "drawCap": "325000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDe", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_3_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "312500", - "assetSymbol": "USDe", - "drawCap": "300000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_4_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_4_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "150000", - "assetSymbol": "USDC", - "drawCap": "187500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_4_0xc94bdd83D2c7655C280655D60954e79E88D4F949": { - "active": true, - "addCap": "37500", - "assetSymbol": "USDC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_5_0xA54382db40EC602c0a173A08f9E86Ed40F9D4D10": { - "active": true, - "addCap": "125000", - "assetSymbol": "GHO", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_5_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "GHO", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_5_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "500000", - "assetSymbol": "GHO", - "drawCap": "562500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_6_0x80835EB50694EE0e519743f67e5401e6FD300006": { - "active": true, - "addCap": "37500", - "assetSymbol": "USDT", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_6_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDT", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x06002e9c4412CB7814a791eA3666D905871E536A_6_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "150000", - "assetSymbol": "USDT", - "drawCap": "187500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_0_0x2087513383330B961A3753B47627Bbf149F31c70": { - "active": true, - "addCap": "0", - "assetSymbol": "WETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_0_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "130", - "assetSymbol": "WETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_0_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "WETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_1_0x5AE3d87De89CA6Ce501e8317887F71EABED69E18": { - "active": true, - "addCap": "0", - "assetSymbol": "WBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_1_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "6", - "assetSymbol": "WBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_1_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "WBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_2_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "5", - "assetSymbol": "cbBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_2_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "cbBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_2_0xD38098faf52D8E915EdED84fBF30F81C17906938": { - "active": true, - "addCap": "0", - "assetSymbol": "cbBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_3_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "114", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_3_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_3_0xFCD3D3C69cd032DE0cc78fE529B7447D2fe7F666": { - "active": true, - "addCap": "0", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_4_0x486415fb1F8b062c89ED548f871cf64304AACb31": { - "active": true, - "addCap": "37500", - "assetSymbol": "USDC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_4_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "150000", - "assetSymbol": "USDC", - "drawCap": "175000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_4_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_5_0x46c588DD8453aC259c1f6a54b4C9A93C2aC3762D": { - "active": true, - "addCap": "37500", - "assetSymbol": "USDT", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_5_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "150000", - "assetSymbol": "USDT", - "drawCap": "187500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_5_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDT", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_6_0x900fD46d565d1ac8995928c0179052ec02a6D0E1": { - "active": true, - "addCap": "125000", - "assetSymbol": "GHO", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_6_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "500000", - "assetSymbol": "GHO", - "drawCap": "562500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931_6_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "GHO", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { - "active": true, - "addCap": "0", - "assetSymbol": "WETH", - "drawCap": "588", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0x7320CF22Ac095bA2a2e0a652F77efB836c2E751b": { - "active": true, - "addCap": "250", - "assetSymbol": "WETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "1500", - "assetSymbol": "WETH", - "drawCap": "130", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "WETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { - "active": true, - "addCap": "0", - "assetSymbol": "WETH", - "drawCap": "530", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_0_0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { - "active": true, - "addCap": "0", - "assetSymbol": "WETH", - "drawCap": "441", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "EURC", - "drawCap": "50000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x6D9e2Cdd61CaF69af99b275704B6e272C41c6718": { - "active": true, - "addCap": "112500", - "assetSymbol": "EURC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "225000", - "assetSymbol": "EURC", - "drawCap": "150000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "0", - "assetSymbol": "EURC", - "drawCap": "50000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "EURC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_10_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "300000", - "assetSymbol": "EURC", - "drawCap": "312500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { - "active": true, - "addCap": "0", - "assetSymbol": "WBTC", - "drawCap": "5", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0x82A9CC4656784E55Ef2E78F704028B5E1Bfc1732": { - "active": true, - "addCap": "0", - "assetSymbol": "WBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "16", - "assetSymbol": "WBTC", - "drawCap": "1", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_11_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "WBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0x33B41B74366F55327d959FfF6D6b6fBc2853dbB1": { - "active": true, - "addCap": "0", - "assetSymbol": "cbBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { - "active": true, - "addCap": "0", - "assetSymbol": "cbBTC", - "drawCap": "3", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "13", - "assetSymbol": "cbBTC", - "drawCap": "1", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_12_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "cbBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_13_0x7961F140B570490849DB878AE222570ea838799d": { - "active": true, - "addCap": "0", - "assetSymbol": "LBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_13_0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { - "active": true, - "addCap": "9", - "assetSymbol": "LBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_13_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "LBTC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_14_0x4E712562fcb5337011398B6C630f55b60641cd5e": { - "active": true, - "addCap": "0", - "assetSymbol": "XAUt", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_14_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "125", - "assetSymbol": "XAUt", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_14_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "XAUt", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_15_0x0A65197b16C5969F92672051c9C9C0C75B369135": { - "active": true, - "addCap": "0", - "assetSymbol": "AAVE", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_15_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "5000", - "assetSymbol": "AAVE", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_15_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "AAVE", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_16_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "31250", - "assetSymbol": "LINK", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_16_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "LINK", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_16_0xE69C2045095C8Ab3E2a7d77de2328faE5baF797c": { - "active": true, - "addCap": "0", - "assetSymbol": "LINK", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "229", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0xcb0E7dA9c635628f6d4827355AeCa75aB8d3560f": { - "active": true, - "addCap": "0", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_1_0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { - "active": true, - "addCap": "406", - "assetSymbol": "wstETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0x559cEc2C840D9DBB18936Afc5E5341D78bfC7Cbe": { - "active": true, - "addCap": "0", - "assetSymbol": "weETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "58", - "assetSymbol": "weETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "weETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_2_0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { - "active": true, - "addCap": "500", - "assetSymbol": "weETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_3_0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { - "active": true, - "addCap": "563", - "assetSymbol": "rsETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_3_0x45a04Ca1A5cbEeA4B44356c75EDd29b33eB2527a": { - "active": true, - "addCap": "0", - "assetSymbol": "rsETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_3_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "rsETH", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x5eC44a70F309854fe04d495cFE1B5dA63DD1cc73": { - "active": true, - "addCap": "312500", - "assetSymbol": "USDT", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "USDT", - "drawCap": "125000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "1250000", - "assetSymbol": "USDT", - "drawCap": "1250000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "0", - "assetSymbol": "USDT", - "drawCap": "125000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDT", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "200000", - "assetSymbol": "USDT", - "drawCap": "50000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_4_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "0", - "assetSymbol": "USDT", - "drawCap": "125000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x531E90a2376902DE8915789Fcc1075e3B0c153E7": { - "active": true, - "addCap": "312500", - "assetSymbol": "USDC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "USDC", - "drawCap": "125000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "1250000", - "assetSymbol": "USDC", - "drawCap": "1250000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "0", - "assetSymbol": "USDC", - "drawCap": "125000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDC", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "187500", - "assetSymbol": "USDC", - "drawCap": "50000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_5_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "0", - "assetSymbol": "USDC", - "drawCap": "125000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0x58C14a5E061c9bC6926c5b853445290F296C2F7B": { - "active": true, - "addCap": "125000", - "assetSymbol": "GHO", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "GHO", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "500000", - "assetSymbol": "GHO", - "drawCap": "500000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "GHO", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_6_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "0", - "assetSymbol": "GHO", - "drawCap": "12500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "RLUSD", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "500000", - "assetSymbol": "RLUSD", - "drawCap": "340000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "RLUSD", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0xC8a125AE4275a78AADc53B46Ca10566Bc9B249E0": { - "active": true, - "addCap": "125000", - "assetSymbol": "RLUSD", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_7_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "0", - "assetSymbol": "RLUSD", - "drawCap": "90000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "USDG", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "500000", - "assetSymbol": "USDG", - "drawCap": "340000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0xAC2435E3C25e8246870D33ce0a26988A46d5DB68": { - "active": true, - "addCap": "125000", - "assetSymbol": "USDG", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "USDG", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_8_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "0", - "assetSymbol": "USDG", - "drawCap": "90000", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x2226749630775ee20230Ad65214fB339087eF30D": { - "active": true, - "addCap": "125000", - "assetSymbol": "frxUSD", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "active": true, - "addCap": "0", - "assetSymbol": "frxUSD", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "active": true, - "addCap": "500000", - "assetSymbol": "frxUSD", - "drawCap": "312500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "active": true, - "addCap": "0", - "assetSymbol": "frxUSD", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0xB9B0b8616f6Bf6841972a52058132BE08d723155": { - "active": true, - "addCap": "1099511627775", - "assetSymbol": "frxUSD", - "drawCap": "0", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "active": true, - "addCap": "0", - "assetSymbol": "frxUSD", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - }, - "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9_9_0xba1B3D55D249692b669A164024A838309B7508AF": { - "active": true, - "addCap": "0", - "assetSymbol": "frxUSD", - "drawCap": "62500", - "halted": false, - "riskPremiumThreshold": 0 - } - }, - "spokeLiquidationConfigs": { - "0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1021800000000000000" - }, - "0x58131E79531caB1d52301228d1f7b842F26B9649": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1027700000000000000" - }, - "0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "healthFactorForMaxBonus": "900000000000000000", - "liquidationBonusFactor": 9000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1307500000000000000" - }, - "0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1061500000000000000" - }, - "0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "healthFactorForMaxBonus": "900000000000000000", - "liquidationBonusFactor": 9000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1240000000000000000" - }, - "0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "healthFactorForMaxBonus": "900000000000000000", - "liquidationBonusFactor": 9000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1174000000000000000" - }, - "0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1044200000000000000" - }, - "0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1019100000000000000" - }, - "0xba1B3D55D249692b669A164024A838309B7508AF": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1033200000000000000" - }, - "0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { - "healthFactorForMaxBonus": "990000000000000000", - "liquidationBonusFactor": 10000, - "maxUserReservesLimit": 65535, - "targetHealthFactor": "1013700000000000000" - } - }, - "spokeReserves": { - "0x3131FE68C4722e726fe6B2819ED68e514395B9a4": { - "0": { - "assetId": 3, - "borrowable": false, - "collateralFactor": 9500, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10100, - "oracleAddress": "0x37C316996C714Bf906743071e04E62220b3271ac", - "oraclePrice": "222136416823", - "paused": false, - "priceSource": "0x47F52B2e43D0386cF161e001835b03Ad49889e3b", - "receiveSharesEnabled": true, - "symbol": "rsETH", - "underlying": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7" - }, - "1": { - "assetId": 0, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x37C316996C714Bf906743071e04E62220b3271ac", - "oraclePrice": "207813165287", - "paused": false, - "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", - "receiveSharesEnabled": true, - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - } - }, - "0x58131E79531caB1d52301228d1f7b842F26B9649": { - "0": { - "assetId": 1, - "borrowable": false, - "collateralFactor": 9580, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10200, - "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", - "oraclePrice": "99689155", - "paused": false, - "priceSource": "0x0a72df02CE3E4185b6CEDf561f0AE651E9BeE235", - "receiveSharesEnabled": true, - "symbol": "PT-USDe-7MAY2026", - "underlying": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0" - }, - "1": { - "assetId": 0, - "borrowable": false, - "collateralFactor": 9400, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10300, - "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", - "oraclePrice": "99683533", - "paused": false, - "priceSource": "0xa0dc0249c32fa79e8B9b17c735908a60b1141B40", - "receiveSharesEnabled": true, - "symbol": "PT-sUSDE-7MAY2026", - "underlying": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49" - }, - "2": { - "assetId": 2, - "borrowable": false, - "collateralFactor": 9200, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10300, - "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", - "oraclePrice": "122650671", - "paused": false, - "priceSource": "0x42bc86f2f08419280a99d8fbEa4672e7c30a86ec", - "receiveSharesEnabled": true, - "symbol": "sUSDe", - "underlying": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497" - }, - "3": { - "assetId": 3, - "borrowable": true, - "collateralFactor": 9300, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10200, - "oracleAddress": "0x9b91a0943CADf554742E8Fb358B1cC4ae4F85F01", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0xC26D4a1c46d884cfF6dE9800B6aE7A8Cf48B4Ff8", - "receiveSharesEnabled": true, - "symbol": "USDe", - "underlying": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3" - } - }, - "0x65407b940966954b23dfA3caA5C0702bB42984DC": { - "0": { - "assetId": 14, - "borrowable": false, - "collateralFactor": 7500, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10666, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "465498700000", - "paused": false, - "priceSource": "0x214eD9Da11D2fbe465a6fc601a91E62EbEc1a0D6", - "receiveSharesEnabled": true, - "symbol": "XAUt", - "underlying": "0x68749665FF8D2d112Fa859AA293F07A622782F38" - }, - "1": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "2": { - "assetId": 7, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "100001110", - "paused": false, - "priceSource": "0xf0eaC18E908B34770FDEe46d069c846bDa866759", - "receiveSharesEnabled": true, - "symbol": "RLUSD", - "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" - }, - "3": { - "assetId": 8, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xF29b1e3b68Fd59DD0a413811fD5d0AbaE653216d", - "receiveSharesEnabled": true, - "symbol": "USDG", - "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" - }, - "4": { - "assetId": 9, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "99992090", - "paused": false, - "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", - "receiveSharesEnabled": true, - "symbol": "frxUSD", - "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" - }, - "5": { - "assetId": 10, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "115424199", - "paused": false, - "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", - "receiveSharesEnabled": true, - "symbol": "EURC", - "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" - }, - "6": { - "assetId": 6, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", - "receiveSharesEnabled": true, - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - }, - "7": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x0083421fd178749af2201ddA5A7C3feB5790B80c", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - } - }, - "0x7EC68b5695e803e98a21a9A05d744F28b0a7753D": { - "0": { - "assetId": 13, - "borrowable": false, - "collateralFactor": 8600, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10300, - "oracleAddress": "0x198Cac7f54FFc7d709Ac0FEc4B6454CE73e21D3D", - "oraclePrice": "6841743291729", - "paused": false, - "priceSource": "0x5C1771583dbbAE5AFEd71ACD2BfC0eA4029EBB04", - "receiveSharesEnabled": true, - "symbol": "LBTC", - "underlying": "0x8236a87084f8B84306f72007F36F2618A5634494" - }, - "1": { - "assetId": 11, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x198Cac7f54FFc7d709Ac0FEc4B6454CE73e21D3D", - "oraclePrice": "6817396697793", - "paused": false, - "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", - "receiveSharesEnabled": true, - "symbol": "WBTC", - "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" - }, - "2": { - "assetId": 12, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x198Cac7f54FFc7d709Ac0FEc4B6454CE73e21D3D", - "oraclePrice": "6817396697793", - "paused": false, - "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", - "receiveSharesEnabled": true, - "symbol": "cbBTC", - "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" - } - }, - "0x94e7A5dCbE816e498b89aB752661904E2F56c485": { - "0": { - "assetId": 0, - "borrowable": true, - "collateralFactor": 8300, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10555, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "207813165287", - "paused": false, - "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", - "receiveSharesEnabled": true, - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - }, - "1": { - "assetId": 1, - "borrowable": false, - "collateralFactor": 8000, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10666, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "255881582629", - "paused": false, - "priceSource": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C", - "receiveSharesEnabled": true, - "symbol": "wstETH", - "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" - }, - "10": { - "assetId": 7, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "100001110", - "paused": false, - "priceSource": "0xf0eaC18E908B34770FDEe46d069c846bDa866759", - "receiveSharesEnabled": true, - "symbol": "RLUSD", - "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" - }, - "11": { - "assetId": 8, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xF29b1e3b68Fd59DD0a413811fD5d0AbaE653216d", - "receiveSharesEnabled": true, - "symbol": "USDG", - "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" - }, - "12": { - "assetId": 9, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "99992090", - "paused": false, - "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", - "receiveSharesEnabled": true, - "symbol": "frxUSD", - "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" - }, - "13": { - "assetId": 6, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", - "receiveSharesEnabled": true, - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - }, - "2": { - "assetId": 2, - "borrowable": false, - "collateralFactor": 8000, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10777, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "226950761571", - "paused": false, - "priceSource": "0xf112aF6F0A332B815fbEf3Ff932c057E570b62d3", - "receiveSharesEnabled": true, - "symbol": "weETH", - "underlying": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" - }, - "3": { - "assetId": 11, - "borrowable": true, - "collateralFactor": 7800, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10555, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "6817396697793", - "paused": false, - "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", - "receiveSharesEnabled": true, - "symbol": "WBTC", - "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" - }, - "4": { - "assetId": 12, - "borrowable": true, - "collateralFactor": 7800, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10555, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "6817396697793", - "paused": false, - "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", - "receiveSharesEnabled": true, - "symbol": "cbBTC", - "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" - }, - "5": { - "assetId": 15, - "borrowable": false, - "collateralFactor": 7600, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10833, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "8613353000", - "paused": false, - "priceSource": "0x547a514d5e3769680Ce22B2361c10Ea13619e8a9", - "receiveSharesEnabled": true, - "symbol": "AAVE", - "underlying": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9" - }, - "6": { - "assetId": 16, - "borrowable": false, - "collateralFactor": 7100, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10777, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "863312586", - "paused": false, - "priceSource": "0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c", - "receiveSharesEnabled": true, - "symbol": "LINK", - "underlying": "0x514910771AF9Ca656af840dff83E8264EcF986CA" - }, - "7": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 7800, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10500, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "8": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 7800, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10500, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "9": { - "assetId": 10, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127", - "oraclePrice": "115424199", - "paused": false, - "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", - "receiveSharesEnabled": true, - "symbol": "EURC", - "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" - } - }, - "0x973a023A77420ba610f06b3858aD991Df6d85A08": { - "0": { - "assetId": 0, - "borrowable": false, - "collateralFactor": 8600, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 1000, - "maxLiquidationBonus": 10444, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "207813165287", - "paused": false, - "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", - "receiveSharesEnabled": true, - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - }, - "1": { - "assetId": 1, - "borrowable": false, - "collateralFactor": 8450, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 1000, - "maxLiquidationBonus": 10444, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "6817396697793", - "paused": false, - "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", - "receiveSharesEnabled": true, - "symbol": "WBTC", - "underlying": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" - }, - "10": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "2": { - "assetId": 2, - "borrowable": false, - "collateralFactor": 8450, - "collateralRisk": 0, - "decimals": 8, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 1000, - "maxLiquidationBonus": 10444, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "6817396697793", - "paused": false, - "priceSource": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", - "receiveSharesEnabled": true, - "symbol": "cbBTC", - "underlying": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" - }, - "3": { - "assetId": 3, - "borrowable": false, - "collateralFactor": 8550, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 1000, - "maxLiquidationBonus": 10444, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "255881582629", - "paused": false, - "priceSource": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C", - "receiveSharesEnabled": true, - "symbol": "wstETH", - "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" - }, - "4": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "5": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "6": { - "assetId": 6, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", - "receiveSharesEnabled": true, - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - }, - "7": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "8": { - "assetId": 9, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "99992090", - "paused": false, - "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", - "receiveSharesEnabled": true, - "symbol": "frxUSD", - "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" - }, - "9": { - "assetId": 10, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xdA1266a7b8620819dAE3F8bd6B546Da36e505bB8", - "oraclePrice": "115424199", - "paused": false, - "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", - "receiveSharesEnabled": true, - "symbol": "EURC", - "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" - } - }, - "0xD8B93635b8C6d0fF98CbE90b5988E3F2d1Cd9da1": { - "0": { - "assetId": 10, - "borrowable": true, - "collateralFactor": 9000, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10200, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "115424199", - "paused": false, - "priceSource": "0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3", - "receiveSharesEnabled": true, - "symbol": "EURC", - "underlying": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c" - }, - "1": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 9000, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10200, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "2": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 9000, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10200, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "3": { - "assetId": 7, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "100001110", - "paused": false, - "priceSource": "0xf0eaC18E908B34770FDEe46d069c846bDa866759", - "receiveSharesEnabled": true, - "symbol": "RLUSD", - "underlying": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" - }, - "4": { - "assetId": 8, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xF29b1e3b68Fd59DD0a413811fD5d0AbaE653216d", - "receiveSharesEnabled": true, - "symbol": "USDG", - "underlying": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D" - }, - "5": { - "assetId": 9, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "99992090", - "paused": false, - "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", - "receiveSharesEnabled": true, - "symbol": "frxUSD", - "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" - }, - "6": { - "assetId": 6, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xB3CE6E7b6d389a66eA4a3777bA07219d00FB3a9D", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", - "receiveSharesEnabled": true, - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - } - }, - "0xbF10BDfE177dE0336aFD7fcCF80A904E15386219": { - "0": { - "assetId": 2, - "borrowable": false, - "collateralFactor": 9550, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10100, - "oracleAddress": "0xd8B153FaAA8f2b1bC774916FEd333A4F3dE48792", - "oraclePrice": "226950761571", - "paused": false, - "priceSource": "0xf112aF6F0A332B815fbEf3Ff932c057E570b62d3", - "receiveSharesEnabled": true, - "symbol": "weETH", - "underlying": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" - }, - "1": { - "assetId": 0, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xd8B153FaAA8f2b1bC774916FEd333A4F3dE48792", - "oraclePrice": "207813165287", - "paused": false, - "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", - "receiveSharesEnabled": true, - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - } - }, - "0xba1B3D55D249692b669A164024A838309B7508AF": { - "0": { - "assetId": 1, - "borrowable": false, - "collateralFactor": 9300, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10300, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "99689155", - "paused": false, - "priceSource": "0x0a72df02CE3E4185b6CEDf561f0AE651E9BeE235", - "receiveSharesEnabled": true, - "symbol": "PT-USDe-7MAY2026", - "underlying": "0xAeBf0Bb9f57E89260d57f31AF34eB58657d96Ce0" - }, - "1": { - "assetId": 0, - "borrowable": false, - "collateralFactor": 9200, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10400, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "99683533", - "paused": false, - "priceSource": "0xa0dc0249c32fa79e8B9b17c735908a60b1141B40", - "receiveSharesEnabled": true, - "symbol": "PT-sUSDE-7MAY2026", - "underlying": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49" - }, - "2": { - "assetId": 2, - "borrowable": false, - "collateralFactor": 9200, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10300, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "122650671", - "paused": false, - "priceSource": "0x42bc86f2f08419280a99d8fbEa4672e7c30a86ec", - "receiveSharesEnabled": true, - "symbol": "sUSDe", - "underlying": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497" - }, - "3": { - "assetId": 3, - "borrowable": true, - "collateralFactor": 9300, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 1000, - "maxLiquidationBonus": 10200, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0xC26D4a1c46d884cfF6dE9800B6aE7A8Cf48B4Ff8", - "receiveSharesEnabled": true, - "symbol": "USDe", - "underlying": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3" - }, - "4": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "5": { - "assetId": 6, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - }, - "6": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0x06002e9c4412CB7814a791eA3666D905871E536A", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "100000000", - "paused": false, - "priceSource": "0xD110cac5d8682A3b045D5524a9903E031d70FCCd", - "receiveSharesEnabled": true, - "symbol": "GHO", - "underlying": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" - }, - "7": { - "assetId": 5, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "99986000", - "paused": false, - "priceSource": "0x581b8Bc9d6104F71ad6da1f483B67500968C5994", - "receiveSharesEnabled": true, - "symbol": "USDC", - "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - }, - "8": { - "assetId": 9, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "99992090", - "paused": false, - "priceSource": "0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD", - "receiveSharesEnabled": true, - "symbol": "frxUSD", - "underlying": "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29" - }, - "9": { - "assetId": 4, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 6, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0xc390dbe9fc00D6db73C52d375642b47008C33c90", - "oraclePrice": "100006413", - "paused": false, - "priceSource": "0x260326c220E469358846b187eE53328303Efe19C", - "receiveSharesEnabled": true, - "symbol": "USDT", - "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7" - } - }, - "0xe1900480ac69f0B296841Cd01cC37546d92F35Cd": { - "0": { - "assetId": 1, - "borrowable": false, - "collateralFactor": 9550, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 1000, - "maxLiquidationBonus": 10100, - "oracleAddress": "0x664D73b6C3591333Fd79510f7ce9ef81228824F5", - "oraclePrice": "255881582629", - "paused": false, - "priceSource": "0x869C9Ae2C8fbe82a8b0F768b9F791f89E083222C", - "receiveSharesEnabled": true, - "symbol": "wstETH", - "underlying": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" - }, - "1": { - "assetId": 0, - "borrowable": true, - "collateralFactor": 0, - "collateralRisk": 0, - "decimals": 18, - "dynamicConfigKey": 0, - "frozen": false, - "hub": "0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9", - "liquidationFee": 0, - "maxLiquidationBonus": 10000, - "oracleAddress": "0x664D73b6C3591333Fd79510f7ce9ef81228824F5", - "oraclePrice": "207813165287", - "paused": false, - "priceSource": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", - "receiveSharesEnabled": true, - "symbol": "WETH", - "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - } - } - } -} \ No newline at end of file diff --git a/src/dependencies/v4/SnapshotV4.sol b/src/dependencies/v4/SnapshotV4.sol index 634b9f00..721ee13a 100644 --- a/src/dependencies/v4/SnapshotV4.sol +++ b/src/dependencies/v4/SnapshotV4.sol @@ -148,6 +148,7 @@ abstract contract SnapshotV4 is Helpers { uint256 assetId ) private view returns (Types.HubAssetSnapshot memory snap) { IHub.AssetConfig memory config = hub.getAssetConfig(assetId); + IHub.Asset memory asset = hub.getAsset(assetId); (address underlying, uint8 decimals) = hub.getAssetUnderlyingAndDecimals(assetId); snap.hubAddress = address(hub); @@ -170,6 +171,12 @@ abstract contract SnapshotV4 is Helpers { snap.rateGrowthAfterOptimal = irData.rateGrowthAfterOptimal; snap.maxDrawnRate = IAssetInterestRateStrategy(config.irStrategy).getMaxDrawnRate(assetId); } + + // Asset state + snap.deficitRay = asset.deficitRay; + snap.swept = asset.swept; + snap.premiumShares = asset.premiumShares; + snap.premiumOffsetRay = asset.premiumOffsetRay; } // Hub spoke caps diff --git a/src/dependencies/v4/Types.sol b/src/dependencies/v4/Types.sol index 3524c783..4cfbe0ff 100644 --- a/src/dependencies/v4/Types.sol +++ b/src/dependencies/v4/Types.sol @@ -99,6 +99,11 @@ library Types { uint32 rateGrowthBeforeOptimal; uint32 rateGrowthAfterOptimal; uint256 maxDrawnRate; + // Asset state (from getAsset) + uint200 deficitRay; + uint120 swept; + uint120 premiumShares; + int200 premiumOffsetRay; } struct SpokeCapSnapshot { diff --git a/src/dependencies/v4/V4DiffWriter.sol b/src/dependencies/v4/V4DiffWriter.sol index 8c5194fd..b081eace 100644 --- a/src/dependencies/v4/V4DiffWriter.sol +++ b/src/dependencies/v4/V4DiffWriter.sol @@ -155,7 +155,12 @@ library V4DiffWriter { vm.serializeUint(k, 'baseDrawnRate', a.baseDrawnRate); vm.serializeUint(k, 'rateGrowthBeforeOptimal', a.rateGrowthBeforeOptimal); vm.serializeUint(k, 'rateGrowthAfterOptimal', a.rateGrowthAfterOptimal); - return vm.serializeString(k, 'maxDrawnRate', vm.toString(a.maxDrawnRate)); + vm.serializeString(k, 'maxDrawnRate', vm.toString(a.maxDrawnRate)); + // Asset state + vm.serializeString(k, 'deficitRay', vm.toString(uint256(a.deficitRay))); + vm.serializeString(k, 'swept', vm.toString(uint256(a.swept))); + vm.serializeString(k, 'premiumShares', vm.toString(uint256(a.premiumShares))); + return vm.serializeString(k, 'premiumOffsetRay', vm.toString(a.premiumOffsetRay)); } function _writeSpokeCaps(string memory path, Types.SpokeCapSnapshot[] memory caps) private { From 0797f33ee6fbabfa7a1f8243bc32bc525213c9d0 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:20:26 -0500 Subject: [PATCH 05/29] fix: pr comments; lint --- .../__tests__/protocol-diff-v4.spec.ts | 4 +++- packages/aave-helpers-js/formatters-v4.ts | 6 ++---- .../aave-helpers-js/sections/hub-assets.ts | 17 ++++++++--------- .../aave-helpers-js/sections/spoke-caps.ts | 18 +++++++++--------- .../aave-helpers-js/sections/spoke-reserves.ts | 5 +---- src/dependencies/v4/Actions.sol | 4 ++++ src/dependencies/v4/GatewayScenarios.sol | 7 +------ src/dependencies/v4/Helpers.sol | 6 ++---- 8 files changed, 30 insertions(+), 37 deletions(-) diff --git a/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts index ae82cf91..72cec786 100644 --- a/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts +++ b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts @@ -130,7 +130,9 @@ describe('BPS formatting via formatV4Value', () => { }); it('formats maxLiquidationBonus as BPS', () => { - expect(formatV4Value('spokeReserve', 'maxLiquidationBonus', 10100, ctx)).toBe('101.00 % [10100]'); + expect(formatV4Value('spokeReserve', 'maxLiquidationBonus', 10100, ctx)).toBe( + '101.00 % [10100]' + ); }); it('formats hub asset IR strategy fields as BPS', () => { diff --git a/packages/aave-helpers-js/formatters-v4.ts b/packages/aave-helpers-js/formatters-v4.ts index 1bc06f99..63975cc7 100644 --- a/packages/aave-helpers-js/formatters-v4.ts +++ b/packages/aave-helpers-js/formatters-v4.ts @@ -121,10 +121,8 @@ for (const field of HUB_ASSET_ADDRESS_FIELDS) { } // Asset state — RAY fields (1e27) -hubAssetFormatters['deficitRay'] = (value) => - `${formatUnits(BigInt(value), 27)} [${value}]`; -hubAssetFormatters['premiumOffsetRay'] = (value) => - `${formatUnits(BigInt(value), 27)} [${value}]`; +hubAssetFormatters['deficitRay'] = (value) => `${formatUnits(BigInt(value), 27)} [${value}]`; +hubAssetFormatters['premiumOffsetRay'] = (value) => `${formatUnits(BigInt(value), 27)} [${value}]`; // --- Spoke Cap formatters --- diff --git a/packages/aave-helpers-js/sections/hub-assets.ts b/packages/aave-helpers-js/sections/hub-assets.ts index 0a6cf3ba..580a7f76 100644 --- a/packages/aave-helpers-js/sections/hub-assets.ts +++ b/packages/aave-helpers-js/sections/hub-assets.ts @@ -26,7 +26,12 @@ const FIELD_ORDER: (keyof V4HubAsset)[] = [ 'premiumOffsetRay', ]; -function hubAssetHeader(asset: V4HubAsset, hubAddr: string, assetId: string, chainId: number): string { +function hubAssetHeader( + asset: V4HubAsset, + hubAddr: string, + assetId: string, + chainId: number +): string { const client = getClient(chainId, {}); const hubLink = toAddressLink(hubAddr as Hex, true, client); return `### ${asset.symbol} (assetId: ${assetId}) on Hub ${hubLink}\n\n`; @@ -71,16 +76,10 @@ function renderHubAssetDiff( return md + '\n'; } -export function renderHubAssetsSection( - before: AaveV4Snapshot, - after: AaveV4Snapshot -): string { +export function renderHubAssetsSection(before: AaveV4Snapshot, after: AaveV4Snapshot): string { const ctx: V4FormatterContext = { chainId: after.chainId }; - const allHubAddrs = new Set([ - ...Object.keys(before.hubAssets), - ...Object.keys(after.hubAssets), - ]); + const allHubAddrs = new Set([...Object.keys(before.hubAssets), ...Object.keys(after.hubAssets)]); let body = ''; diff --git a/packages/aave-helpers-js/sections/spoke-caps.ts b/packages/aave-helpers-js/sections/spoke-caps.ts index 6bde843b..0e157697 100644 --- a/packages/aave-helpers-js/sections/spoke-caps.ts +++ b/packages/aave-helpers-js/sections/spoke-caps.ts @@ -29,7 +29,13 @@ function parseCapKey(key: string): { hubAddr: string; assetId: string; spokeAddr return { hubAddr, assetId, spokeAddr }; } -function capHeader(cap: V4SpokeCap, hubAddr: string, assetId: string, spokeAddr: string, chainId: number): string { +function capHeader( + cap: V4SpokeCap, + hubAddr: string, + assetId: string, + spokeAddr: string, + chainId: number +): string { const client = getClient(chainId, {}); const hubLink = toAddressLink(hubAddr as Hex, true, client); const spokeLink = toAddressLink(spokeAddr as Hex, true, client); @@ -77,16 +83,10 @@ function renderCapDiff( return md + '\n'; } -export function renderSpokeCapsSection( - before: AaveV4Snapshot, - after: AaveV4Snapshot -): string { +export function renderSpokeCapsSection(before: AaveV4Snapshot, after: AaveV4Snapshot): string { const ctx: V4FormatterContext = { chainId: after.chainId }; - const allKeys = new Set([ - ...Object.keys(before.spokeCaps), - ...Object.keys(after.spokeCaps), - ]); + const allKeys = new Set([...Object.keys(before.spokeCaps), ...Object.keys(after.spokeCaps)]); let body = ''; diff --git a/packages/aave-helpers-js/sections/spoke-reserves.ts b/packages/aave-helpers-js/sections/spoke-reserves.ts index f95b8e93..b7a0e748 100644 --- a/packages/aave-helpers-js/sections/spoke-reserves.ts +++ b/packages/aave-helpers-js/sections/spoke-reserves.ts @@ -77,10 +77,7 @@ function renderReserveDiff( return md + '\n'; } -export function renderSpokeReservesSection( - before: AaveV4Snapshot, - after: AaveV4Snapshot -): string { +export function renderSpokeReservesSection(before: AaveV4Snapshot, after: AaveV4Snapshot): string { const ctx: V4FormatterContext = { chainId: after.chainId }; const allSpokeAddrs = new Set([ diff --git a/src/dependencies/v4/Actions.sol b/src/dependencies/v4/Actions.sol index 55f41d0a..1a034436 100644 --- a/src/dependencies/v4/Actions.sol +++ b/src/dependencies/v4/Actions.sol @@ -322,6 +322,10 @@ abstract contract Actions is CommonTestBase { snapshotBefore.spokeOnHub.drawnShares + expectedDrawnShares, 'BORROW: hub drawn shares mismatch' ); + + // Health factor must remain above liquidation threshold after borrow + uint256 healthFactor = spoke.getUserAccountData(user).healthFactor; + assertGt(healthFactor, HEALTH_FACTOR_LIQUIDATION_THRESHOLD, 'BORROW: health factor below 1'); } function _repay( diff --git a/src/dependencies/v4/GatewayScenarios.sol b/src/dependencies/v4/GatewayScenarios.sol index 844d665f..cff4b904 100644 --- a/src/dependencies/v4/GatewayScenarios.sol +++ b/src/dependencies/v4/GatewayScenarios.sol @@ -4,12 +4,7 @@ pragma solidity ^0.8.0; import 'forge-std/Test.sol'; import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; -import { - ISpoke, - IAaveOracle, - INativeTokenGateway, - ISignatureGateway -} from 'aave-address-book/AaveV4.sol'; +import {ISpoke, IAaveOracle, INativeTokenGateway, ISignatureGateway} from 'aave-address-book/AaveV4.sol'; import {IHubBase} from 'aave-v4/hub/interfaces/IHubBase.sol'; import {Types} from 'src/dependencies/v4/Types.sol'; import {Helpers} from 'src/dependencies/v4/Helpers.sol'; diff --git a/src/dependencies/v4/Helpers.sol b/src/dependencies/v4/Helpers.sol index dd5886d9..de3963ff 100644 --- a/src/dependencies/v4/Helpers.sol +++ b/src/dependencies/v4/Helpers.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { - IERC20Metadata -} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {IERC20Metadata} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; import {ISpoke, IHubConfigurator, IAaveOracle} from 'aave-address-book/AaveV4.sol'; import {AaveV4Ethereum} from 'aave-address-book/AaveV4Ethereum.sol'; import {Types} from 'src/dependencies/v4/Types.sol'; @@ -104,7 +102,7 @@ abstract contract Helpers is Actions { address borrower, uint256 amount ) internal { - _supply({spoke: spoke, reserveInfo: reserveInfo, user: borrower, amount: amount}); + _supply(spoke, reserveInfo, borrower, amount); vm.prank(borrower); spoke.setUsingAsCollateral({ reserveId: reserveInfo.reserveId, From 784979b351e21d0e6a3320ecb172039db3ccaa38 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:52:14 -0500 Subject: [PATCH 06/29] rft: _setCapsToMax; diffWriter --- packages/aave-helpers-js/package.json | 4 +- src/dependencies/v4/Helpers.sol | 78 ++++++++++++------- src/dependencies/v4/TokenizationScenarios.sol | 19 ++--- src/dependencies/v4/V4DiffWriter.sol | 49 ++++++------ 4 files changed, 81 insertions(+), 69 deletions(-) diff --git a/packages/aave-helpers-js/package.json b/packages/aave-helpers-js/package.json index 0133430c..47b04a72 100644 --- a/packages/aave-helpers-js/package.json +++ b/packages/aave-helpers-js/package.json @@ -5,9 +5,9 @@ "type": "module", "exports": { ".": { + "types": "./dist/index.d.ts", "import": "./dist/index.mjs", - "require": "./dist/index.cjs", - "types": "./dist/index.d.ts" + "require": "./dist/index.cjs" } }, "main": "./dist/index.cjs", diff --git a/src/dependencies/v4/Helpers.sol b/src/dependencies/v4/Helpers.sol index de3963ff..d9e496cb 100644 --- a/src/dependencies/v4/Helpers.sol +++ b/src/dependencies/v4/Helpers.sol @@ -1,9 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IERC20Metadata} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; -import {ISpoke, IHubConfigurator, IAaveOracle} from 'aave-address-book/AaveV4.sol'; -import {AaveV4Ethereum} from 'aave-address-book/AaveV4Ethereum.sol'; +import { + IERC20Metadata +} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {IAccessManaged} from 'aave-v4/dependencies/openzeppelin/IAccessManaged.sol'; +import {HubConfigurator} from 'aave-v4/hub/HubConfigurator.sol'; +import {ISpoke, IHub, IHubConfigurator, IAaveOracle} from 'aave-address-book/AaveV4.sol'; import {Types} from 'src/dependencies/v4/Types.sol'; import {Actions} from 'src/dependencies/v4/Actions.sol'; @@ -334,45 +337,64 @@ abstract contract Helpers is Actions { /// @notice Set all addCap/drawCap to max for every reserve on the spoke. function _setCapsToMax(ISpoke spoke) internal { - IHubConfigurator hubConfigurator = AaveV4Ethereum.HUB_CONFIGURATOR; + _setSpokeCapsToMaxForAllReserves({spoke: spoke, maxAddCap: true, maxDrawCap: true}); + } + /// @notice Set all addCap to max for every reserve on the spoke (leaves drawCap unchanged). + function _setAddCapsToMax(ISpoke spoke) internal { + _setSpokeCapsToMaxForAllReserves({spoke: spoke, maxAddCap: true, maxDrawCap: false}); + } + + /// @notice Set caps to max for a single hub-asset-spoke combination. + /// @param maxAddCap If true, set addCap to max. + /// @param maxDrawCap If true, set drawCap to max. + function _setSpokeCapsToMax( + IHub hub, + uint256 assetId, + address spoke, + bool maxAddCap, + bool maxDrawCap + ) internal { + IHubConfigurator configurator = _deployMockedHubConfigurator(hub); + IHub.SpokeConfig memory config = hub.getSpokeConfig(assetId, spoke); + if (maxAddCap) { + config.addCap = hub.MAX_ALLOWED_SPOKE_CAP(); + } + if (maxDrawCap) { + config.drawCap = hub.MAX_ALLOWED_SPOKE_CAP(); + } + configurator.updateSpokeCaps({ + hub: address(hub), + assetId: assetId, + spoke: spoke, + addCap: config.addCap, + drawCap: config.drawCap + }); + } + + function _setSpokeCapsToMaxForAllReserves(ISpoke spoke, bool maxAddCap, bool maxDrawCap) private { Types.ReserveInfo[] memory infos = _getReserveInfo(spoke); - vm.mockCall( - address(AaveV4Ethereum.ACCESS_MANAGER), - abi.encodeWithSelector(bytes4(keccak256('canCall(address,address,bytes4)'))), - abi.encode(true, uint32(0)) - ); for (uint256 i; i < infos.length; i++) { - hubConfigurator.updateSpokeCaps({ - hub: infos[i].hub, + _setSpokeCapsToMax({ + hub: IHub(infos[i].hub), assetId: infos[i].assetId, spoke: address(spoke), - addCap: type(uint40).max, - drawCap: type(uint40).max + maxAddCap: maxAddCap, + maxDrawCap: maxDrawCap }); } vm.clearMockedCalls(); } - /// @notice Set all addCap to max for every reserve on the spoke (leaves drawCap unchanged). - function _setAddCapsToMax(ISpoke spoke) internal { - IHubConfigurator hubConfigurator = AaveV4Ethereum.HUB_CONFIGURATOR; - - Types.ReserveInfo[] memory infos = _getReserveInfo(spoke); + /// @notice Deploy a temporary HubConfigurator with the hub's access manager, mocked to allow all calls. + function _deployMockedHubConfigurator(IHub hub) internal returns (IHubConfigurator) { + address accessManager = IAccessManaged(address(hub)).authority(); vm.mockCall( - address(AaveV4Ethereum.ACCESS_MANAGER), + accessManager, abi.encodeWithSelector(bytes4(keccak256('canCall(address,address,bytes4)'))), abi.encode(true, uint32(0)) ); - for (uint256 i; i < infos.length; i++) { - hubConfigurator.updateSpokeAddCap({ - hub: infos[i].hub, - assetId: infos[i].assetId, - spoke: address(spoke), - addCap: type(uint40).max - }); - } - vm.clearMockedCalls(); + return IHubConfigurator(address(new HubConfigurator(accessManager))); } function _safeSymbol(address token) internal view returns (string memory) { diff --git a/src/dependencies/v4/TokenizationScenarios.sol b/src/dependencies/v4/TokenizationScenarios.sol index fed57f2e..c7e68fc4 100644 --- a/src/dependencies/v4/TokenizationScenarios.sol +++ b/src/dependencies/v4/TokenizationScenarios.sol @@ -4,8 +4,7 @@ pragma solidity ^0.8.0; import 'forge-std/Test.sol'; import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; -import {ITokenizationSpoke, IHub, IHubConfigurator} from 'aave-address-book/AaveV4.sol'; -import {AaveV4Ethereum} from 'aave-address-book/AaveV4Ethereum.sol'; +import {ITokenizationSpoke, IHub} from 'aave-address-book/AaveV4.sol'; import {Types} from 'src/dependencies/v4/Types.sol'; import {TokenizationActions} from 'src/dependencies/v4/TokenizationActions.sol'; @@ -42,21 +41,15 @@ abstract contract TokenizationScenarios is TokenizationActions { }); } - /// @notice Set addCap to max for a tokenization spoke's asset. + /// @notice Set addCap/drawCap to max for a tokenization spoke's asset. function _setTokenizationCapsToMax(ITokenizationSpoke tokenizationSpoke) internal { - vm.mockCall( - address(AaveV4Ethereum.ACCESS_MANAGER), - abi.encodeWithSelector(bytes4(keccak256('canCall(address,address,bytes4)'))), - abi.encode(true, uint32(0)) - ); - AaveV4Ethereum.HUB_CONFIGURATOR.updateSpokeCaps({ - hub: tokenizationSpoke.hub(), + _setSpokeCapsToMax({ + hub: IHub(tokenizationSpoke.hub()), assetId: tokenizationSpoke.assetId(), spoke: address(tokenizationSpoke), - addCap: type(uint40).max, - drawCap: type(uint40).max + maxAddCap: true, + maxDrawCap: true }); - vm.clearMockedCalls(); } // ------------------------------------------------------------------------- diff --git a/src/dependencies/v4/V4DiffWriter.sol b/src/dependencies/v4/V4DiffWriter.sol index b081eace..2ad297d0 100644 --- a/src/dependencies/v4/V4DiffWriter.sol +++ b/src/dependencies/v4/V4DiffWriter.sol @@ -37,31 +37,33 @@ library V4DiffWriter { for (uint256 i; i < reserves.length; i++) { string memory obj = _serReserve(reserves[i]); - string memory spokeKey = string.concat('spoke_', vm.toString(reserves[i].spokeAddress)); - if (reserves[i].reserveId == 0) vm.serializeJson(spokeKey, '{}'); + string memory spokeKey = string.concat( + 'spoke_', + vm.toString(reserves[i].spokeAddress) + ); + if (reserves[i].reserveId == 0) { + vm.serializeJson(spokeKey, '{}'); + } string memory spokeObj = vm.serializeString( spokeKey, vm.toString(reserves[i].reserveId), obj ); - if (_isLastForSpoke(reserves, i)) { - content = vm.serializeString(sectionKey, vm.toString(reserves[i].spokeAddress), spokeObj); + if ( + i + 1 == reserves.length || + reserves[i + 1].spokeAddress != reserves[i].spokeAddress + ) { + content = vm.serializeString( + sectionKey, + vm.toString(reserves[i].spokeAddress), + spokeObj + ); } } vm.writeJson(vm.serializeString('root', 'spokeReserves', content), path); } - function _isLastForSpoke( - Types.SpokeReserveSnapshot[] memory arr, - uint256 idx - ) private pure returns (bool) { - for (uint256 j = idx + 1; j < arr.length; j++) { - if (arr[j].spokeAddress == arr[idx].spokeAddress) return false; - } - return true; - } - function _serReserve(Types.SpokeReserveSnapshot memory r) private returns (string memory) { string memory k = string.concat(vm.toString(r.spokeAddress), '_', vm.toString(r.reserveId)); vm.serializeJson(k, '{}'); @@ -121,26 +123,21 @@ library V4DiffWriter { string memory obj = _serHubAsset(assets[i]); string memory hubKey = string.concat('hub_', vm.toString(assets[i].hubAddress)); - if (assets[i].assetId == 0) vm.serializeJson(hubKey, '{}'); + if (i == 0 || assets[i].hubAddress != assets[i - 1].hubAddress) { + vm.serializeJson(hubKey, '{}'); + } string memory hubObj = vm.serializeString(hubKey, vm.toString(assets[i].assetId), obj); - if (_isLastForHub(assets, i)) { + if ( + i + 1 == assets.length || + assets[i + 1].hubAddress != assets[i].hubAddress + ) { content = vm.serializeString(sectionKey, vm.toString(assets[i].hubAddress), hubObj); } } vm.writeJson(vm.serializeString('root', 'hubAssets', content), path); } - function _isLastForHub( - Types.HubAssetSnapshot[] memory arr, - uint256 idx - ) private pure returns (bool) { - for (uint256 j = idx + 1; j < arr.length; j++) { - if (arr[j].hubAddress == arr[idx].hubAddress) return false; - } - return true; - } - function _serHubAsset(Types.HubAssetSnapshot memory a) private returns (string memory) { string memory k = string.concat(vm.toString(a.hubAddress), '_', vm.toString(a.assetId)); vm.serializeJson(k, '{}'); From 561773963ca4e09a733474c65a0c97b53ca48cde Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:01:15 -0500 Subject: [PATCH 07/29] test: liquidation assertions; lint --- src/dependencies/v4/Actions.sol | 100 ++++++++++++++++++++++++++- src/dependencies/v4/Helpers.sol | 4 +- src/dependencies/v4/V4DiffWriter.sol | 21 ++---- 3 files changed, 104 insertions(+), 21 deletions(-) diff --git a/src/dependencies/v4/Actions.sol b/src/dependencies/v4/Actions.sol index 1a034436..74988b11 100644 --- a/src/dependencies/v4/Actions.sol +++ b/src/dependencies/v4/Actions.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import 'forge-std/Test.sol'; import {SafeERC20, IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; import {CommonTestBase} from 'src/CommonTestBase.sol'; -import {ISpoke} from 'aave-address-book/AaveV4.sol'; +import {ISpoke, IAaveOracle} from 'aave-address-book/AaveV4.sol'; import {IHubBase} from 'aave-v4/hub/interfaces/IHubBase.sol'; import {WadRayMath} from 'aave-v4/libraries/math/WadRayMath.sol'; import {Types} from 'src/dependencies/v4/Types.sol'; @@ -418,6 +418,17 @@ abstract contract Actions is CommonTestBase { amount: debtSnapshotBefore.user.totalDebt }); + // Capture pre-liquidation state for profitability check + // Track debt and collateral token balances + share positions separately + uint256 liquidatorDebtBalanceBefore = IERC20(debtInfo.underlying).balanceOf(liquidator); + uint256 liquidatorCollateralTokenBefore = IERC20(collateralInfo.underlying).balanceOf( + liquidator + ); + uint256 liquidatorCollateralSharesBefore = spoke.getUserSuppliedAssets( + collateralInfo.reserveId, + liquidator + ); + if (debtToCover == UINT256_MAX) { console.log( 'LIQUIDATE: %s, DebtToCover: UINT256_MAX, TotalDebt: %e', @@ -470,6 +481,93 @@ abstract contract Actions is CommonTestBase { collateralSnapshotBefore.user.collateralAssets, 'LIQUIDATE: collateral did not decrease' ); + + // Liquidation profitability: collateral value received > debt value paid + _assertLiquidationProfitable({ + spoke: spoke, + collateralInfo: collateralInfo, + debtInfo: debtInfo, + liquidator: liquidator, + liquidatorDebtBalanceBefore: liquidatorDebtBalanceBefore, + liquidatorCollateralTokenBefore: liquidatorCollateralTokenBefore, + liquidatorCollateralSharesBefore: liquidatorCollateralSharesBefore + }); + } + + function _assertLiquidationProfitable( + ISpoke spoke, + Types.ReserveInfo memory collateralInfo, + Types.ReserveInfo memory debtInfo, + address liquidator, + uint256 liquidatorDebtBalanceBefore, + uint256 liquidatorCollateralTokenBefore, + uint256 liquidatorCollateralSharesBefore + ) private view { + // with the same asset, total position (tokens + shares) should increase after liquidation. + if (collateralInfo.underlying == debtInfo.underlying) { + _assertProfitableSameUnderlying({ + spoke: spoke, + collateralInfo: collateralInfo, + liquidator: liquidator, + liquidatorCollateralTokenBefore: liquidatorCollateralTokenBefore, + liquidatorCollateralSharesBefore: liquidatorCollateralSharesBefore + }); + } else { + _assertProfitableDiffUnderlying({ + spoke: spoke, + collateralInfo: collateralInfo, + liquidator: liquidator, + liquidatorCollateralTokenBefore: liquidatorCollateralTokenBefore, + liquidatorCollateralSharesBefore: liquidatorCollateralSharesBefore + }); + } + } + + /// @dev Same underlying: total position (tokens + shares) should increase after liquidation. + function _assertProfitableSameUnderlying( + ISpoke spoke, + Types.ReserveInfo memory collateralInfo, + address liquidator, + uint256 liquidatorCollateralTokenBefore, + uint256 liquidatorCollateralSharesBefore + ) private view { + uint256 totalBefore = liquidatorCollateralTokenBefore + liquidatorCollateralSharesBefore; + uint256 totalAfter = IERC20(collateralInfo.underlying).balanceOf(liquidator) + + spoke.getUserSuppliedAssets(collateralInfo.reserveId, liquidator); + + assertGt( + totalAfter, + totalBefore, + 'LIQUIDATE: not profitable (same underlying) - total position did not increase' + ); + } + + /// @dev Different underlyings: compare oracle-normalized collateral gained vs debt spent. + /// @dev Different underlyings: oracle prices may be mocked during liquidation tests, + /// so cross-asset value comparison is unreliable. Just verify the liquidator received collateral. + function _assertProfitableDiffUnderlying( + ISpoke spoke, + Types.ReserveInfo memory collateralInfo, + address liquidator, + uint256 liquidatorCollateralTokenBefore, + uint256 liquidatorCollateralSharesBefore + ) private view { + uint256 collateralGained = IERC20(collateralInfo.underlying).balanceOf(liquidator) + + spoke.getUserSuppliedAssets(collateralInfo.reserveId, liquidator) - + liquidatorCollateralTokenBefore - + liquidatorCollateralSharesBefore; + + assertGt(collateralGained, 0, 'LIQUIDATE: liquidator received no collateral'); + } + + /// @notice Convert a token amount to its oracle-denominated value. + function _getOracleValue( + IAaveOracle oracle, + Types.ReserveInfo memory reserveInfo, + uint256 amount + ) internal view returns (uint256) { + uint256 price = oracle.getReservePrice(reserveInfo.reserveId); + return (amount * price) / (10 ** reserveInfo.decimals); } function _maxDealAmount(uint8 decimals) internal pure returns (uint256) { diff --git a/src/dependencies/v4/Helpers.sol b/src/dependencies/v4/Helpers.sol index d9e496cb..4519a043 100644 --- a/src/dependencies/v4/Helpers.sol +++ b/src/dependencies/v4/Helpers.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { - IERC20Metadata -} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {IERC20Metadata} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; import {IAccessManaged} from 'aave-v4/dependencies/openzeppelin/IAccessManaged.sol'; import {HubConfigurator} from 'aave-v4/hub/HubConfigurator.sol'; import {ISpoke, IHub, IHubConfigurator, IAaveOracle} from 'aave-address-book/AaveV4.sol'; diff --git a/src/dependencies/v4/V4DiffWriter.sol b/src/dependencies/v4/V4DiffWriter.sol index 2ad297d0..006a7abf 100644 --- a/src/dependencies/v4/V4DiffWriter.sol +++ b/src/dependencies/v4/V4DiffWriter.sol @@ -37,10 +37,7 @@ library V4DiffWriter { for (uint256 i; i < reserves.length; i++) { string memory obj = _serReserve(reserves[i]); - string memory spokeKey = string.concat( - 'spoke_', - vm.toString(reserves[i].spokeAddress) - ); + string memory spokeKey = string.concat('spoke_', vm.toString(reserves[i].spokeAddress)); if (reserves[i].reserveId == 0) { vm.serializeJson(spokeKey, '{}'); } @@ -50,15 +47,8 @@ library V4DiffWriter { obj ); - if ( - i + 1 == reserves.length || - reserves[i + 1].spokeAddress != reserves[i].spokeAddress - ) { - content = vm.serializeString( - sectionKey, - vm.toString(reserves[i].spokeAddress), - spokeObj - ); + if (i + 1 == reserves.length || reserves[i + 1].spokeAddress != reserves[i].spokeAddress) { + content = vm.serializeString(sectionKey, vm.toString(reserves[i].spokeAddress), spokeObj); } } vm.writeJson(vm.serializeString('root', 'spokeReserves', content), path); @@ -128,10 +118,7 @@ library V4DiffWriter { } string memory hubObj = vm.serializeString(hubKey, vm.toString(assets[i].assetId), obj); - if ( - i + 1 == assets.length || - assets[i + 1].hubAddress != assets[i].hubAddress - ) { + if (i + 1 == assets.length || assets[i + 1].hubAddress != assets[i].hubAddress) { content = vm.serializeString(sectionKey, vm.toString(assets[i].hubAddress), hubObj); } } From b6568331de21df6cbc8df18d647b629284de4113 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:06:51 -0500 Subject: [PATCH 08/29] test: liq assertions; refactor; fix precision loss --- src/dependencies/v4/Actions.sol | 132 +++++++++++++++--------------- src/dependencies/v4/Scenarios.sol | 23 ++---- 2 files changed, 70 insertions(+), 85 deletions(-) diff --git a/src/dependencies/v4/Actions.sol b/src/dependencies/v4/Actions.sol index 74988b11..d058678d 100644 --- a/src/dependencies/v4/Actions.sol +++ b/src/dependencies/v4/Actions.sol @@ -418,16 +418,14 @@ abstract contract Actions is CommonTestBase { amount: debtSnapshotBefore.user.totalDebt }); - // Capture pre-liquidation state for profitability check - // Track debt and collateral token balances + share positions separately - uint256 liquidatorDebtBalanceBefore = IERC20(debtInfo.underlying).balanceOf(liquidator); - uint256 liquidatorCollateralTokenBefore = IERC20(collateralInfo.underlying).balanceOf( + // Capture pre-liquidation totals (token balance + supplied assets) for profitability check + uint256 liquidatorDebtTotalBefore = IERC20(debtInfo.underlying).balanceOf(liquidator) + + spoke.getUserSuppliedAssets(debtInfo.reserveId, liquidator); + uint256 liquidatorCollateralTotalBefore = IERC20(collateralInfo.underlying).balanceOf( liquidator - ); - uint256 liquidatorCollateralSharesBefore = spoke.getUserSuppliedAssets( - collateralInfo.reserveId, - liquidator - ); + ) + spoke.getUserSuppliedAssets(collateralInfo.reserveId, liquidator); + + ISpoke.UserAccountData memory borrowerAccountDataBefore = spoke.getUserAccountData(borrower); if (debtToCover == UINT256_MAX) { console.log( @@ -481,6 +479,32 @@ abstract contract Actions is CommonTestBase { collateralSnapshotBefore.user.collateralAssets, 'LIQUIDATE: collateral did not decrease' ); + // hub collateral can remain unchanged if receiveShares is true + assertLe( + collateralSnapshotAfter.spokeOnHub.totalDebt, + collateralSnapshotAfter.spokeOnHub.totalDebt, + 'LIQUIDATE: hub collateral did not decrease or stay the same' + ); + assertLe( + spoke.getUserAccountData(borrower).activeCollateralCount, + borrowerAccountDataBefore.activeCollateralCount, + 'LIQUIDATE: borrower collateral count did not decrease or stay the same' + ); + assertLe( + spoke.getUserAccountData(borrower).borrowCount, + borrowerAccountDataBefore.borrowCount, + 'LIQUIDATE: borrower borrow count did not decrease or stay the same' + ); + assertLe( + spoke.getUserAccountData(borrower).totalCollateralValue, + borrowerAccountDataBefore.totalCollateralValue, + 'LIQUIDATE: borrower collateral value did not decrease or stay the same' + ); + assertLe( + spoke.getUserAccountData(borrower).totalDebtValueRay, + borrowerAccountDataBefore.totalDebtValueRay, + 'LIQUIDATE: borrower debt value did not decrease or stay the same' + ); // Liquidation profitability: collateral value received > debt value paid _assertLiquidationProfitable({ @@ -488,76 +512,50 @@ abstract contract Actions is CommonTestBase { collateralInfo: collateralInfo, debtInfo: debtInfo, liquidator: liquidator, - liquidatorDebtBalanceBefore: liquidatorDebtBalanceBefore, - liquidatorCollateralTokenBefore: liquidatorCollateralTokenBefore, - liquidatorCollateralSharesBefore: liquidatorCollateralSharesBefore + liquidatorDebtBefore: liquidatorDebtTotalBefore, + liquidatorCollateralBefore: liquidatorCollateralTotalBefore }); } + /// @dev Totals = token balance + supplied share assets. Caller passes pre-computed before-totals. function _assertLiquidationProfitable( ISpoke spoke, Types.ReserveInfo memory collateralInfo, Types.ReserveInfo memory debtInfo, address liquidator, - uint256 liquidatorDebtBalanceBefore, - uint256 liquidatorCollateralTokenBefore, - uint256 liquidatorCollateralSharesBefore + uint256 liquidatorDebtBefore, + uint256 liquidatorCollateralBefore ) private view { - // with the same asset, total position (tokens + shares) should increase after liquidation. - if (collateralInfo.underlying == debtInfo.underlying) { - _assertProfitableSameUnderlying({ - spoke: spoke, - collateralInfo: collateralInfo, - liquidator: liquidator, - liquidatorCollateralTokenBefore: liquidatorCollateralTokenBefore, - liquidatorCollateralSharesBefore: liquidatorCollateralSharesBefore - }); - } else { - _assertProfitableDiffUnderlying({ - spoke: spoke, - collateralInfo: collateralInfo, - liquidator: liquidator, - liquidatorCollateralTokenBefore: liquidatorCollateralTokenBefore, - liquidatorCollateralSharesBefore: liquidatorCollateralSharesBefore - }); - } - } - - /// @dev Same underlying: total position (tokens + shares) should increase after liquidation. - function _assertProfitableSameUnderlying( - ISpoke spoke, - Types.ReserveInfo memory collateralInfo, - address liquidator, - uint256 liquidatorCollateralTokenBefore, - uint256 liquidatorCollateralSharesBefore - ) private view { - uint256 totalBefore = liquidatorCollateralTokenBefore + liquidatorCollateralSharesBefore; - uint256 totalAfter = IERC20(collateralInfo.underlying).balanceOf(liquidator) + + uint256 liquidatorDebtAfter = IERC20(debtInfo.underlying).balanceOf(liquidator) + + spoke.getUserSuppliedAssets(debtInfo.reserveId, liquidator); + uint256 liquidatorCollateralAfter = IERC20(collateralInfo.underlying).balanceOf(liquidator) + spoke.getUserSuppliedAssets(collateralInfo.reserveId, liquidator); - assertGt( - totalAfter, - totalBefore, - 'LIQUIDATE: not profitable (same underlying) - total position did not increase' - ); - } - - /// @dev Different underlyings: compare oracle-normalized collateral gained vs debt spent. - /// @dev Different underlyings: oracle prices may be mocked during liquidation tests, - /// so cross-asset value comparison is unreliable. Just verify the liquidator received collateral. - function _assertProfitableDiffUnderlying( - ISpoke spoke, - Types.ReserveInfo memory collateralInfo, - address liquidator, - uint256 liquidatorCollateralTokenBefore, - uint256 liquidatorCollateralSharesBefore - ) private view { - uint256 collateralGained = IERC20(collateralInfo.underlying).balanceOf(liquidator) + - spoke.getUserSuppliedAssets(collateralInfo.reserveId, liquidator) - - liquidatorCollateralTokenBefore - - liquidatorCollateralSharesBefore; + uint256 debtSpent = liquidatorDebtAfter.delta(liquidatorDebtBefore); + uint256 collateralGained = liquidatorCollateralAfter.delta(liquidatorCollateralBefore); - assertGt(collateralGained, 0, 'LIQUIDATE: liquidator received no collateral'); + if (collateralInfo.underlying == debtInfo.underlying) { + // Same underlying: collateral/debt assets are the same, so debtSpent should == collateralGained and be positive + assertEq(debtSpent, collateralGained); // sanity check + assertGt( + collateralGained, + 0, + 'LIQUIDATE: not profitable (same underlying) - collateral gained <= debt spent' + ); + } else { + // Different underlyings: compare oracle-normalized $ values. + // Cross-multiply to avoid precision loss from division with extreme mocked prices: + // collGained * collPrice * 10^debtDecimals > debtSpent * debtPrice * 10^collDecimals + IAaveOracle oracle = IAaveOracle(spoke.ORACLE()); + uint256 collPrice = oracle.getReservePrice(collateralInfo.reserveId); + uint256 debtPrice = oracle.getReservePrice(debtInfo.reserveId); + + assertGt( + collateralGained * collPrice * (10 ** debtInfo.decimals), + debtSpent * debtPrice * (10 ** collateralInfo.decimals), + 'LIQUIDATE: not profitable - collateral $ value <= debt $ value' + ); + } } /// @notice Convert a token amount to its oracle-denominated value. diff --git a/src/dependencies/v4/Scenarios.sol b/src/dependencies/v4/Scenarios.sol index 1cf2c080..f763297f 100644 --- a/src/dependencies/v4/Scenarios.sol +++ b/src/dependencies/v4/Scenarios.sol @@ -337,39 +337,26 @@ abstract contract Scenarios is Helpers { skip(skipDays * 1 days); // Verify health factor is below 1 after making liquidatable - ISpoke.UserAccountData memory accountData = spoke.getUserAccountData(collateralSupplier); assertLt( - accountData.healthFactor, + spoke.getUserAccountData(collateralSupplier).healthFactor, HEALTH_FACTOR_LIQUIDATION_THRESHOLD, 'HEALTH: should be below 1 for liquidation' ); address liquidator = vm.randomAddress(); uint256 snapshotBeforeLiquidation = vm.snapshotState(); + bool receiveSharesEnabled = vm.randomBool(); - bool receiveSharesEnabled = spoke - .getReserveConfig(collateralInfo.reserveId) - .receiveSharesEnabled; - - // Partial liquidation — only if no dust remains + // Partial liquidation _testPartialLiquidation({ spoke: spoke, collateralInfo: collateralInfo, testAssetInfo: testAssetInfo, liquidator: liquidator, borrower: collateralSupplier, - receiveShares: false + receiveShares: receiveSharesEnabled }); - if (receiveSharesEnabled) { - _testPartialLiquidation({ - spoke: spoke, - collateralInfo: collateralInfo, - testAssetInfo: testAssetInfo, - liquidator: liquidator, - borrower: collateralSupplier, - receiveShares: true - }); - } + vm.revertToState(snapshotBeforeLiquidation); // Full liquidation - receive underlying _liquidationCall({ From a9d4c6ecb37ad8d3f011fde218e9c7b2bdcd33ec Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:13:29 -0500 Subject: [PATCH 09/29] fix: pr comments --- packages/aave-helpers-js/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aave-helpers-js/package.json b/packages/aave-helpers-js/package.json index 47b04a72..0133430c 100644 --- a/packages/aave-helpers-js/package.json +++ b/packages/aave-helpers-js/package.json @@ -5,9 +5,9 @@ "type": "module", "exports": { ".": { - "types": "./dist/index.d.ts", "import": "./dist/index.mjs", - "require": "./dist/index.cjs" + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" } }, "main": "./dist/index.cjs", From b8d73f86e271e29c8e3020df54ea8cc935a38930 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:47:14 -0500 Subject: [PATCH 10/29] fix: edge cases on liquidations tests/assertions --- src/dependencies/v4/Scenarios.sol | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/dependencies/v4/Scenarios.sol b/src/dependencies/v4/Scenarios.sol index f763297f..fd7bfb3e 100644 --- a/src/dependencies/v4/Scenarios.sol +++ b/src/dependencies/v4/Scenarios.sol @@ -39,14 +39,14 @@ abstract contract Scenarios is Helpers { vm.mockCall( oracle, abi.encodeWithSelector(IPriceOracle.getReservePrice.selector, i), - abi.encode(currentPrice / 1000) + abi.encode(currentPrice / 100) ); } else if (userDebt > 0 && userSupply == 0) { uint256 currentPrice = IAaveOracle(oracle).getReservePrice(i); vm.mockCall( oracle, abi.encodeWithSelector(IPriceOracle.getReservePrice.selector, i), - abi.encode(currentPrice * 1000) + abi.encode(currentPrice * 100) ); } } @@ -345,7 +345,7 @@ abstract contract Scenarios is Helpers { address liquidator = vm.randomAddress(); uint256 snapshotBeforeLiquidation = vm.snapshotState(); - bool receiveSharesEnabled = vm.randomBool(); + bool receiveShares = vm.randomBool(); // Partial liquidation _testPartialLiquidation({ @@ -354,7 +354,7 @@ abstract contract Scenarios is Helpers { testAssetInfo: testAssetInfo, liquidator: liquidator, borrower: collateralSupplier, - receiveShares: receiveSharesEnabled + receiveShares: receiveShares }); vm.revertToState(snapshotBeforeLiquidation); @@ -371,7 +371,7 @@ abstract contract Scenarios is Helpers { vm.revertToState(snapshotBeforeLiquidation); // Full liquidation - receive shares (only if enabled on collateral reserve) - if (receiveSharesEnabled) { + if (receiveShares) { _liquidationCall({ spoke: spoke, collateralInfo: collateralInfo, @@ -403,16 +403,16 @@ abstract contract Scenarios is Helpers { uint256 totalDebt = spoke.getUserTotalDebt(testAssetInfo.reserveId, borrower); uint256 totalCollateral = spoke.getUserSuppliedAssets(collateralInfo.reserveId, borrower); // only execute partial liquidations above $1.5k - uint256 liquidationThreshold = 1_500; + uint256 liquidationDollarThreshold = 1_500; uint256 minDebtAssets = _getTokenAmountByDollarValue({ oracleAddr: oracleAddr, reserveInfo: testAssetInfo, - dollarValue: liquidationThreshold + dollarValue: liquidationDollarThreshold }); uint256 minCollateralAssets = _getTokenAmountByDollarValue({ oracleAddr: oracleAddr, reserveInfo: collateralInfo, - dollarValue: liquidationThreshold + dollarValue: liquidationDollarThreshold }); // Skip if either debt or collateral is too small — partial liq leads to dust @@ -426,7 +426,7 @@ abstract contract Scenarios is Helpers { uint256 partialDebt = _getTokenAmountByDollarValue({ oracleAddr: oracleAddr, reserveInfo: testAssetInfo, - dollarValue: vm.randomUint(1, 400) + dollarValue: vm.randomUint(100, 400) }); // simple check - ensure at least 1 share worth of debt assets is liquidated // technically possible to liquidate less if premium debt exists, but serves as a basic check From 013f4915bd734304ca09cabb4ca215aaf593613e Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:49:56 -0500 Subject: [PATCH 11/29] feat: aave-helpers-js change set --- .changeset/cool-feet-fall.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cool-feet-fall.md diff --git a/.changeset/cool-feet-fall.md b/.changeset/cool-feet-fall.md new file mode 100644 index 00000000..1cb73395 --- /dev/null +++ b/.changeset/cool-feet-fall.md @@ -0,0 +1,5 @@ +--- +'@aave-dao/aave-helpers-js': minor +--- + +Add Aave V4 snapshot diff support and CLI command From 63d6bcb49958d2729f3f6d4223781e04bd9a91d0 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Wed, 22 Apr 2026 00:11:36 -0500 Subject: [PATCH 12/29] test: fix assertion; validate decimals; clean up --- src/dependencies/v4/Actions.sol | 29 +++-- src/dependencies/v4/GatewayScenarios.sol | 10 +- src/dependencies/v4/Helpers.sol | 2 + src/dependencies/v4/Scenarios.sol | 4 +- src/dependencies/v4/TokenizationActions.sol | 118 ++++++++++++------ src/dependencies/v4/TokenizationScenarios.sol | 5 + src/dependencies/v4/V4DiffWriter.sol | 2 +- 7 files changed, 115 insertions(+), 55 deletions(-) diff --git a/src/dependencies/v4/Actions.sol b/src/dependencies/v4/Actions.sol index d058678d..5ce7f0f5 100644 --- a/src/dependencies/v4/Actions.sol +++ b/src/dependencies/v4/Actions.sol @@ -369,22 +369,37 @@ abstract contract Actions is CommonTestBase { if (amount >= snapshotBefore.user.totalDebt) { assertEq(snapshotAfter.user.totalDebt, 0, 'REPAY: user debt should be zero'); } else { + assertGe( + snapshotBefore.user.totalDebt, + snapshotAfter.user.totalDebt, + 'REPAY: user debt did not decrease' + ); assertApproxEqAbs( - snapshotAfter.user.totalDebt.delta(snapshotBefore.user.totalDebt), + snapshotBefore.user.totalDebt - snapshotAfter.user.totalDebt, amount, 2, 'REPAY: user debt mismatch' ); } // Hub spoke - up to 2 wei diff due to premium/drawn debt + assertGe( + snapshotBefore.spokeOnHub.totalDebt, + snapshotAfter.spokeOnHub.totalDebt, + 'REPAY: hub debt did not decrease' + ); assertApproxEqAbs( - snapshotBefore.spokeOnHub.totalDebt.delta(snapshotAfter.spokeOnHub.totalDebt), + snapshotBefore.spokeOnHub.totalDebt - snapshotAfter.spokeOnHub.totalDebt, effectiveRepayAmount, 2, 'REPAY: hub debt mismatch' ); + assertGe( + snapshotBefore.spokeOnHub.drawnShares, + snapshotAfter.spokeOnHub.drawnShares, + 'REPAY: hub drawn shares did not decrease' + ); assertEq( - snapshotBefore.spokeOnHub.drawnShares.delta(snapshotAfter.spokeOnHub.drawnShares), + snapshotBefore.spokeOnHub.drawnShares - snapshotAfter.spokeOnHub.drawnShares, expectedRestoredShares, 'REPAY: hub drawn shares mismatch' ); @@ -481,8 +496,8 @@ abstract contract Actions is CommonTestBase { ); // hub collateral can remain unchanged if receiveShares is true assertLe( - collateralSnapshotAfter.spokeOnHub.totalDebt, - collateralSnapshotAfter.spokeOnHub.totalDebt, + collateralSnapshotAfter.spokeOnHub.collateralAssets, + collateralSnapshotBefore.spokeOnHub.collateralAssets, 'LIQUIDATE: hub collateral did not decrease or stay the same' ); assertLe( @@ -500,10 +515,10 @@ abstract contract Actions is CommonTestBase { borrowerAccountDataBefore.totalCollateralValue, 'LIQUIDATE: borrower collateral value did not decrease or stay the same' ); - assertLe( + assertLt( spoke.getUserAccountData(borrower).totalDebtValueRay, borrowerAccountDataBefore.totalDebtValueRay, - 'LIQUIDATE: borrower debt value did not decrease or stay the same' + 'LIQUIDATE: borrower debt value did not decrease' ); // Liquidation profitability: collateral value received > debt value paid diff --git a/src/dependencies/v4/GatewayScenarios.sol b/src/dependencies/v4/GatewayScenarios.sol index cff4b904..763ccb1e 100644 --- a/src/dependencies/v4/GatewayScenarios.sol +++ b/src/dependencies/v4/GatewayScenarios.sol @@ -272,7 +272,7 @@ abstract contract GatewayScenarios is Helpers { assertEq(amountWithdrawn, withdrawAmount, 'NATIVE_WITHDRAW: amount mismatch'); } assertEq( - user.balance.delta(ethBefore), + user.balance - ethBefore, expectedWithdrawnAmount, 'NATIVE_WITHDRAW: user ETH mismatch' ); @@ -281,24 +281,24 @@ abstract contract GatewayScenarios is Helpers { Types.PositionSnapshot memory snapshotAfter = _getPositionSnapshot(spoke, wethInfo, user); assertApproxEqAbs( - snapshotBefore.user.collateralAssets.delta(snapshotAfter.user.collateralAssets), + snapshotBefore.user.collateralAssets - snapshotAfter.user.collateralAssets, expectedWithdrawnAmount, 1, 'NATIVE_WITHDRAW: user assets mismatch' ); assertEq( - snapshotBefore.user.collateralShares.delta(snapshotAfter.user.collateralShares), + snapshotBefore.user.collateralShares - snapshotAfter.user.collateralShares, sharesWithdrawn, 'NATIVE_WITHDRAW: user shares mismatch' ); assertApproxEqAbs( - snapshotBefore.spokeOnHub.collateralAssets.delta(snapshotAfter.spokeOnHub.collateralAssets), + snapshotBefore.spokeOnHub.collateralAssets - snapshotAfter.spokeOnHub.collateralAssets, expectedWithdrawnAmount, 1, 'NATIVE_WITHDRAW: hub assets mismatch' ); assertEq( - snapshotBefore.spokeOnHub.collateralShares.delta(snapshotAfter.spokeOnHub.collateralShares), + snapshotBefore.spokeOnHub.collateralShares - snapshotAfter.spokeOnHub.collateralShares, sharesWithdrawn, 'NATIVE_WITHDRAW: hub shares mismatch' ); diff --git a/src/dependencies/v4/Helpers.sol b/src/dependencies/v4/Helpers.sol index 4519a043..f9987a9f 100644 --- a/src/dependencies/v4/Helpers.sol +++ b/src/dependencies/v4/Helpers.sol @@ -368,6 +368,8 @@ abstract contract Helpers is Actions { addCap: config.addCap, drawCap: config.drawCap }); + // clear mocked call from _deployMockedHubConfigurator + vm.clearMockedCalls(); } function _setSpokeCapsToMaxForAllReserves(ISpoke spoke, bool maxAddCap, bool maxDrawCap) private { diff --git a/src/dependencies/v4/Scenarios.sol b/src/dependencies/v4/Scenarios.sol index fd7bfb3e..28dcb6c2 100644 --- a/src/dependencies/v4/Scenarios.sol +++ b/src/dependencies/v4/Scenarios.sol @@ -609,7 +609,7 @@ abstract contract Scenarios is Helpers { // Remove addCaps so enough collateral can be supplied to borrow up to drawCap _setAddCapsToMax(spoke); - console.log('TEST_DRAW_CAP: drawCap=%e', drawCap); + _logAction('TEST_DRAW_CAP', 'drawCap', drawCap); address borrower = vm.randomAddress(); uint256 drawCapScaled = uint256(drawCap) * 10 ** reserveInfo.decimals; uint256 currentDebt = spoke.getReserveTotalDebt(reserveInfo.reserveId); @@ -619,7 +619,7 @@ abstract contract Scenarios is Helpers { uint256 room = drawCapScaled - currentDebt; - // Supply the debt asset itself as collateral (3x room for borrow headroom) + liquidity + // Supply the debt asset itself as collateral (10x room for borrow headroom) + liquidity uint256 collateralAmount = room * 10; _supply({spoke: spoke, reserveInfo: reserveInfo, user: borrower, amount: collateralAmount}); vm.prank(borrower); diff --git a/src/dependencies/v4/TokenizationActions.sol b/src/dependencies/v4/TokenizationActions.sol index 53adef0e..ef22778f 100644 --- a/src/dependencies/v4/TokenizationActions.sol +++ b/src/dependencies/v4/TokenizationActions.sol @@ -26,7 +26,7 @@ abstract contract TokenizationActions is Helpers { return Types.TokenizationSnapshot({ userShares: userShares, - userAssets: userShares > 0 ? tokenizationSpoke.convertToAssets(userShares) : 0, + userAssets: tokenizationSpoke.convertToAssets(userShares), totalShares: tokenizationSpoke.totalSupply(), totalAssets: tokenizationSpoke.totalAssets(), spokeOnHub: _getSpokeOnHubAccounting(ISpoke(address(tokenizationSpoke)), reserveInfo) @@ -40,6 +40,17 @@ abstract contract TokenizationActions is Helpers { assertEq(snapshot.spokeOnHub.drawnDebt, 0, 'TOKENIZATION: hub drawn debt should be zero'); assertEq(snapshot.spokeOnHub.drawnShares, 0, 'TOKENIZATION: hub drawn shares should be zero'); assertEq(snapshot.spokeOnHub.totalDebt, 0, 'TOKENIZATION: hub total debt should be zero'); + assertEq(snapshot.spokeOnHub.premiumDebt, 0, 'TOKENIZATION: hub premium debt should be zero'); + assertEq( + snapshot.spokeOnHub.premiumShares, + 0, + 'TOKENIZATION: hub premium shares should be zero' + ); + assertEq( + snapshot.spokeOnHub.premiumOffsetRay, + 0, + 'TOKENIZATION: hub premium offset should be zero' + ); } // ------------------------------------------------------------------------- @@ -56,6 +67,7 @@ abstract contract TokenizationActions is Helpers { reserveInfo, user ); + _assertTokenizationNoDebt(snapshotBefore); uint256 expectedShares = tokenizationSpoke.previewDeposit(assets); @@ -123,13 +135,14 @@ abstract contract TokenizationActions is Helpers { reserveInfo, user ); + _assertTokenizationNoDebt(snapshotBefore); uint256 expectedAssets = tokenizationSpoke.previewMint(shares); vm.startPrank(user); - // Add some extra assets to avoid rounding errors - deal2(reserveInfo.underlying, user, expectedAssets * 2); - IERC20(reserveInfo.underlying).forceApprove(address(tokenizationSpoke), expectedAssets * 2); + // previewMint rounds assets UP per EIP-4626, so the exact amount is sufficient + deal2(reserveInfo.underlying, user, expectedAssets); + IERC20(reserveInfo.underlying).forceApprove(address(tokenizationSpoke), expectedAssets); _logAction('TOKENIZATION_MINT', reserveInfo.symbol, shares); uint256 assetsDeposited = tokenizationSpoke.mint(shares, user); vm.stopPrank(); @@ -180,6 +193,7 @@ abstract contract TokenizationActions is Helpers { reserveInfo, user ); + _assertTokenizationNoDebt(snapshotBefore); uint256 expectedSharesBurned = tokenizationSpoke.previewWithdraw(assets); @@ -233,6 +247,7 @@ abstract contract TokenizationActions is Helpers { reserveInfo, user ); + _assertTokenizationNoDebt(snapshotBefore); uint256 expectedAssets = tokenizationSpoke.previewRedeem(shares); @@ -286,12 +301,12 @@ abstract contract TokenizationActions is Helpers { uint256 shares ) internal { address user = vm.addr(privateKey); - uint256 userSharesBefore = tokenizationSpoke.balanceOf(user); - uint256 totalAssetsBefore = tokenizationSpoke.totalAssets(); - uint256 hubCollateralBefore = IHubBase(reserveInfo.hub).getSpokeAddedAssets( - reserveInfo.assetId, - address(tokenizationSpoke) + Types.TokenizationSnapshot memory snapshotBefore = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user ); + _assertTokenizationNoDebt(snapshotBefore); uint256 assetsDeposited = _executeTokenizationMintWithSig({ tokenizationSpoke: tokenizationSpoke, @@ -301,26 +316,41 @@ abstract contract TokenizationActions is Helpers { shares: shares }); + Types.TokenizationSnapshot memory snapshotAfter = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + assertEq( - tokenizationSpoke.balanceOf(user) - userSharesBefore, + snapshotAfter.userShares - snapshotBefore.userShares, shares, 'TOKENIZATION_MINT_WITH_SIG: user shares mismatch' ); assertApproxEqAbs( - tokenizationSpoke.totalAssets() - totalAssetsBefore, + snapshotAfter.totalAssets - snapshotBefore.totalAssets, assetsDeposited, 1, 'TOKENIZATION_MINT_WITH_SIG: totalAssets mismatch' ); assertApproxEqAbs( - IHubBase(reserveInfo.hub).getSpokeAddedAssets( - reserveInfo.assetId, - address(tokenizationSpoke) - ) - hubCollateralBefore, + snapshotAfter.spokeOnHub.collateralAssets - snapshotBefore.spokeOnHub.collateralAssets, assetsDeposited, 1, 'TOKENIZATION_MINT_WITH_SIG: hub collateral assets mismatch' ); + { + uint256 expectedAddedShares = IHubBase(reserveInfo.hub).previewAddByAssets( + reserveInfo.assetId, + assetsDeposited + ); + assertEq( + snapshotAfter.spokeOnHub.collateralShares, + snapshotBefore.spokeOnHub.collateralShares + expectedAddedShares, + 'TOKENIZATION_MINT_WITH_SIG: hub collateral shares mismatch' + ); + } + _assertTokenizationNoDebt(snapshotAfter); } function _executeTokenizationMintWithSig( @@ -333,9 +363,9 @@ abstract contract TokenizationActions is Helpers { uint256 expectedAssets = tokenizationSpoke.previewMint(shares); _logAction('TOKENIZATION_MINT_WITH_SIG', reserveInfo.symbol, shares); - deal2(reserveInfo.underlying, user, expectedAssets * 2); + deal2(reserveInfo.underlying, user, expectedAssets); vm.prank(user); - IERC20(reserveInfo.underlying).forceApprove(address(tokenizationSpoke), expectedAssets * 2); + IERC20(reserveInfo.underlying).forceApprove(address(tokenizationSpoke), expectedAssets); uint192 nonceKey = tokenizationSpoke.PERMIT_NONCE_NAMESPACE(); uint256 nonce = tokenizationSpoke.nonces(user, nonceKey); @@ -385,12 +415,12 @@ abstract contract TokenizationActions is Helpers { uint256 shares ) internal { address user = vm.addr(userPrivateKey); - uint256 userSharesBefore = tokenizationSpoke.balanceOf(user); - uint256 totalAssetsBefore = tokenizationSpoke.totalAssets(); - uint256 hubCollateralBefore = IHubBase(reserveInfo.hub).getSpokeAddedAssets( - reserveInfo.assetId, - address(tokenizationSpoke) + Types.TokenizationSnapshot memory snapshotBefore = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user ); + _assertTokenizationNoDebt(snapshotBefore); uint256 assetsReceived = _executeTokenizationRedeemWithSig({ tokenizationSpoke: tokenizationSpoke, @@ -400,34 +430,37 @@ abstract contract TokenizationActions is Helpers { shares: shares }); + Types.TokenizationSnapshot memory snapshotAfter = _getTokenizationSnapshot( + tokenizationSpoke, + reserveInfo, + user + ); + assertEq( - userSharesBefore - tokenizationSpoke.balanceOf(user), + snapshotBefore.userShares - snapshotAfter.userShares, shares, 'TOKENIZATION_REDEEM_WITH_SIG: user shares mismatch' ); - if (shares == userSharesBefore) { + if (shares == snapshotBefore.userShares) { assertEq( - tokenizationSpoke.balanceOf(user), + snapshotAfter.userShares, 0, 'TOKENIZATION_REDEEM_WITH_SIG: user shares should be zero' ); } assertApproxEqAbs( - totalAssetsBefore - tokenizationSpoke.totalAssets(), + snapshotBefore.totalAssets - snapshotAfter.totalAssets, assetsReceived, 1, 'TOKENIZATION_REDEEM_WITH_SIG: totalAssets mismatch' ); assertApproxEqAbs( - hubCollateralBefore - - IHubBase(reserveInfo.hub).getSpokeAddedAssets( - reserveInfo.assetId, - address(tokenizationSpoke) - ), + snapshotBefore.spokeOnHub.collateralAssets - snapshotAfter.spokeOnHub.collateralAssets, assetsReceived, 1, 'TOKENIZATION_REDEEM_WITH_SIG: hub collateral assets mismatch' ); + _assertTokenizationNoDebt(snapshotAfter); } function _executeTokenizationRedeemWithSig( @@ -491,12 +524,16 @@ abstract contract TokenizationActions is Helpers { uint256 deadline ) internal view returns (bytes32) { // Query nonces and DOMAIN_SEPARATOR from the underlying ERC20Permit token - (, bytes memory nonceData) = underlying.staticcall( + (bool nonceOk, bytes memory nonceData) = underlying.staticcall( abi.encodeWithSignature('nonces(address)', owner) ); + require(nonceOk && nonceData.length == 32, 'PERMIT: underlying missing nonces(address)'); uint256 nonce = abi.decode(nonceData, (uint256)); - (, bytes memory dsData) = underlying.staticcall(abi.encodeWithSignature('DOMAIN_SEPARATOR()')); + (bool dsOk, bytes memory dsData) = underlying.staticcall( + abi.encodeWithSignature('DOMAIN_SEPARATOR()') + ); + require(dsOk && dsData.length == 32, 'PERMIT: underlying missing DOMAIN_SEPARATOR()'); bytes32 domainSeparator = abi.decode(dsData, (bytes32)); bytes32 permitTypehash = keccak256( @@ -520,17 +557,18 @@ abstract contract TokenizationActions is Helpers { reserveInfo, user ); + _assertTokenizationNoDebt(snapshotBefore); deal2(reserveInfo.underlying, user, assets); uint256 deadline = vm.getBlockTimestamp() + 1 hours; - bytes32 digest = _buildUnderlyingPermitDigest( - reserveInfo.underlying, - user, - address(tokenizationSpoke), - assets, - deadline - ); + bytes32 digest = _buildUnderlyingPermitDigest({ + underlying: reserveInfo.underlying, + owner: user, + spender: address(tokenizationSpoke), + value: assets, + deadline: deadline + }); (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); vm.prank(user); diff --git a/src/dependencies/v4/TokenizationScenarios.sol b/src/dependencies/v4/TokenizationScenarios.sol index c7e68fc4..25f1b30f 100644 --- a/src/dependencies/v4/TokenizationScenarios.sol +++ b/src/dependencies/v4/TokenizationScenarios.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import 'forge-std/Test.sol'; import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; +import {IERC20Metadata} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; import {ITokenizationSpoke, IHub} from 'aave-address-book/AaveV4.sol'; import {Types} from 'src/dependencies/v4/Types.sol'; @@ -21,6 +22,10 @@ abstract contract TokenizationScenarios is TokenizationActions { uint16 assetId = uint16(tokenizationSpoke.assetId()); address underlying = tokenizationSpoke.asset(); uint8 decimals = tokenizationSpoke.decimals(); + require( + decimals == IERC20Metadata(underlying).decimals(), + 'TOKENIZATION: spoke decimals must match underlying' + ); string memory symbol = _safeSymbol(underlying); return diff --git a/src/dependencies/v4/V4DiffWriter.sol b/src/dependencies/v4/V4DiffWriter.sol index 006a7abf..f3acb2f2 100644 --- a/src/dependencies/v4/V4DiffWriter.sol +++ b/src/dependencies/v4/V4DiffWriter.sol @@ -16,7 +16,7 @@ library V4DiffWriter { string memory path = string.concat('./reports/', reportName, '.json'); vm.writeFile( path, - '{ "spokeReserves": {}, "spokeLiquidationConfigs": {}, "hubAssets": {}, "spokeCaps": {}, "raw": {} }' + '{ "spokeReserves": {}, "spokeLiquidationConfigs": {}, "hubAssets": {}, "spokeCaps": {} }' ); vm.serializeUint('root', 'chainId', block.chainid); From e42c3fc9a6b48189bd77781bf4f90e731ea60324 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Mon, 4 May 2026 14:56:16 -0500 Subject: [PATCH 13/29] test: relax bounds on asset amts --- src/dependencies/v4/Actions.sol | 12 ++++++++---- src/dependencies/v4/GatewayScenarios.sol | 12 ++++++------ src/dependencies/v4/TokenizationActions.sol | 14 +++++++------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/dependencies/v4/Actions.sol b/src/dependencies/v4/Actions.sol index 5ce7f0f5..42c0c504 100644 --- a/src/dependencies/v4/Actions.sol +++ b/src/dependencies/v4/Actions.sol @@ -189,7 +189,7 @@ abstract contract Actions is CommonTestBase { assertApproxEqAbs( snapshotAfter.user.collateralAssets, snapshotBefore.user.collateralAssets + amount, - 1, + 2, 'SUPPLY: user assets mismatch' ); assertEq( @@ -201,7 +201,7 @@ abstract contract Actions is CommonTestBase { assertApproxEqAbs( snapshotAfter.spokeOnHub.collateralAssets, snapshotBefore.spokeOnHub.collateralAssets + amount, - 1, + 2, 'SUPPLY: hub assets mismatch' ); uint256 expectedAddedShares = IHubBase(reserveInfo.hub).previewAddByAssets( @@ -249,7 +249,7 @@ abstract contract Actions is CommonTestBase { assertApproxEqAbs( snapshotAfter.user.collateralAssets, snapshotBefore.user.collateralAssets - withdrawnAmount, - 1, + 2, 'WITHDRAW: user assets mismatch' ); assertEq( @@ -262,7 +262,7 @@ abstract contract Actions is CommonTestBase { assertApproxEqAbs( snapshotBefore.spokeOnHub.collateralAssets - snapshotAfter.spokeOnHub.collateralAssets, withdrawnAmount, - 1, + 2, 'WITHDRAW: hub assets mismatch' ); assertEq( @@ -270,6 +270,10 @@ abstract contract Actions is CommonTestBase { returnedShares, 'WITHDRAW: hub shares mismatch' ); + + // Health factor must remain above liquidation threshold after withdraw + uint256 healthFactor = spoke.getUserAccountData(user).healthFactor; + assertGt(healthFactor, HEALTH_FACTOR_LIQUIDATION_THRESHOLD, 'WITHDRAW: health factor below 1'); } function _borrow( diff --git a/src/dependencies/v4/GatewayScenarios.sol b/src/dependencies/v4/GatewayScenarios.sol index 763ccb1e..aa3c8914 100644 --- a/src/dependencies/v4/GatewayScenarios.sol +++ b/src/dependencies/v4/GatewayScenarios.sol @@ -149,7 +149,7 @@ abstract contract GatewayScenarios is Helpers { assertApproxEqAbs( snapshotAfter.user.collateralAssets.delta(snapshotBefore.user.collateralAssets), amountSupplied, - 1, + 2, 'NATIVE_SUPPLY: user assets mismatch' ); assertEq( @@ -160,7 +160,7 @@ abstract contract GatewayScenarios is Helpers { assertApproxEqAbs( snapshotAfter.spokeOnHub.collateralAssets.delta(snapshotBefore.spokeOnHub.collateralAssets), amountSupplied, - 1, + 2, 'NATIVE_SUPPLY: hub assets mismatch' ); vm.revertToState(snapshot); @@ -194,7 +194,7 @@ abstract contract GatewayScenarios is Helpers { assertApproxEqAbs( snapshotAfter.user.collateralAssets.delta(snapshotBefore.user.collateralAssets), amountSupplied, - 1, + 2, 'NATIVE_SUPPLY_AS_COLLATERAL: user assets mismatch' ); assertEq( @@ -205,7 +205,7 @@ abstract contract GatewayScenarios is Helpers { assertApproxEqAbs( snapshotAfter.spokeOnHub.collateralAssets.delta(snapshotBefore.spokeOnHub.collateralAssets), amountSupplied, - 1, + 2, 'NATIVE_SUPPLY_AS_COLLATERAL: hub assets mismatch' ); } @@ -283,7 +283,7 @@ abstract contract GatewayScenarios is Helpers { assertApproxEqAbs( snapshotBefore.user.collateralAssets - snapshotAfter.user.collateralAssets, expectedWithdrawnAmount, - 1, + 2, 'NATIVE_WITHDRAW: user assets mismatch' ); assertEq( @@ -294,7 +294,7 @@ abstract contract GatewayScenarios is Helpers { assertApproxEqAbs( snapshotBefore.spokeOnHub.collateralAssets - snapshotAfter.spokeOnHub.collateralAssets, expectedWithdrawnAmount, - 1, + 2, 'NATIVE_WITHDRAW: hub assets mismatch' ); assertEq( diff --git a/src/dependencies/v4/TokenizationActions.sol b/src/dependencies/v4/TokenizationActions.sol index ef22778f..38f55821 100644 --- a/src/dependencies/v4/TokenizationActions.sol +++ b/src/dependencies/v4/TokenizationActions.sol @@ -107,7 +107,7 @@ abstract contract TokenizationActions is Helpers { assertApproxEqAbs( snapshotAfter.spokeOnHub.collateralAssets, snapshotBefore.spokeOnHub.collateralAssets + assets, - 1, + 2, 'TOKENIZATION_DEPOSIT: hub collateral assets mismatch' ); { @@ -176,7 +176,7 @@ abstract contract TokenizationActions is Helpers { assertApproxEqAbs( snapshotAfter.spokeOnHub.collateralAssets, snapshotBefore.spokeOnHub.collateralAssets + assetsDeposited, - 1, + 2, 'TOKENIZATION_MINT: hub collateral assets mismatch' ); _assertTokenizationNoDebt(snapshotAfter); @@ -230,7 +230,7 @@ abstract contract TokenizationActions is Helpers { assertApproxEqAbs( snapshotBefore.spokeOnHub.collateralAssets - snapshotAfter.spokeOnHub.collateralAssets, assets, - 1, + 2, 'TOKENIZATION_WITHDRAW: hub collateral assets mismatch' ); _assertTokenizationNoDebt(snapshotAfter); @@ -288,7 +288,7 @@ abstract contract TokenizationActions is Helpers { assertApproxEqAbs( snapshotBefore.spokeOnHub.collateralAssets - snapshotAfter.spokeOnHub.collateralAssets, assetsReceived, - 1, + 2, 'TOKENIZATION_REDEEM: hub collateral assets mismatch' ); _assertTokenizationNoDebt(snapshotAfter); @@ -336,7 +336,7 @@ abstract contract TokenizationActions is Helpers { assertApproxEqAbs( snapshotAfter.spokeOnHub.collateralAssets - snapshotBefore.spokeOnHub.collateralAssets, assetsDeposited, - 1, + 2, 'TOKENIZATION_MINT_WITH_SIG: hub collateral assets mismatch' ); { @@ -457,7 +457,7 @@ abstract contract TokenizationActions is Helpers { assertApproxEqAbs( snapshotBefore.spokeOnHub.collateralAssets - snapshotAfter.spokeOnHub.collateralAssets, assetsReceived, - 1, + 2, 'TOKENIZATION_REDEEM_WITH_SIG: hub collateral assets mismatch' ); _assertTokenizationNoDebt(snapshotAfter); @@ -598,7 +598,7 @@ abstract contract TokenizationActions is Helpers { assertApproxEqAbs( snapshotAfter.spokeOnHub.collateralAssets, snapshotBefore.spokeOnHub.collateralAssets + assets, - 1, + 2, 'TOKENIZATION_DEPOSIT_WITH_PERMIT: hub collateral assets mismatch' ); _assertTokenizationNoDebt(snapshotAfter); From 8bb437522854e472efcc9dac126c0e99522f69d1 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Mon, 4 May 2026 16:27:01 -0500 Subject: [PATCH 14/29] feat: formatting on caps vals w units --- .../__tests__/protocol-diff-v4.spec.ts | 31 ++++++++++++++----- packages/aave-helpers-js/formatters-v4.ts | 9 ++++++ .../aave-helpers-js/sections/spoke-caps.ts | 8 +++-- packages/aave-helpers-js/snapshot-types-v4.ts | 4 +-- src/dependencies/v4/V4DiffWriter.sol | 4 +-- 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts index 72cec786..e2f76891 100644 --- a/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts +++ b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts @@ -73,8 +73,8 @@ function makeSnapshot(overrides?: Partial): AaveV4Snapshot { spokeCaps: { [`${HUB_ADDR}_0_${SPOKE_ADDR}`]: { assetSymbol: 'WETH', - addCap: '1000000', - drawCap: '500000', + addCap: 1000000, + drawCap: 500000, riskPremiumThreshold: 100, active: true, halted: false, @@ -184,7 +184,24 @@ describe('BPS formatting via formatV4Value', () => { it('falls back to raw string for unformatted fields', () => { expect(formatV4Value('spokeLiq', 'maxUserReservesLimit', 128, ctx)).toBe('128'); - expect(formatV4Value('spokeCap', 'addCap', '1000000', ctx)).toBe('1000000'); + }); + + it('formats spoke cap uint40 fields with thousands separators and asset symbol', () => { + const capCtx = { + ...ctx, + spokeCap: { + assetSymbol: 'USDT', + addCap: 0, + drawCap: 0, + riskPremiumThreshold: 0, + active: true, + halted: false, + }, + }; + expect(formatV4Value('spokeCap', 'addCap', 1000000, capCtx)).toBe('1,000,000 USDT'); + expect(formatV4Value('spokeCap', 'drawCap', 1880000, capCtx)).toBe('1,880,000 USDT'); + // Falls back gracefully when symbol unavailable + expect(formatV4Value('spokeCap', 'addCap', 1000000, ctx)).toBe('1,000,000'); }); }); @@ -275,7 +292,7 @@ describe('diffV4Snapshots', () => { const capKey = `${HUB_ADDR}_0_${SPOKE_ADDR}`; after.spokeCaps[capKey] = { ...after.spokeCaps[capKey], - addCap: '2000000', + addCap: 2000000, halted: true, }; @@ -357,8 +374,8 @@ describe('diffV4Snapshots', () => { const newCapKey = `${HUB_ADDR}_1_${SPOKE_ADDR}`; after.spokeCaps[newCapKey] = { assetSymbol: 'USDC', - addCap: '500000', - drawCap: '250000', + addCap: 500000, + drawCap: 250000, riskPremiumThreshold: 50, active: true, halted: false, @@ -436,7 +453,7 @@ describe('diffV4Snapshots', () => { const capKey = `${HUB_ADDR}_0_${SPOKE_ADDR}`; after.spokeCaps[capKey] = { ...after.spokeCaps[capKey], - addCap: '9999999', + addCap: 9999999, }; const md = await diffV4Snapshots(before, after); diff --git a/packages/aave-helpers-js/formatters-v4.ts b/packages/aave-helpers-js/formatters-v4.ts index 63975cc7..9ab4a8d0 100644 --- a/packages/aave-helpers-js/formatters-v4.ts +++ b/packages/aave-helpers-js/formatters-v4.ts @@ -12,6 +12,7 @@ import type { export interface V4FormatterContext { chainId: number; + spokeCap?: V4SpokeCap; } export type FieldFormatter = (value: T, ctx: V4FormatterContext) => string; @@ -130,6 +131,9 @@ type SpokeCapKey = keyof V4SpokeCap; const SPOKE_CAP_BOOL_FIELDS: readonly SpokeCapKey[] = ['active', 'halted'] as const; +/** uint40 token-unit cap fields — formatted with thousands separators + asset symbol. */ +const SPOKE_CAP_TOKEN_AMOUNT_FIELDS: readonly SpokeCapKey[] = ['addCap', 'drawCap'] as const; + export const spokeCapFormatters: Partial<{ [K in SpokeCapKey]: FieldFormatter; }> = {}; @@ -138,6 +142,11 @@ for (const field of SPOKE_CAP_BOOL_FIELDS) { (spokeCapFormatters[field] as FieldFormatter) = (value) => boolToMarkdown(value); } +for (const field of SPOKE_CAP_TOKEN_AMOUNT_FIELDS) { + (spokeCapFormatters[field] as FieldFormatter) = (value, ctx) => + `${value.toLocaleString('en-US')} ${ctx.spokeCap?.assetSymbol ?? ''}`.trim(); +} + // --- Spoke Liquidation Config formatters --- type SpokeLiqKey = keyof V4SpokeLiquidationConfig; diff --git a/packages/aave-helpers-js/sections/spoke-caps.ts b/packages/aave-helpers-js/sections/spoke-caps.ts index 0e157697..4b051303 100644 --- a/packages/aave-helpers-js/sections/spoke-caps.ts +++ b/packages/aave-helpers-js/sections/spoke-caps.ts @@ -49,11 +49,12 @@ function renderNewCap( spokeAddr: string, ctx: V4FormatterContext ): string { + const capCtx: V4FormatterContext = { ...ctx, spokeCap: cap }; let md = capHeader(cap, hubAddr, assetId, spokeAddr, ctx.chainId); md += '**NEW SPOKE**\n\n'; md += '| description | value |\n| --- | --- |\n'; for (const key of FIELD_ORDER) { - md += `| ${key} | ${formatV4Value('spokeCap', key, cap[key], ctx)} |\n`; + md += `| ${key} | ${formatV4Value('spokeCap', key, cap[key], capCtx)} |\n`; } return md + '\n'; } @@ -66,13 +67,14 @@ function renderCapDiff( spokeAddr: string, ctx: V4FormatterContext ): string { + const capCtx: V4FormatterContext = { ...ctx, spokeCap: after }; const rows: string[] = []; for (const key of FIELD_ORDER) { const bVal = before[key]; const aVal = after[key]; if (String(bVal) === String(aVal)) continue; - const fromFmt = formatV4Value('spokeCap', key, bVal, ctx); - const toFmt = formatV4Value('spokeCap', key, aVal, ctx); + const fromFmt = formatV4Value('spokeCap', key, bVal, capCtx); + const toFmt = formatV4Value('spokeCap', key, aVal, capCtx); rows.push(`| ${key} | ${fromFmt} | ${toFmt} |`); } if (rows.length === 0) return ''; diff --git a/packages/aave-helpers-js/snapshot-types-v4.ts b/packages/aave-helpers-js/snapshot-types-v4.ts index 493538b5..591e838f 100644 --- a/packages/aave-helpers-js/snapshot-types-v4.ts +++ b/packages/aave-helpers-js/snapshot-types-v4.ts @@ -64,8 +64,8 @@ export type V4HubAsset = z.infer; export const v4SpokeCapSchema = z.object({ assetSymbol: z.string(), - addCap: z.string(), // uint40 serialized as string - drawCap: z.string(), // uint40 serialized as string + addCap: z.number(), // uint40 — fits in JS safe int + drawCap: z.number(), // uint40 — fits in JS safe int riskPremiumThreshold: z.number(), active: z.boolean(), halted: z.boolean(), diff --git a/src/dependencies/v4/V4DiffWriter.sol b/src/dependencies/v4/V4DiffWriter.sol index f3acb2f2..3685c93c 100644 --- a/src/dependencies/v4/V4DiffWriter.sol +++ b/src/dependencies/v4/V4DiffWriter.sol @@ -162,8 +162,8 @@ library V4DiffWriter { ); vm.serializeJson(k, '{}'); vm.serializeString(k, 'assetSymbol', caps[i].assetSymbol); - vm.serializeString(k, 'addCap', vm.toString(uint256(caps[i].addCap))); - vm.serializeString(k, 'drawCap', vm.toString(uint256(caps[i].drawCap))); + vm.serializeUint(k, 'addCap', uint256(caps[i].addCap)); + vm.serializeUint(k, 'drawCap', uint256(caps[i].drawCap)); vm.serializeUint(k, 'riskPremiumThreshold', caps[i].riskPremiumThreshold); vm.serializeBool(k, 'active', caps[i].active); string memory obj = vm.serializeBool(k, 'halted', caps[i].halted); From f6d8f1c2829c15bce9810bfa21c142262c5247f3 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Mon, 4 May 2026 18:19:53 -0500 Subject: [PATCH 15/29] fix: format RP threshold bps --- packages/aave-helpers-js/formatters-v4.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/aave-helpers-js/formatters-v4.ts b/packages/aave-helpers-js/formatters-v4.ts index 9ab4a8d0..3080804b 100644 --- a/packages/aave-helpers-js/formatters-v4.ts +++ b/packages/aave-helpers-js/formatters-v4.ts @@ -147,6 +147,8 @@ for (const field of SPOKE_CAP_TOKEN_AMOUNT_FIELDS) { `${value.toLocaleString('en-US')} ${ctx.spokeCap?.assetSymbol ?? ''}`.trim(); } +spokeCapFormatters['riskPremiumThreshold'] = (value) => formatBps(value); + // --- Spoke Liquidation Config formatters --- type SpokeLiqKey = keyof V4SpokeLiquidationConfig; From 364a0c5cfd071b40bf1257bf4a351c0f94ae9094 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Wed, 6 May 2026 21:00:58 -0500 Subject: [PATCH 16/29] fix: bump helpers js dep --- src/dependencies/v4/SnapshotV4.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dependencies/v4/SnapshotV4.sol b/src/dependencies/v4/SnapshotV4.sol index 721ee13a..c0bef2aa 100644 --- a/src/dependencies/v4/SnapshotV4.sol +++ b/src/dependencies/v4/SnapshotV4.sol @@ -41,7 +41,7 @@ abstract contract SnapshotV4 is Helpers { string[] memory inputs = new string[](7); inputs[0] = 'npx'; - inputs[1] = '@aave-dao/aave-helpers-js@^1.0.1'; + inputs[1] = '@aave-dao/aave-helpers-js@^1.1.0'; inputs[2] = 'diff-v4-snapshots'; inputs[3] = beforePath; inputs[4] = afterPath; From 84be1baf8f0c51d7eff9641bef55382d87d17195 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Wed, 6 May 2026 21:56:38 -0500 Subject: [PATCH 17/29] test: configChangePlausibilityTest; revert version dep --- src/ProtocolV4TestBase.sol | 39 +++++++++++- src/dependencies/v4/SnapshotV4.sol | 2 +- tests/ProtocolV4TestBase.t.sol | 95 ++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 4 deletions(-) diff --git a/src/ProtocolV4TestBase.sol b/src/ProtocolV4TestBase.sol index c1eae45b..fed882dc 100644 --- a/src/ProtocolV4TestBase.sol +++ b/src/ProtocolV4TestBase.sol @@ -53,7 +53,8 @@ contract ProtocolV4TestBase is SnapshotV4, Scenarios, TokenizationScenarios, Gat bool testPositionManagers ) public { if (payload != address(0)) { - _snapshotDiffAndExecute(reportName, spokes, payload); + Types.V4Snapshot memory snapshotAfter = _snapshotDiffAndExecute(reportName, spokes, payload); + configChangePlausibilityTest(snapshotAfter); } if (runE2E) { @@ -64,11 +65,43 @@ contract ProtocolV4TestBase is SnapshotV4, Scenarios, TokenizationScenarios, Gat } } + /// @notice Sanity-check post-payload spoke caps for known invariants. + /// @dev Liquidity is pooled at the hub, so invariant is per-asset aggregate across all spokes + function configChangePlausibilityTest(Types.V4Snapshot memory snapshotAfter) public pure { + Types.SpokeCapSnapshot[] memory caps = snapshotAfter.spokeCaps; + for (uint256 i; i < caps.length; i++) { + // Skip (hub, assetId) groups already aggregated in a prior iteration. + bool alreadyAggregated = false; + for (uint256 j; j < i; j++) { + if (caps[j].hubAddress == caps[i].hubAddress && caps[j].assetId == caps[i].assetId) { + alreadyAggregated = true; + break; + } + } + if (alreadyAggregated) { + continue; + } + + uint256 sumAdd; + uint256 sumDraw; + for (uint256 k; k < caps.length; k++) { + if (caps[k].hubAddress == caps[i].hubAddress && caps[k].assetId == caps[i].assetId) { + sumAdd += uint256(caps[k].addCap); + sumDraw += uint256(caps[k].drawCap); + } + } + if (sumDraw == 0) { + continue; + } + require(sumDraw <= sumAdd, 'PL_ADD_LT_DRAW'); + } + } + function _snapshotDiffAndExecute( string memory reportName, ISpoke[] memory spokes, address payload - ) internal virtual { + ) internal virtual returns (Types.V4Snapshot memory snapshotAfter) { IHub[] memory hubs = AaveV4EthereumHubHelpers.getHubs(); string memory beforeName = string.concat(reportName, '_before'); string memory afterName = string.concat(reportName, '_after'); @@ -89,7 +122,7 @@ contract ProtocolV4TestBase is SnapshotV4, Scenarios, TokenizationScenarios, Gat ); } - Types.V4Snapshot memory snapshotAfter = createV4Snapshot(spokes, hubs); + snapshotAfter = createV4Snapshot(spokes, hubs); writeV4SnapshotJson(afterName, snapshotAfter); string memory afterPath = string.concat('./reports/', afterName, '.json'); diff --git a/src/dependencies/v4/SnapshotV4.sol b/src/dependencies/v4/SnapshotV4.sol index c0bef2aa..721ee13a 100644 --- a/src/dependencies/v4/SnapshotV4.sol +++ b/src/dependencies/v4/SnapshotV4.sol @@ -41,7 +41,7 @@ abstract contract SnapshotV4 is Helpers { string[] memory inputs = new string[](7); inputs[0] = 'npx'; - inputs[1] = '@aave-dao/aave-helpers-js@^1.1.0'; + inputs[1] = '@aave-dao/aave-helpers-js@^1.0.1'; inputs[2] = 'diff-v4-snapshots'; inputs[3] = beforePath; inputs[4] = afterPath; diff --git a/tests/ProtocolV4TestBase.t.sol b/tests/ProtocolV4TestBase.t.sol index 6ef09d08..fb6ead1b 100644 --- a/tests/ProtocolV4TestBase.t.sol +++ b/tests/ProtocolV4TestBase.t.sol @@ -238,3 +238,98 @@ contract ProtocolV4TestStorageValidation is ProtocolV4TestBaseTest { _cleanupArtifacts(name); } } + +contract ProtocolV4TestPlausibility is Test, ProtocolV4TestBase { + address internal HUB_A = makeAddr('HUB_A'); + address internal HUB_B = makeAddr('HUB_B'); + + + + function test_emptyCapsPasses() public view { + Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](0); + configChangePlausibilityTest(_snapshot(caps)); + } + + function test_zeroDrawCapSkipped() public view { + Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](1); + caps[0] = _cap(HUB_A, 0, 0, 0); + configChangePlausibilityTest(_snapshot(caps)); + } + + function test_singleSpokeDrawLeAddPasses() public view { + Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](1); + caps[0] = _cap(HUB_A, 0, 1_000_000, 500_000); + configChangePlausibilityTest(_snapshot(caps)); + } + + function test_singleSpokeDrawGtAddReverts() public { + Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](1); + caps[0] = _cap(HUB_A, 0, 500_000, 1_000_000); + vm.expectRevert(bytes('PL_ADD_LT_DRAW')); + this.configChangePlausibilityTest(_snapshot(caps)); + } + + function test_borrowOnlySpokePassesWhenAggregateHolds() public view { + // Spoke A: borrow-only (addCap=0, drawCap=1M) — invalid per-spoke but valid in V4. + // Spoke B: supply-only (addCap=5M, drawCap=0). Aggregate: 1M draw <= 5M add. + Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](2); + caps[0] = _cap(HUB_A, 0, 0, 1_000_000); + caps[1] = _cap(HUB_A, 0, 5_000_000, 0); + configChangePlausibilityTest(_snapshot(caps)); + } + + function test_aggregateDrawGtAggregateAddReverts() public { + // Same (hub, asset), two spokes: 2M+1M=3M add, 2M+2M=4M draw -> 4M > 3M reverts. + Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](2); + caps[0] = _cap(HUB_A, 0, 2_000_000, 2_000_000); + caps[1] = _cap(HUB_A, 0, 1_000_000, 2_000_000); + vm.expectRevert(bytes('PL_ADD_LT_DRAW')); + this.configChangePlausibilityTest(_snapshot(caps)); + } + + function test_distinctAssetGroupsAreIndependent() public { + // (HUB_A, 0): 1M add, 0 draw -> skipped. (HUB_A, 1): 1M add, 2M draw -> reverts. + Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](2); + caps[0] = _cap(HUB_A, 0, 1_000_000, 0); + caps[1] = _cap(HUB_A, 1, 1_000_000, 2_000_000); + vm.expectRevert(bytes('PL_ADD_LT_DRAW')); + this.configChangePlausibilityTest(_snapshot(caps)); + } + + function test_distinctHubsAreIndependent() public { + // Same assetId on two hubs aggregate separately. + // HUB_A asset 0: 1M add, 500k draw -> passes. + // HUB_B asset 0: 500k add, 1M draw -> reverts. + Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](2); + caps[0] = _cap(HUB_A, 0, 1_000_000, 500_000); + caps[1] = _cap(HUB_B, 0, 500_000, 1_000_000); + vm.expectRevert(bytes('PL_ADD_LT_DRAW')); + this.configChangePlausibilityTest(_snapshot(caps)); + } + + function _cap( + address hub, + uint256 assetId, + uint40 addCap, + uint40 drawCap + ) internal pure returns (Types.SpokeCapSnapshot memory) { + return + Types.SpokeCapSnapshot({ + hubAddress: hub, + assetId: assetId, + assetSymbol: 'X', + spokeAddress: address(0), + addCap: addCap, + drawCap: drawCap, + riskPremiumThreshold: 0, + active: true, + halted: false + }); + } + + function _snapshot( + Types.SpokeCapSnapshot[] memory caps + ) internal pure returns (Types.V4Snapshot memory snap) { + snap.spokeCaps = caps; + } +} From bcecc9b8b941ab4c1355bfe5e9ab149e83ddbc4c Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Thu, 7 May 2026 09:07:01 -0500 Subject: [PATCH 18/29] chore: lint --- tests/ProtocolV4TestBase.t.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/ProtocolV4TestBase.t.sol b/tests/ProtocolV4TestBase.t.sol index fb6ead1b..dfb44051 100644 --- a/tests/ProtocolV4TestBase.t.sol +++ b/tests/ProtocolV4TestBase.t.sol @@ -243,8 +243,6 @@ contract ProtocolV4TestPlausibility is Test, ProtocolV4TestBase { address internal HUB_A = makeAddr('HUB_A'); address internal HUB_B = makeAddr('HUB_B'); - - function test_emptyCapsPasses() public view { Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](0); configChangePlausibilityTest(_snapshot(caps)); From 18185fdc413a85e3a921d0cc5c978151edb7681a Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Thu, 7 May 2026 11:43:32 -0500 Subject: [PATCH 19/29] fix: more formatting on numbers, exp --- .../__tests__/protocol-diff-v4.spec.ts | 32 ++++++++++++++++--- packages/aave-helpers-js/formatters-v4.ts | 20 +++++++++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts index e2f76891..c2ce71cd 100644 --- a/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts +++ b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts @@ -186,7 +186,7 @@ describe('BPS formatting via formatV4Value', () => { expect(formatV4Value('spokeLiq', 'maxUserReservesLimit', 128, ctx)).toBe('128'); }); - it('formats spoke cap uint40 fields with thousands separators and asset symbol', () => { + it('formats spoke cap uint40 fields with separators, asset symbol, and exponential', () => { const capCtx = { ...ctx, spokeCap: { @@ -198,10 +198,34 @@ describe('BPS formatting via formatV4Value', () => { halted: false, }, }; - expect(formatV4Value('spokeCap', 'addCap', 1000000, capCtx)).toBe('1,000,000 USDT'); - expect(formatV4Value('spokeCap', 'drawCap', 1880000, capCtx)).toBe('1,880,000 USDT'); + expect(formatV4Value('spokeCap', 'addCap', 1000000, capCtx)).toBe('1,000,000 (1e6) USDT'); + expect(formatV4Value('spokeCap', 'drawCap', 1880000, capCtx)).toBe('1,880,000 (1.88e6) USDT'); // Falls back gracefully when symbol unavailable - expect(formatV4Value('spokeCap', 'addCap', 1000000, ctx)).toBe('1,000,000'); + expect(formatV4Value('spokeCap', 'addCap', 1000000, ctx)).toBe('1,000,000 (1e6)'); + // Small caps (< 1000) skip the exponential + expect(formatV4Value('spokeCap', 'addCap', 500, capCtx)).toBe('500 USDT'); + }); + + it('formats numeric-string fields (oraclePrice, swept, premiumShares) with separators + exp', () => { + // Price feed: uint256 oracle price serialized as string — full precision in exp + expect(formatV4Value('spokeReserve', 'oraclePrice', '99999850', ctx)).toBe( + '99,999,850 (9.999985e7)' + ); + expect(formatV4Value('spokeReserve', 'oraclePrice', '150000000', ctx)).toBe( + '150,000,000 (1.5e8)' + ); + // Sub-1000 trailing digits stay (1234 -> 1.234e3, not 1.23e3) + expect(formatV4Value('spokeReserve', 'oraclePrice', '1234', ctx)).toBe('1,234 (1.234e3)'); + // uint120 hub-asset state fields, including values that exceed JS safe-int range. + // Comma-separated form preserves exact value; exponent's mantissa is rounded by Number. + expect(formatV4Value('hubAsset', 'swept', '12345678901234567890', ctx)).toBe( + '12,345,678,901,234,567,890 (1.2345678901234567e19)' + ); + expect(formatV4Value('hubAsset', 'premiumShares', '1000000', ctx)).toBe('1,000,000 (1e6)'); + // Small numbers (< 1000) — no separators, no exponential + expect(formatV4Value('spokeReserve', 'oraclePrice', '999', ctx)).toBe('999'); + // Zero + expect(formatV4Value('hubAsset', 'swept', '0', ctx)).toBe('0'); }); }); diff --git a/packages/aave-helpers-js/formatters-v4.ts b/packages/aave-helpers-js/formatters-v4.ts index 3080804b..4afabf93 100644 --- a/packages/aave-helpers-js/formatters-v4.ts +++ b/packages/aave-helpers-js/formatters-v4.ts @@ -39,6 +39,18 @@ function formatBps(bps: number): string { return `${w}.${fs} % [${bps}]`; } +/** Render a uint as "1,500,000 (1.5e6) USDT"; suffix (e.g. asset symbol) goes + * at the end after the exponential. Values < 1000 skip the exponential. + * Exponential uses `Number.toExponential()`, so bigints exceeding + * Number.MAX_SAFE_INTEGER (~9e15) the exponent is rounded */ +export function formatBigIntWithExp(value: bigint, suffix?: string): string { + const commas = value.toLocaleString('en-US'); + const useExp = value >= 1000n || value <= -1000n; + const expPart = useExp ? ` (${Number(value).toExponential().replace('e+', 'e')})` : ''; + const suf = suffix ? ` ${suffix}` : ''; + return `${commas}${expPart}${suf}`; +} + // --- Spoke Reserve formatters --- type SpokeReserveKey = keyof V4SpokeReserve; @@ -144,7 +156,7 @@ for (const field of SPOKE_CAP_BOOL_FIELDS) { for (const field of SPOKE_CAP_TOKEN_AMOUNT_FIELDS) { (spokeCapFormatters[field] as FieldFormatter) = (value, ctx) => - `${value.toLocaleString('en-US')} ${ctx.spokeCap?.assetSymbol ?? ''}`.trim(); + formatBigIntWithExp(BigInt(value), ctx.spokeCap?.assetSymbol); } spokeCapFormatters['riskPremiumThreshold'] = (value) => formatBps(value); @@ -195,5 +207,11 @@ export function formatV4Value( if (typeof value === 'boolean') return boolToMarkdown(value); if (typeof value === 'number') return value.toLocaleString('en-US'); if (isAddress(value)) return addressLink(value as string, ctx.chainId); + // Pure numeric strings (uint serialized as string) — render with thousand separators + // and a parenthetical exponential ("1,500,000 (1.5e6)") so price feeds and large uints + // are easy to scan at both scales. + if (typeof value === 'string' && /^-?\d+$/.test(value)) { + return formatBigIntWithExp(BigInt(value)); + } return String(value); } From 5694666f0d2bb3da955c697893b8d16f513aa142 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Mon, 11 May 2026 08:51:41 -0500 Subject: [PATCH 20/29] fix: nit space --- src/dependencies/v4/SnapshotV4.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dependencies/v4/SnapshotV4.sol b/src/dependencies/v4/SnapshotV4.sol index 721ee13a..6c2ff547 100644 --- a/src/dependencies/v4/SnapshotV4.sol +++ b/src/dependencies/v4/SnapshotV4.sol @@ -51,7 +51,6 @@ abstract contract SnapshotV4 is Helpers { } // Spoke reserves - function _snapshotSpokeReserves( ISpoke[] memory spokes ) private view returns (Types.SpokeReserveSnapshot[] memory) { From cb62cc2d94e984328b31fffda14baf5327d338d0 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Mon, 11 May 2026 14:58:13 -0500 Subject: [PATCH 21/29] feat: tests for v4 snapshot; mocks --- tests/dependencies/v4/SnapshotV4.t.sol | 32 + tests/dependencies/v4/SnapshotV4Base.t.sol | 681 +++++++++++++++++++++ tests/mocks/v4/V4Mocks.sol | 170 +++++ 3 files changed, 883 insertions(+) create mode 100644 tests/dependencies/v4/SnapshotV4.t.sol create mode 100644 tests/dependencies/v4/SnapshotV4Base.t.sol create mode 100644 tests/mocks/v4/V4Mocks.sol diff --git a/tests/dependencies/v4/SnapshotV4.t.sol b/tests/dependencies/v4/SnapshotV4.t.sol new file mode 100644 index 00000000..da430b5d --- /dev/null +++ b/tests/dependencies/v4/SnapshotV4.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'tests/dependencies/v4/SnapshotV4Base.t.sol'; + +contract SnapshotV4Test is SnapshotV4BaseTest { + function test_createV4Snapshot_spokeReserves() public view { + assertEq(_createV4Snapshot().spokeReserves, _reserveFixtures); + } + + function test_createV4Snapshot_liquidationConfigs() public view { + assertEq(_createV4Snapshot().spokeLiquidationConfigs, _liqConfigFixtures); + } + + function test_createV4Snapshot_hubAssets() public view { + assertEq(_createV4Snapshot().hubAssets, _hubAssetFixtures); + } + + function test_createV4Snapshot_spokeCaps() public view { + assertEq(_createV4Snapshot().spokeCaps, _spokeCapFixtures); + } + + function test_createV4Snapshot_emptyInputs() public view { + ISpoke[] memory spokes = new ISpoke[](0); + IHub[] memory hubs = new IHub[](0); + Types.V4Snapshot memory snap = createV4Snapshot(spokes, hubs); + assertEq(snap.spokeReserves.length, 0, 'empty reserves'); + assertEq(snap.spokeLiquidationConfigs.length, 0, 'empty liq'); + assertEq(snap.hubAssets.length, 0, 'empty hubAssets'); + assertEq(snap.spokeCaps.length, 0, 'empty caps'); + } +} diff --git a/tests/dependencies/v4/SnapshotV4Base.t.sol b/tests/dependencies/v4/SnapshotV4Base.t.sol new file mode 100644 index 00000000..b315d2aa --- /dev/null +++ b/tests/dependencies/v4/SnapshotV4Base.t.sol @@ -0,0 +1,681 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {ISpoke, IHub} from 'aave-address-book/AaveV4.sol'; +import {IHubBase} from 'aave-v4/hub/interfaces/IHubBase.sol'; +import {IAssetInterestRateStrategy} from 'aave-v4/hub/interfaces/IAssetInterestRateStrategy.sol'; +import {SnapshotV4} from 'src/dependencies/v4/SnapshotV4.sol'; +import {Types} from 'src/dependencies/v4/Types.sol'; +import {MockSpoke, MockHub, MockOracle, MockIR, MockERC20Symbol} from 'tests/mocks/v4/V4Mocks.sol'; + +abstract contract SnapshotV4BaseTest is Test, SnapshotV4 { + struct ReserveFixture { + MockSpoke spoke; + MockOracle oracle; + address underlying; + address hubAddr; + uint16 assetId; + uint8 decimals; + bool paused; + bool frozen; + bool borrowable; + bool receiveSharesEnabled; + uint24 collateralRisk; + uint32 dynamicConfigKey; + uint16 collateralFactor; + uint32 maxLiquidationBonus; + uint16 liquidationFee; + address priceSource; + uint256 oraclePrice; + } + + struct CachedReserveFixture { + ReserveFixture input; + uint16 reserveId; + } + + /// @notice Fixtures captured by `_addReserve` + CachedReserveFixture[] internal _reserveFixtures; + + struct HubAssetFixture { + address underlying; + uint8 decimals; + uint16 liquidityFee; + address irStrategy; + address feeReceiver; + address reinvController; + uint200 deficitRay; + uint120 swept; + uint120 premiumShares; + int200 premiumOffsetRay; + } + + /// @dev Stored alongside the input fixture so tests can match snapshots against + /// the `assetId` returned by `MockHub.addAsset`. + struct CachedHubAssetFixture { + HubAssetFixture input; + uint256 assetId; + } + + /// @notice Fixtures captured by `_addHubAsset` + CachedHubAssetFixture[] internal _hubAssetFixtures; + + struct LiqConfigFixture { + MockSpoke spoke; + uint128 targetHealthFactor; + uint64 healthFactorForMaxBonus; + uint16 liquidationBonusFactor; + uint16 maxUserReservesLimit; + } + + /// @notice Fixtures captured by `_addLiqConfig`. The spoke's `setLiquidationConfig` + /// and `setMaxUserReservesLimit` calls produce no return value, so no wrapper struct. + LiqConfigFixture[] internal _liqConfigFixtures; + + struct SpokeCapFixture { + uint256 assetId; + MockSpoke spoke; + uint40 addCap; + uint40 drawCap; + uint24 riskPremiumThreshold; + bool active; + bool halted; + } + + /// @notice Fixtures captured by `_addSpokeCap`. `MockHub.addSpokeConfig` returns nothing + /// and the snapshot key (hub, assetId, spoke) is already in the input, so no wrapper struct. + SpokeCapFixture[] internal _spokeCapFixtures; + + MockSpoke internal spokeA; + MockSpoke internal spokeB; + MockHub internal hub; + MockOracle internal oracleA; + MockOracle internal oracleB; + MockIR internal ir0; + MockIR internal ir1; + + MockERC20Symbol internal usdc; + MockERC20Symbol internal weth; + MockERC20Symbol internal wbtc; + + address internal priceSource0 = makeAddr('CHAINLINK_USDC'); + address internal priceSource1 = makeAddr('CHAINLINK_WETH'); + address internal priceSource2 = makeAddr('CHAINLINK_WBTC'); + + address internal feeReceiverA = makeAddr('FEE_RECEIVER_A'); + address internal feeReceiverB = makeAddr('FEE_RECEIVER_B'); + address internal reinvA = makeAddr('REINVEST_A'); + address internal reinvB = makeAddr('REINVEST_B'); + + function setUp() public { + _deployMocks(); + _setSpokeOracles(); + _addLiqConfigFixtures(); + _addReserveFixtures(); + _addHubAssetFixtures(); + _configureIRStrategies(); + _addSpokeCapFixtures(); + } + + function _setSpokeOracles() internal { + spokeA.setOracle(address(oracleA)); + spokeB.setOracle(address(oracleB)); + } + + function _addLiqConfigFixtures() internal { + _addLiqConfig( + LiqConfigFixture({ + spoke: spokeA, + targetHealthFactor: 1.05e18, + healthFactorForMaxBonus: 0.95e18, + liquidationBonusFactor: 100, + maxUserReservesLimit: 8 + }) + ); + _addLiqConfig( + LiqConfigFixture({ + spoke: spokeB, + targetHealthFactor: 1.10e18, + healthFactorForMaxBonus: 0.97e18, + liquidationBonusFactor: 200, + maxUserReservesLimit: 12 + }) + ); + } + + function _addLiqConfig(LiqConfigFixture memory f) internal { + f.spoke.setMaxUserReservesLimit(f.maxUserReservesLimit); + f.spoke.setLiquidationConfig( + ISpoke.LiquidationConfig({ + targetHealthFactor: f.targetHealthFactor, + healthFactorForMaxBonus: f.healthFactorForMaxBonus, + liquidationBonusFactor: f.liquidationBonusFactor + }) + ); + _liqConfigFixtures.push(f); + } + + /// @notice Assert a `SpokeLiquidationSnapshot` matches the stored fixture inputs. + function assertEq( + Types.SpokeLiquidationSnapshot memory snap, + LiqConfigFixture memory expected, + uint256 idx + ) internal pure { + string memory pfx = string.concat('liqConfig[', vm.toString(idx), '] '); + assertEq(snap.spokeAddress, address(expected.spoke), string.concat(pfx, 'spoke')); + assertEq( + snap.targetHealthFactor, + uint256(expected.targetHealthFactor), + string.concat(pfx, 'targetHealthFactor') + ); + assertEq( + snap.healthFactorForMaxBonus, + uint256(expected.healthFactorForMaxBonus), + string.concat(pfx, 'healthFactorForMaxBonus') + ); + assertEq( + uint256(snap.liquidationBonusFactor), + uint256(expected.liquidationBonusFactor), + string.concat(pfx, 'liquidationBonusFactor') + ); + assertEq( + uint256(snap.maxUserReservesLimit), + uint256(expected.maxUserReservesLimit), + string.concat(pfx, 'maxUserReservesLimit') + ); + } + + function _deployMocks() internal { + usdc = new MockERC20Symbol('USDC'); + weth = new MockERC20Symbol('WETH'); + wbtc = new MockERC20Symbol('WBTC'); + + oracleA = new MockOracle(); + oracleB = new MockOracle(); + + ir0 = new MockIR(); + ir1 = new MockIR(); + + hub = new MockHub(); + spokeA = new MockSpoke(); + spokeB = new MockSpoke(); + } + + function _addHubAssetFixtures() internal { + _addHubAsset( + HubAssetFixture({ + underlying: address(usdc), + decimals: 6, + liquidityFee: 10, + irStrategy: address(ir0), + feeReceiver: feeReceiverA, + reinvController: reinvA, + deficitRay: 11, + swept: 22, + premiumShares: 33, + premiumOffsetRay: int200(44) + }) + ); + _addHubAsset( + HubAssetFixture({ + underlying: address(weth), + decimals: 18, + liquidityFee: 20, + irStrategy: address(ir1), + feeReceiver: feeReceiverB, + reinvController: reinvB, + deficitRay: 55, + swept: 66, + premiumShares: 77, + premiumOffsetRay: int200(-88) + }) + ); + // No IR strategy on this one — exercises the `irStrategy == address(0)` branch. + _addHubAsset( + HubAssetFixture({ + underlying: address(wbtc), + decimals: 8, + liquidityFee: 30, + irStrategy: address(0), + feeReceiver: feeReceiverA, + reinvController: address(0), + deficitRay: 99, + swept: 100, + premiumShares: 101, + premiumOffsetRay: int200(102) + }) + ); + } + + function _configureIRStrategies() internal { + ir0.setData( + 0, + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: 8000, + baseDrawnRate: 100, + rateGrowthBeforeOptimal: 400, + rateGrowthAfterOptimal: 6000 + }), + 30_000 + ); + ir1.setData( + 1, + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: 7000, + baseDrawnRate: 200, + rateGrowthBeforeOptimal: 500, + rateGrowthAfterOptimal: 7000 + }), + 50_000 + ); + } + + function _addSpokeCapFixtures() internal { + _addSpokeCap( + SpokeCapFixture({ + assetId: 0, + spoke: spokeA, + addCap: 1_000_000, + drawCap: 500_000, + riskPremiumThreshold: 100, + active: true, + halted: false + }) + ); + _addSpokeCap( + SpokeCapFixture({ + assetId: 0, + spoke: spokeB, + addCap: 2_000_000, + drawCap: 1_500_000, + riskPremiumThreshold: 200, + active: true, + halted: true + }) + ); + _addSpokeCap( + SpokeCapFixture({ + assetId: 1, + spoke: spokeA, + addCap: 3_000_000, + drawCap: 2_500_000, + riskPremiumThreshold: 300, + active: false, + halted: false + }) + ); + _addSpokeCap( + SpokeCapFixture({ + assetId: 2, + spoke: spokeB, + addCap: 4_000_000, + drawCap: 3_500_000, + riskPremiumThreshold: 400, + active: true, + halted: false + }) + ); + } + + function _addSpokeCap(SpokeCapFixture memory f) internal { + hub.addSpokeConfig( + f.assetId, + address(f.spoke), + IHub.SpokeConfig({ + addCap: f.addCap, + drawCap: f.drawCap, + riskPremiumThreshold: f.riskPremiumThreshold, + active: f.active, + halted: f.halted + }) + ); + _spokeCapFixtures.push(f); + } + + /// @notice Assert a `SpokeCapSnapshot` matches the stored fixture inputs. + /// `assetSymbol` is resolved from the asset's underlying token, mirroring + /// `SnapshotV4._snapshotCapsForHub`. + function assertEq( + Types.SpokeCapSnapshot memory snap, + SpokeCapFixture memory expected, + uint256 idx + ) internal view { + string memory pfx = string.concat('spokeCap[', vm.toString(idx), '] '); + assertEq(snap.hubAddress, address(hub), string.concat(pfx, 'hub')); + assertEq(snap.assetId, expected.assetId, string.concat(pfx, 'assetId')); + assertEq(snap.spokeAddress, address(expected.spoke), string.concat(pfx, 'spoke')); + (address underlying, ) = hub.getAssetUnderlyingAndDecimals(expected.assetId); + assertEq( + snap.assetSymbol, + MockERC20Symbol(underlying).symbol(), + string.concat(pfx, 'assetSymbol') + ); + assertEq(uint256(snap.addCap), uint256(expected.addCap), string.concat(pfx, 'addCap')); + assertEq(uint256(snap.drawCap), uint256(expected.drawCap), string.concat(pfx, 'drawCap')); + assertEq( + uint256(snap.riskPremiumThreshold), + uint256(expected.riskPremiumThreshold), + string.concat(pfx, 'riskPremiumThreshold') + ); + assertEq(snap.active, expected.active, string.concat(pfx, 'active')); + assertEq(snap.halted, expected.halted, string.concat(pfx, 'halted')); + } + + function _createV4Snapshot() internal view returns (Types.V4Snapshot memory) { + ISpoke[] memory spokes = new ISpoke[](2); + spokes[0] = ISpoke(address(spokeA)); + spokes[1] = ISpoke(address(spokeB)); + IHub[] memory hubs = new IHub[](1); + hubs[0] = IHub(address(hub)); + return createV4Snapshot(spokes, hubs); + } + + function _addReserveFixtures() internal { + _addReserve( + ReserveFixture({ + spoke: spokeA, + oracle: oracleA, + underlying: address(usdc), + hubAddr: address(hub), + assetId: 0, + decimals: 6, + paused: false, + frozen: false, + borrowable: true, + receiveSharesEnabled: true, + collateralRisk: 1000, + dynamicConfigKey: 1, + collateralFactor: 7500, + maxLiquidationBonus: 10500, + liquidationFee: 100, + priceSource: priceSource0, + oraclePrice: 1e8 + }) + ); + _addReserve( + ReserveFixture({ + spoke: spokeA, + oracle: oracleA, + underlying: address(weth), + hubAddr: address(hub), + assetId: 1, + decimals: 18, + paused: false, + frozen: true, + borrowable: false, + receiveSharesEnabled: false, + collateralRisk: 2500, + dynamicConfigKey: 2, + collateralFactor: 8000, + maxLiquidationBonus: 11000, + liquidationFee: 150, + priceSource: priceSource1, + oraclePrice: 2_000e8 + }) + ); + + _addReserve( + ReserveFixture({ + spoke: spokeB, + oracle: oracleB, + underlying: address(wbtc), + hubAddr: address(hub), + assetId: 2, + decimals: 8, + paused: true, + frozen: false, + borrowable: true, + receiveSharesEnabled: true, + collateralRisk: 3000, + dynamicConfigKey: 3, + collateralFactor: 7000, + maxLiquidationBonus: 11500, + liquidationFee: 200, + priceSource: priceSource2, + oraclePrice: 60_000e8 + }) + ); + } + + function _addReserve(ReserveFixture memory f) internal { + ISpoke.Reserve memory reserve; + reserve.underlying = f.underlying; + reserve.hub = IHubBase(f.hubAddr); + reserve.assetId = f.assetId; + reserve.decimals = f.decimals; + reserve.collateralRisk = f.collateralRisk; + reserve.dynamicConfigKey = f.dynamicConfigKey; + + ISpoke.ReserveConfig memory config = ISpoke.ReserveConfig({ + collateralRisk: f.collateralRisk, + paused: f.paused, + frozen: f.frozen, + borrowable: f.borrowable, + receiveSharesEnabled: f.receiveSharesEnabled + }); + + ISpoke.DynamicReserveConfig memory dyn = ISpoke.DynamicReserveConfig({ + collateralFactor: f.collateralFactor, + maxLiquidationBonus: f.maxLiquidationBonus, + liquidationFee: f.liquidationFee + }); + + uint256 reserveId = f.spoke.addReserve(reserve, config, dyn); + f.oracle.setReserve(reserveId, f.priceSource, f.oraclePrice); + _reserveFixtures.push(CachedReserveFixture({input: f, reserveId: uint16(reserveId)})); + } + + /// @notice Assert a `SpokeReserveSnapshot` matches the stored fixture inputs. + function assertEq( + Types.SpokeReserveSnapshot memory snap, + CachedReserveFixture memory expected, + uint256 idx + ) internal view { + string memory pfx = string.concat('reserve[', vm.toString(idx), '] '); + assertEq(snap.spokeAddress, address(expected.input.spoke), string.concat(pfx, 'spoke')); + assertEq(snap.reserveId, expected.reserveId, string.concat(pfx, 'reserveId')); + assertEq(snap.underlying, expected.input.underlying, string.concat(pfx, 'underlying')); + assertEq( + snap.symbol, + MockERC20Symbol(expected.input.underlying).symbol(), + string.concat(pfx, 'symbol') + ); + assertEq(snap.hub, expected.input.hubAddr, string.concat(pfx, 'hub')); + assertEq(uint256(snap.assetId), uint256(expected.input.assetId), string.concat(pfx, 'assetId')); + assertEq( + uint256(snap.decimals), + uint256(expected.input.decimals), + string.concat(pfx, 'decimals') + ); + assertEq( + uint256(snap.collateralRisk), + uint256(expected.input.collateralRisk), + string.concat(pfx, 'collateralRisk') + ); + assertEq(snap.paused, expected.input.paused, string.concat(pfx, 'paused')); + assertEq(snap.frozen, expected.input.frozen, string.concat(pfx, 'frozen')); + assertEq(snap.borrowable, expected.input.borrowable, string.concat(pfx, 'borrowable')); + assertEq( + snap.receiveSharesEnabled, + expected.input.receiveSharesEnabled, + string.concat(pfx, 'receiveSharesEnabled') + ); + assertEq( + uint256(snap.dynamicConfigKey), + uint256(expected.input.dynamicConfigKey), + string.concat(pfx, 'dynamicConfigKey') + ); + assertEq( + uint256(snap.collateralFactor), + uint256(expected.input.collateralFactor), + string.concat(pfx, 'collateralFactor') + ); + assertEq( + uint256(snap.maxLiquidationBonus), + uint256(expected.input.maxLiquidationBonus), + string.concat(pfx, 'maxLiquidationBonus') + ); + assertEq( + uint256(snap.liquidationFee), + uint256(expected.input.liquidationFee), + string.concat(pfx, 'liquidationFee') + ); + assertEq( + snap.oracleAddress, + address(expected.input.oracle), + string.concat(pfx, 'oracleAddress') + ); + assertEq(snap.priceSource, expected.input.priceSource, string.concat(pfx, 'priceSource')); + assertEq(snap.oraclePrice, expected.input.oraclePrice, string.concat(pfx, 'oraclePrice')); + } + + /// @notice Assert a `HubAssetSnapshot` matches the stored fixture inputs. + /// IR-strategy fields are zero when `irStrategy == address(0)`; otherwise queried + /// from the IR mock at assert time, mirroring how `SnapshotV4` resolves them. + function assertEq( + Types.HubAssetSnapshot memory snap, + CachedHubAssetFixture memory expected, + uint256 idx + ) internal view { + string memory pfx = string.concat('hubAsset[', vm.toString(idx), '] '); + assertEq(snap.hubAddress, address(hub), string.concat(pfx, 'hub')); + assertEq(snap.assetId, expected.assetId, string.concat(pfx, 'assetId')); + assertEq(snap.underlying, expected.input.underlying, string.concat(pfx, 'underlying')); + assertEq( + snap.symbol, + MockERC20Symbol(expected.input.underlying).symbol(), + string.concat(pfx, 'symbol') + ); + assertEq( + uint256(snap.decimals), + uint256(expected.input.decimals), + string.concat(pfx, 'decimals') + ); + assertEq( + uint256(snap.liquidityFee), + uint256(expected.input.liquidityFee), + string.concat(pfx, 'liquidityFee') + ); + assertEq(snap.irStrategy, expected.input.irStrategy, string.concat(pfx, 'irStrategy')); + assertEq(snap.feeReceiver, expected.input.feeReceiver, string.concat(pfx, 'feeReceiver')); + assertEq( + snap.reinvestmentController, + expected.input.reinvController, + string.concat(pfx, 'reinvController') + ); + + // IR strategy fields — zero when address(0), otherwise queried from the mock. + uint256 expOptimalUR; + uint256 expBaseRate; + uint256 expGrowthBefore; + uint256 expGrowthAfter; + uint256 expMaxDrawnRate; + if (expected.input.irStrategy != address(0)) { + IAssetInterestRateStrategy ir = IAssetInterestRateStrategy(expected.input.irStrategy); + IAssetInterestRateStrategy.InterestRateData memory data = ir.getInterestRateData( + expected.assetId + ); + expOptimalUR = data.optimalUsageRatio; + expBaseRate = data.baseDrawnRate; + expGrowthBefore = data.rateGrowthBeforeOptimal; + expGrowthAfter = data.rateGrowthAfterOptimal; + expMaxDrawnRate = ir.getMaxDrawnRate(expected.assetId); + } + assertEq(snap.optimalUsageRatio, expOptimalUR, string.concat(pfx, 'optimalUsageRatio')); + assertEq(snap.baseDrawnRate, expBaseRate, string.concat(pfx, 'baseDrawnRate')); + assertEq( + snap.rateGrowthBeforeOptimal, + expGrowthBefore, + string.concat(pfx, 'rateGrowthBeforeOptimal') + ); + assertEq( + snap.rateGrowthAfterOptimal, + expGrowthAfter, + string.concat(pfx, 'rateGrowthAfterOptimal') + ); + assertEq(snap.maxDrawnRate, expMaxDrawnRate, string.concat(pfx, 'maxDrawnRate')); + + // Asset state — written directly from the fixture into the mock, so equality is exact. + assertEq( + uint256(snap.deficitRay), + uint256(expected.input.deficitRay), + string.concat(pfx, 'deficitRay') + ); + assertEq(uint256(snap.swept), uint256(expected.input.swept), string.concat(pfx, 'swept')); + assertEq( + uint256(snap.premiumShares), + uint256(expected.input.premiumShares), + string.concat(pfx, 'premiumShares') + ); + assertEq( + snap.premiumOffsetRay, + expected.input.premiumOffsetRay, + string.concat(pfx, 'premiumOffsetRay') + ); + } + + function _addHubAsset(HubAssetFixture memory f) internal { + IHub.Asset memory asset; + asset.underlying = f.underlying; + asset.decimals = f.decimals; + asset.liquidityFee = f.liquidityFee; + asset.irStrategy = f.irStrategy; + asset.reinvestmentController = f.reinvController; + asset.feeReceiver = f.feeReceiver; + asset.deficitRay = f.deficitRay; + asset.swept = f.swept; + asset.premiumShares = f.premiumShares; + asset.premiumOffsetRay = f.premiumOffsetRay; + + IHub.AssetConfig memory config = IHub.AssetConfig({ + feeReceiver: f.feeReceiver, + liquidityFee: f.liquidityFee, + irStrategy: f.irStrategy, + reinvestmentController: f.reinvController + }); + + uint256 assetId = hub.addAsset(asset, config); + _hubAssetFixtures.push(CachedHubAssetFixture({input: f, assetId: assetId})); + } + + function assertEq( + Types.SpokeReserveSnapshot[] memory snaps, + CachedReserveFixture[] memory expected + ) internal view { + assertEq(snaps.length, expected.length, 'spokeReserves length'); + for (uint256 i; i < expected.length; i++) { + assertEq(snaps[i], expected[i], i); + } + } + + function assertEq( + Types.HubAssetSnapshot[] memory snaps, + CachedHubAssetFixture[] memory expected + ) internal view { + assertEq(snaps.length, expected.length, 'hubAssets length'); + for (uint256 i; i < expected.length; i++) { + assertEq(snaps[i], expected[i], i); + } + } + + function assertEq( + Types.SpokeLiquidationSnapshot[] memory snaps, + LiqConfigFixture[] memory expected + ) internal pure { + assertEq(snaps.length, expected.length, 'liqConfigs length'); + for (uint256 i; i < expected.length; i++) { + assertEq(snaps[i], expected[i], i); + } + } + + function assertEq( + Types.SpokeCapSnapshot[] memory snaps, + SpokeCapFixture[] memory expected + ) internal view { + assertEq(snaps.length, expected.length, 'spokeCaps length'); + for (uint256 i; i < expected.length; i++) { + assertEq(snaps[i], expected[i], i); + } + } +} diff --git a/tests/mocks/v4/V4Mocks.sol b/tests/mocks/v4/V4Mocks.sol new file mode 100644 index 00000000..e840fa07 --- /dev/null +++ b/tests/mocks/v4/V4Mocks.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ISpoke, IHub} from 'aave-address-book/AaveV4.sol'; +import {IAssetInterestRateStrategy} from 'aave-v4/hub/interfaces/IAssetInterestRateStrategy.sol'; + +/// @notice ERC20 fixture that only exposes `symbol()`, the single call SnapshotV4 +/// makes on the underlying token. +contract MockERC20Symbol { + string public symbol; + + constructor(string memory _symbol) { + symbol = _symbol; + } +} + +/// @notice Minimal IAaveOracle mock. Only the two functions SnapshotV4 reads +/// (`getReserveSource`, `getReservePrice`) are implemented. +contract MockOracle { + mapping(uint256 => address) private _sources; + mapping(uint256 => uint256) private _prices; + + function setReserve(uint256 reserveId, address source, uint256 price) external { + _sources[reserveId] = source; + _prices[reserveId] = price; + } + + function getReserveSource(uint256 reserveId) external view returns (address) { + return _sources[reserveId]; + } + + function getReservePrice(uint256 reserveId) external view returns (uint256) { + return _prices[reserveId]; + } +} + +/// @notice Minimal IAssetInterestRateStrategy mock. +contract MockIR { + mapping(uint256 => IAssetInterestRateStrategy.InterestRateData) private _data; + mapping(uint256 => uint256) private _maxRates; + + function setData( + uint256 assetId, + IAssetInterestRateStrategy.InterestRateData memory data, + uint256 maxDrawnRate + ) external { + _data[assetId] = data; + _maxRates[assetId] = maxDrawnRate; + } + + function getInterestRateData( + uint256 assetId + ) external view returns (IAssetInterestRateStrategy.InterestRateData memory) { + return _data[assetId]; + } + + function getMaxDrawnRate(uint256 assetId) external view returns (uint256) { + return _maxRates[assetId]; + } +} + +/// @notice Minimal ISpoke mock covering the reads SnapshotV4 performs on a spoke. +contract MockSpoke { + address public ORACLE; + uint16 public MAX_USER_RESERVES_LIMIT; + ISpoke.LiquidationConfig private _liqConfig; + + ISpoke.Reserve[] private _reserves; + mapping(uint256 => ISpoke.ReserveConfig) private _reserveConfigs; + mapping(uint256 => mapping(uint32 => ISpoke.DynamicReserveConfig)) private _dynConfigs; + + function setOracle(address oracle) external { + ORACLE = oracle; + } + + function setMaxUserReservesLimit(uint16 limit) external { + MAX_USER_RESERVES_LIMIT = limit; + } + + function setLiquidationConfig(ISpoke.LiquidationConfig memory cfg) external { + _liqConfig = cfg; + } + + function addReserve( + ISpoke.Reserve memory reserve, + ISpoke.ReserveConfig memory config, + ISpoke.DynamicReserveConfig memory dyn + ) external returns (uint256 reserveId) { + reserveId = _reserves.length; + _reserves.push(reserve); + _reserveConfigs[reserveId] = config; + _dynConfigs[reserveId][reserve.dynamicConfigKey] = dyn; + } + + function getReserveCount() external view returns (uint256) { + return _reserves.length; + } + + function getReserve(uint256 reserveId) external view returns (ISpoke.Reserve memory) { + return _reserves[reserveId]; + } + + function getReserveConfig(uint256 reserveId) external view returns (ISpoke.ReserveConfig memory) { + return _reserveConfigs[reserveId]; + } + + function getDynamicReserveConfig( + uint256 reserveId, + uint32 dynamicConfigKey + ) external view returns (ISpoke.DynamicReserveConfig memory) { + return _dynConfigs[reserveId][dynamicConfigKey]; + } + + function getLiquidationConfig() external view returns (ISpoke.LiquidationConfig memory) { + return _liqConfig; + } +} + +/// @notice Minimal IHub mock covering the reads SnapshotV4 performs on a hub. +contract MockHub { + IHub.Asset[] private _assets; + mapping(uint256 => IHub.AssetConfig) private _assetConfigs; + mapping(uint256 => address[]) private _spokesByAsset; + mapping(uint256 => mapping(address => IHub.SpokeConfig)) private _spokeConfigs; + + function addAsset( + IHub.Asset memory asset, + IHub.AssetConfig memory config + ) external returns (uint256 assetId) { + assetId = _assets.length; + _assets.push(asset); + _assetConfigs[assetId] = config; + } + + function addSpokeConfig(uint256 assetId, address spoke, IHub.SpokeConfig memory config) external { + _spokesByAsset[assetId].push(spoke); + _spokeConfigs[assetId][spoke] = config; + } + + function getAssetCount() external view returns (uint256) { + return _assets.length; + } + + function getAsset(uint256 assetId) external view returns (IHub.Asset memory) { + return _assets[assetId]; + } + + function getAssetConfig(uint256 assetId) external view returns (IHub.AssetConfig memory) { + return _assetConfigs[assetId]; + } + + function getAssetUnderlyingAndDecimals(uint256 assetId) external view returns (address, uint8) { + return (_assets[assetId].underlying, _assets[assetId].decimals); + } + + function getSpokeCount(uint256 assetId) external view returns (uint256) { + return _spokesByAsset[assetId].length; + } + + function getSpokeAddress(uint256 assetId, uint256 index) external view returns (address) { + return _spokesByAsset[assetId][index]; + } + + function getSpokeConfig( + uint256 assetId, + address spoke + ) external view returns (IHub.SpokeConfig memory) { + return _spokeConfigs[assetId][spoke]; + } +} From 637047fed1345be31ec8e6357b5722fd4f254b09 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Mon, 11 May 2026 15:36:24 -0500 Subject: [PATCH 22/29] fix: v4 diff writer tests --- foundry.toml | 5 +- src/dependencies/v4/V4DiffWriter.sol | 14 +- tests/dependencies/v4/V4DiffWriter.t.sol | 444 ++++++++++++++++++ tests/dependencies/v4/V4DiffWriterHarness.sol | 43 ++ 4 files changed, 498 insertions(+), 8 deletions(-) create mode 100644 tests/dependencies/v4/V4DiffWriter.t.sol create mode 100644 tests/dependencies/v4/V4DiffWriterHarness.sol diff --git a/foundry.toml b/foundry.toml index 6d3c4ce4..1a2749fa 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,7 +5,10 @@ script = 'scripts' out = 'out' libs = ['lib'] remappings = [] -fs_permissions = [{ access = "read-write", path = "./reports" }] +fs_permissions = [ + { access = "read-write", path = "./reports" }, + { access = "read-write", path = "./diffs" }, +] ffi = true evm_version = 'cancun' decode_external_storage = true diff --git a/src/dependencies/v4/V4DiffWriter.sol b/src/dependencies/v4/V4DiffWriter.sol index 3685c93c..c4d96877 100644 --- a/src/dependencies/v4/V4DiffWriter.sol +++ b/src/dependencies/v4/V4DiffWriter.sol @@ -29,7 +29,7 @@ library V4DiffWriter { function _writeSpokeReserves( string memory path, Types.SpokeReserveSnapshot[] memory reserves - ) private { + ) internal { string memory sectionKey = 'spokeReserves'; string memory content = '{}'; vm.serializeJson(sectionKey, '{}'); @@ -54,7 +54,7 @@ library V4DiffWriter { vm.writeJson(vm.serializeString('root', 'spokeReserves', content), path); } - function _serReserve(Types.SpokeReserveSnapshot memory r) private returns (string memory) { + function _serReserve(Types.SpokeReserveSnapshot memory r) internal returns (string memory) { string memory k = string.concat(vm.toString(r.spokeAddress), '_', vm.toString(r.reserveId)); vm.serializeJson(k, '{}'); vm.serializeString(k, 'symbol', r.symbol); @@ -79,7 +79,7 @@ library V4DiffWriter { function _writeSpokeLiqConfigs( string memory path, Types.SpokeLiquidationSnapshot[] memory configs - ) private { + ) internal { string memory sectionKey = 'spokeLiqConfigs'; string memory content = '{}'; vm.serializeJson(sectionKey, '{}'); @@ -104,13 +104,13 @@ library V4DiffWriter { vm.writeJson(vm.serializeString('root', 'spokeLiquidationConfigs', content), path); } - function _writeHubAssets(string memory path, Types.HubAssetSnapshot[] memory assets) private { + function _writeHubAssets(string memory path, Types.HubAssetSnapshot[] memory assets) internal { string memory sectionKey = 'hubAssets'; string memory content = '{}'; vm.serializeJson(sectionKey, '{}'); for (uint256 i; i < assets.length; i++) { - string memory obj = _serHubAsset(assets[i]); + string memory obj = _serializeHubAsset(assets[i]); string memory hubKey = string.concat('hub_', vm.toString(assets[i].hubAddress)); if (i == 0 || assets[i].hubAddress != assets[i - 1].hubAddress) { @@ -125,7 +125,7 @@ library V4DiffWriter { vm.writeJson(vm.serializeString('root', 'hubAssets', content), path); } - function _serHubAsset(Types.HubAssetSnapshot memory a) private returns (string memory) { + function _serializeHubAsset(Types.HubAssetSnapshot memory a) internal returns (string memory) { string memory k = string.concat(vm.toString(a.hubAddress), '_', vm.toString(a.assetId)); vm.serializeJson(k, '{}'); vm.serializeString(k, 'symbol', a.symbol); @@ -147,7 +147,7 @@ library V4DiffWriter { return vm.serializeString(k, 'premiumOffsetRay', vm.toString(a.premiumOffsetRay)); } - function _writeSpokeCaps(string memory path, Types.SpokeCapSnapshot[] memory caps) private { + function _writeSpokeCaps(string memory path, Types.SpokeCapSnapshot[] memory caps) internal { string memory sectionKey = 'spokeCaps'; string memory content = '{}'; vm.serializeJson(sectionKey, '{}'); diff --git a/tests/dependencies/v4/V4DiffWriter.t.sol b/tests/dependencies/v4/V4DiffWriter.t.sol new file mode 100644 index 00000000..e1d7a500 --- /dev/null +++ b/tests/dependencies/v4/V4DiffWriter.t.sol @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {SnapshotV4} from 'src/dependencies/v4/SnapshotV4.sol'; +import {V4DiffWriter} from 'src/dependencies/v4/V4DiffWriter.sol'; +import {Types} from 'src/dependencies/v4/Types.sol'; +import {V4DiffWriterHarness} from './V4DiffWriterHarness.sol'; + +abstract contract V4DiffWriterTestBase is Test { + address internal spokeA; + address internal spokeB; + address internal hubX; + address internal hubY; + address internal underlyingUsdc; + address internal underlyingWeth; + address internal oracleAddr; + address internal priceSource; + address internal irStrategy; + address internal feeReceiver; + address internal reinvController; + + function _setUpAddresses() internal { + spokeA = makeAddr('spokeA'); + spokeB = makeAddr('spokeB'); + hubX = makeAddr('hubX'); + hubY = makeAddr('hubY'); + underlyingUsdc = makeAddr('underlyingUsdc'); + underlyingWeth = makeAddr('underlyingWeth'); + oracleAddr = makeAddr('oracleAddr'); + priceSource = makeAddr('priceSource'); + irStrategy = makeAddr('irStrategy'); + feeReceiver = makeAddr('feeReceiver'); + reinvController = makeAddr('reinvController'); + } + + function _makeReserve( + address spoke, + uint256 reserveId, + string memory symbol, + uint16 collateralFactor + ) internal view returns (Types.SpokeReserveSnapshot memory) { + return + Types.SpokeReserveSnapshot({ + spokeAddress: spoke, + reserveId: reserveId, + underlying: underlyingUsdc, + symbol: symbol, + hub: hubX, + assetId: uint16(reserveId), + decimals: 6, + collateralRisk: 1000, + paused: false, + frozen: false, + borrowable: true, + receiveSharesEnabled: true, + dynamicConfigKey: 1, + collateralFactor: collateralFactor, + maxLiquidationBonus: 10500, + liquidationFee: 100, + oracleAddress: oracleAddr, + priceSource: priceSource, + oraclePrice: 1e8 + }); + } + + function _makeHubAsset( + address hub, + uint256 assetId, + string memory symbol, + uint16 liquidityFee + ) internal view returns (Types.HubAssetSnapshot memory) { + return + Types.HubAssetSnapshot({ + hubAddress: hub, + assetId: assetId, + underlying: underlyingUsdc, + symbol: symbol, + decimals: 6, + liquidityFee: liquidityFee, + irStrategy: irStrategy, + feeReceiver: feeReceiver, + reinvestmentController: reinvController, + optimalUsageRatio: 8000, + baseDrawnRate: 100, + rateGrowthBeforeOptimal: 400, + rateGrowthAfterOptimal: 6000, + maxDrawnRate: 30_000, + deficitRay: 0, + swept: 0, + premiumShares: 0, + premiumOffsetRay: int200(0) + }); + } +} + +/// Inherits SnapshotV4 so we can execute `writeV4SnapshotJson` and `diffV4Snapshots` +contract V4DiffWriterTest is V4DiffWriterTestBase, SnapshotV4 { + string internal constant REPORT = 'v4diff_writer_test'; + + function setUp() public { + _setUpAddresses(); + vm.chainId(1); + } + + function test_writeSnapshotJson_persistsAllSections() public { + Types.V4Snapshot memory snap = _baseSnapshot(); + V4DiffWriter.writeSnapshotJson('v4diff_writer_single', snap); + + string memory json = vm.readFile('./reports/v4diff_writer_single.json'); + + // Top-level sections are present + assertTrue(vm.contains(json, '"spokeReserves"'), 'spokeReserves missing'); + assertTrue(vm.contains(json, '"spokeLiquidationConfigs"'), 'spokeLiqConfigs missing'); + assertTrue(vm.contains(json, '"hubAssets"'), 'hubAssets missing'); + assertTrue(vm.contains(json, '"spokeCaps"'), 'spokeCaps missing'); + + // Per-entity values + assertTrue(vm.contains(json, '"symbol": "USDC"'), 'reserve symbol'); + assertTrue(vm.contains(json, '"collateralFactor": 7500'), 'CF value'); + assertTrue(vm.contains(json, '"paused": false'), 'paused value'); + assertTrue(vm.contains(json, '"liquidityFee": 10'), 'liqFee value'); + assertTrue(vm.contains(json, '"addCap": 1000000'), 'addCap value'); + assertTrue(vm.contains(json, '"liquidationBonusFactor": 100'), 'liqBonusFactor value'); + + vm.removeFile('./reports/v4diff_writer_single.json'); + } + + function test_diffV4Snapshots_emitsChanges() public { + // Build the two snapshots independently so the "before" arrays aren't mutated by the + // "after" assignments, ie array references on copy. + Types.V4Snapshot memory before = _baseSnapshot(); + Types.V4Snapshot memory afterSnap = _baseSnapshot(); + afterSnap.spokeReserves[0].collateralFactor = 8000; + afterSnap.spokeReserves[0].paused = true; + afterSnap.hubAssets[0].liquidityFee = 50; + afterSnap.spokeCaps[0].addCap = 2_000_000; + afterSnap.spokeCaps[0].halted = true; + afterSnap.spokeLiquidationConfigs[0].liquidationBonusFactor = 200; + + writeV4SnapshotJson(string.concat(REPORT, '_before'), before); + writeV4SnapshotJson(string.concat(REPORT, '_after'), afterSnap); + + // ---- JSON contents: before has old values, after has new values ---- + string memory beforeJson = vm.readFile(string.concat('./reports/', REPORT, '_before.json')); + string memory afterJson = vm.readFile(string.concat('./reports/', REPORT, '_after.json')); + + assertTrue(vm.contains(beforeJson, '"collateralFactor": 7500'), 'before CF'); + assertTrue(vm.contains(afterJson, '"collateralFactor": 8000'), 'after CF'); + + assertTrue(vm.contains(beforeJson, '"paused": false'), 'before paused'); + assertTrue(vm.contains(afterJson, '"paused": true'), 'after paused'); + + assertTrue(vm.contains(beforeJson, '"liquidityFee": 10'), 'before liqFee'); + assertTrue(vm.contains(afterJson, '"liquidityFee": 50'), 'after liqFee'); + + assertTrue(vm.contains(beforeJson, '"addCap": 1000000'), 'before addCap'); + assertTrue(vm.contains(afterJson, '"addCap": 2000000'), 'after addCap'); + + assertTrue(vm.contains(beforeJson, '"halted": false'), 'before halted'); + assertTrue(vm.contains(afterJson, '"halted": true'), 'after halted'); + + assertTrue(vm.contains(beforeJson, '"liquidationBonusFactor": 100'), 'before liqBonus'); + assertTrue(vm.contains(afterJson, '"liquidationBonusFactor": 200'), 'after liqBonus'); + + // ---- Run the TypeScript CLI to render the markdown diff ---- + diffV4Snapshots(REPORT); + + string memory md = vm.readFile( + string.concat('./diffs/', REPORT, '_before_', REPORT, '_after.md') + ); + + // Spoke reserve section: BPS fields are formatted as "W.FF % [bps]" + assertTrue(vm.contains(md, '## Spoke Reserve Changes'), 'spoke reserve section'); + assertTrue(vm.contains(md, 'collateralFactor'), 'CF row'); + assertTrue(vm.contains(md, '75.00 % [7500]'), 'CF before formatted'); + assertTrue(vm.contains(md, '80.00 % [8000]'), 'CF after formatted'); + assertTrue(vm.contains(md, 'paused'), 'paused row'); + + // Hub asset section + assertTrue(vm.contains(md, '## Hub Asset Changes'), 'hub asset section'); + assertTrue(vm.contains(md, 'liquidityFee'), 'liqFee row'); + assertTrue(vm.contains(md, '0.10 % [10]'), 'liqFee before'); + assertTrue(vm.contains(md, '0.50 % [50]'), 'liqFee after'); + + // Spoke cap section — uint40s rendered with thousand separators + assertTrue(vm.contains(md, '## Hub Spoke Cap Changes'), 'spoke cap section'); + assertTrue(vm.contains(md, 'addCap'), 'addCap row'); + assertTrue(vm.contains(md, '1,000,000'), 'addCap before'); + assertTrue(vm.contains(md, '2,000,000'), 'addCap after'); + assertTrue(vm.contains(md, 'halted'), 'halted row'); + + // Spoke liquidation section + assertTrue(vm.contains(md, '## Spoke Liquidation Config Changes'), 'liq config section'); + assertTrue(vm.contains(md, 'liquidationBonusFactor'), 'liqBonus row'); + assertTrue(vm.contains(md, '1.00 % [100]'), 'liqBonus before'); + assertTrue(vm.contains(md, '2.00 % [200]'), 'liqBonus after'); + + // No noise from unchanged sections is necessary — but the raw diff block always closes the doc + assertTrue(vm.contains(md, '## Raw diff'), 'raw diff trailer'); + + _cleanup(); + } + + function _baseSnapshot() internal view returns (Types.V4Snapshot memory snap) { + snap.spokeReserves = new Types.SpokeReserveSnapshot[](1); + snap.spokeReserves[0] = Types.SpokeReserveSnapshot({ + spokeAddress: spokeA, + reserveId: 0, + underlying: underlyingUsdc, + symbol: 'USDC', + hub: hubX, + assetId: 0, + decimals: 6, + collateralRisk: 1000, + paused: false, + frozen: false, + borrowable: true, + receiveSharesEnabled: true, + dynamicConfigKey: 1, + collateralFactor: 7500, + maxLiquidationBonus: 10500, + liquidationFee: 100, + oracleAddress: oracleAddr, + priceSource: priceSource, + oraclePrice: 1e8 + }); + + snap.spokeLiquidationConfigs = new Types.SpokeLiquidationSnapshot[](1); + snap.spokeLiquidationConfigs[0] = Types.SpokeLiquidationSnapshot({ + spokeAddress: spokeA, + targetHealthFactor: 1.05e18, + healthFactorForMaxBonus: 0.95e18, + liquidationBonusFactor: 100, + maxUserReservesLimit: 8 + }); + + snap.hubAssets = new Types.HubAssetSnapshot[](1); + snap.hubAssets[0] = Types.HubAssetSnapshot({ + hubAddress: hubX, + assetId: 0, + underlying: underlyingUsdc, + symbol: 'USDC', + decimals: 6, + liquidityFee: 10, + irStrategy: irStrategy, + feeReceiver: feeReceiver, + reinvestmentController: reinvController, + optimalUsageRatio: 8000, + baseDrawnRate: 100, + rateGrowthBeforeOptimal: 400, + rateGrowthAfterOptimal: 6000, + maxDrawnRate: 30_000, + deficitRay: 0, + swept: 0, + premiumShares: 0, + premiumOffsetRay: int200(0) + }); + + snap.spokeCaps = new Types.SpokeCapSnapshot[](1); + snap.spokeCaps[0] = Types.SpokeCapSnapshot({ + hubAddress: hubX, + assetId: 0, + assetSymbol: 'USDC', + spokeAddress: spokeA, + addCap: 1_000_000, + drawCap: 500_000, + riskPremiumThreshold: 100, + active: true, + halted: false + }); + } + + function _cleanup() internal { + string memory beforePath = string.concat('./reports/', REPORT, '_before.json'); + string memory afterPath = string.concat('./reports/', REPORT, '_after.json'); + string memory diffPath = string.concat('./diffs/', REPORT, '_before_', REPORT, '_after.md'); + if (vm.exists(beforePath)) vm.removeFile(beforePath); + if (vm.exists(afterPath)) vm.removeFile(afterPath); + if (vm.exists(diffPath)) vm.removeFile(diffPath); + } +} + +/// Individual V4DiffWriter helpers for individual tests +contract V4DiffWriterHarnessTest is V4DiffWriterTestBase { + V4DiffWriterHarness internal harness; + + function setUp() public { + _setUpAddresses(); + harness = new V4DiffWriterHarness(); + vm.chainId(1); + } + + function test_serReserve_includesAllFields() public { + Types.SpokeReserveSnapshot memory r = _makeReserve(spokeA, 0, 'USDC', 7500); + string memory json = harness.serReserve(r); + + // vm.serialize* returns compact JSON (no whitespace after colons), + // unlike the pretty-printed file output. + assertTrue(vm.contains(json, '"symbol":"USDC"'), 'symbol'); + assertTrue(vm.contains(json, '"decimals":6'), 'decimals'); + assertTrue(vm.contains(json, '"collateralRisk":1000'), 'collateralRisk'); + assertTrue(vm.contains(json, '"paused":false'), 'paused'); + assertTrue(vm.contains(json, '"frozen":false'), 'frozen'); + assertTrue(vm.contains(json, '"borrowable":true'), 'borrowable'); + assertTrue(vm.contains(json, '"receiveSharesEnabled":true'), 'receiveSharesEnabled'); + assertTrue(vm.contains(json, '"dynamicConfigKey":1'), 'dynamicConfigKey'); + assertTrue(vm.contains(json, '"collateralFactor":7500'), 'collateralFactor'); + assertTrue(vm.contains(json, '"maxLiquidationBonus":10500'), 'maxLiquidationBonus'); + assertTrue(vm.contains(json, '"liquidationFee":100'), 'liquidationFee'); + assertTrue(vm.contains(json, '"oraclePrice":"100000000"'), 'oraclePrice'); + } + + function test_writeSpokeReserves_groupsBySpokeAddress() public { + Types.SpokeReserveSnapshot[] memory reserves = new Types.SpokeReserveSnapshot[](3); + reserves[0] = _makeReserve(spokeA, 0, 'USDC', 7500); + reserves[1] = _makeReserve(spokeA, 1, 'WETH', 8000); + reserves[2] = _makeReserve(spokeB, 0, 'USDC', 7000); + + string memory path = './reports/harness_spoke_reserves.json'; + harness.writeSpokeReserves(path, reserves); + + string memory json = vm.readFile(path); + assertTrue(vm.contains(json, '"spokeReserves"'), 'section'); + assertTrue(vm.contains(json, '"collateralFactor": 7500'), 'CF reserve 0'); + assertTrue(vm.contains(json, '"collateralFactor": 8000'), 'CF reserve 1'); + assertTrue(vm.contains(json, '"collateralFactor": 7000'), 'CF reserve 2'); + assertTrue(vm.contains(json, vm.toString(spokeA)), 'spoke A key'); + assertTrue(vm.contains(json, vm.toString(spokeB)), 'spoke B key'); + assertTrue(vm.contains(json, '"symbol": "USDC"'), 'USDC symbol'); + assertTrue(vm.contains(json, '"symbol": "WETH"'), 'WETH symbol'); + + vm.removeFile(path); + } + + function test_writeSpokeLiqConfigs_writesAllConfigs() public { + Types.SpokeLiquidationSnapshot[] memory configs = new Types.SpokeLiquidationSnapshot[](2); + configs[0] = Types.SpokeLiquidationSnapshot({ + spokeAddress: spokeA, + targetHealthFactor: 1.05e18, + healthFactorForMaxBonus: 0.95e18, + liquidationBonusFactor: 100, + maxUserReservesLimit: 8 + }); + configs[1] = Types.SpokeLiquidationSnapshot({ + spokeAddress: spokeB, + targetHealthFactor: 1.10e18, + healthFactorForMaxBonus: 0.90e18, + liquidationBonusFactor: 250, + maxUserReservesLimit: 12 + }); + + string memory path = './reports/harness_spoke_liq.json'; + harness.writeSpokeLiqConfigs(path, configs); + + string memory json = vm.readFile(path); + assertTrue(vm.contains(json, '"spokeLiquidationConfigs"'), 'section'); + assertTrue(vm.contains(json, '"liquidationBonusFactor": 100'), 'first liqBonus'); + assertTrue(vm.contains(json, '"liquidationBonusFactor": 250'), 'second liqBonus'); + assertTrue(vm.contains(json, '"maxUserReservesLimit": 8'), 'first limit'); + assertTrue(vm.contains(json, '"maxUserReservesLimit": 12'), 'second limit'); + assertTrue(vm.contains(json, '"targetHealthFactor": "1050000000000000000"'), 'first HF'); + + vm.removeFile(path); + } + + function test_serializeHubAsset_includesAllFields() public { + Types.HubAssetSnapshot memory a = _makeHubAsset(hubX, 0, 'USDC', 10); + string memory json = harness.serializeHubAsset(a); + + assertTrue(vm.contains(json, '"symbol":"USDC"'), 'symbol'); + assertTrue(vm.contains(json, '"decimals":6'), 'decimals'); + assertTrue(vm.contains(json, '"liquidityFee":10'), 'liquidityFee'); + assertTrue(vm.contains(json, '"optimalUsageRatio":8000'), 'optimalUsageRatio'); + assertTrue(vm.contains(json, '"baseDrawnRate":100'), 'baseDrawnRate'); + assertTrue(vm.contains(json, '"rateGrowthBeforeOptimal":400'), 'rateGrowthBeforeOptimal'); + assertTrue(vm.contains(json, '"rateGrowthAfterOptimal":6000'), 'rateGrowthAfterOptimal'); + assertTrue(vm.contains(json, '"maxDrawnRate":"30000"'), 'maxDrawnRate'); + assertTrue(vm.contains(json, '"deficitRay":"0"'), 'deficitRay'); + assertTrue(vm.contains(json, '"premiumOffsetRay":"0"'), 'premiumOffsetRay'); + } + + function test_writeHubAssets_groupsByHubAddress() public { + Types.HubAssetSnapshot[] memory assets = new Types.HubAssetSnapshot[](3); + assets[0] = _makeHubAsset(hubX, 0, 'USDC', 10); + assets[1] = _makeHubAsset(hubX, 1, 'WETH', 20); + assets[2] = _makeHubAsset(hubY, 0, 'USDC', 30); + + string memory path = './reports/harness_hub_assets.json'; + harness.writeHubAssets(path, assets); + + string memory json = vm.readFile(path); + assertTrue(vm.contains(json, '"hubAssets"'), 'section'); + assertTrue(vm.contains(json, '"liquidityFee": 10'), 'asset 0 fee'); + assertTrue(vm.contains(json, '"liquidityFee": 20'), 'asset 1 fee'); + assertTrue(vm.contains(json, '"liquidityFee": 30'), 'asset 2 fee'); + assertTrue(vm.contains(json, vm.toString(hubX)), 'hub X key'); + assertTrue(vm.contains(json, vm.toString(hubY)), 'hub Y key'); + + vm.removeFile(path); + } + + function test_writeSpokeCaps_writesAllCaps() public { + Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](2); + caps[0] = Types.SpokeCapSnapshot({ + hubAddress: hubX, + assetId: 0, + assetSymbol: 'USDC', + spokeAddress: spokeA, + addCap: 1_000_000, + drawCap: 500_000, + riskPremiumThreshold: 100, + active: true, + halted: false + }); + caps[1] = Types.SpokeCapSnapshot({ + hubAddress: hubX, + assetId: 0, + assetSymbol: 'USDC', + spokeAddress: spokeB, + addCap: 2_000_000, + drawCap: 800_000, + riskPremiumThreshold: 200, + active: false, + halted: true + }); + + string memory path = './reports/harness_spoke_caps.json'; + harness.writeSpokeCaps(path, caps); + + string memory json = vm.readFile(path); + assertTrue(vm.contains(json, '"spokeCaps"'), 'section'); + assertTrue(vm.contains(json, '"addCap": 1000000'), 'first addCap'); + assertTrue(vm.contains(json, '"addCap": 2000000'), 'second addCap'); + assertTrue(vm.contains(json, '"drawCap": 500000'), 'first drawCap'); + assertTrue(vm.contains(json, '"drawCap": 800000'), 'second drawCap'); + assertTrue(vm.contains(json, '"active": true'), 'first active'); + assertTrue(vm.contains(json, '"active": false'), 'second active'); + assertTrue(vm.contains(json, '"halted": false'), 'first halted'); + assertTrue(vm.contains(json, '"halted": true'), 'second halted'); + + vm.removeFile(path); + } +} diff --git a/tests/dependencies/v4/V4DiffWriterHarness.sol b/tests/dependencies/v4/V4DiffWriterHarness.sol new file mode 100644 index 00000000..963c2166 --- /dev/null +++ b/tests/dependencies/v4/V4DiffWriterHarness.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {V4DiffWriter} from 'src/dependencies/v4/V4DiffWriter.sol'; +import {Types} from 'src/dependencies/v4/Types.sol'; + +/// @title V4DiffWriterHarness +/// @notice Harness contract that exposes internal helpers as external entrypoints +contract V4DiffWriterHarness { + function writeSnapshotJson(string memory reportName, Types.V4Snapshot memory snapshot) external { + V4DiffWriter.writeSnapshotJson(reportName, snapshot); + } + + function writeSpokeReserves( + string memory path, + Types.SpokeReserveSnapshot[] memory reserves + ) external { + V4DiffWriter._writeSpokeReserves(path, reserves); + } + + function serReserve(Types.SpokeReserveSnapshot memory r) external returns (string memory) { + return V4DiffWriter._serReserve(r); + } + + function writeSpokeLiqConfigs( + string memory path, + Types.SpokeLiquidationSnapshot[] memory configs + ) external { + V4DiffWriter._writeSpokeLiqConfigs(path, configs); + } + + function writeHubAssets(string memory path, Types.HubAssetSnapshot[] memory assets) external { + V4DiffWriter._writeHubAssets(path, assets); + } + + function serializeHubAsset(Types.HubAssetSnapshot memory a) external returns (string memory) { + return V4DiffWriter._serializeHubAsset(a); + } + + function writeSpokeCaps(string memory path, Types.SpokeCapSnapshot[] memory caps) external { + V4DiffWriter._writeSpokeCaps(path, caps); + } +} From ed9b99c8616580b37c47062874add08a596cccb3 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Mon, 11 May 2026 15:37:46 -0500 Subject: [PATCH 23/29] fix: mv harness file --- tests/dependencies/v4/V4DiffWriter.t.sol | 2 +- tests/{dependencies => mocks}/v4/V4DiffWriterHarness.sol | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/{dependencies => mocks}/v4/V4DiffWriterHarness.sol (100%) diff --git a/tests/dependencies/v4/V4DiffWriter.t.sol b/tests/dependencies/v4/V4DiffWriter.t.sol index e1d7a500..bd5f39b7 100644 --- a/tests/dependencies/v4/V4DiffWriter.t.sol +++ b/tests/dependencies/v4/V4DiffWriter.t.sol @@ -5,7 +5,7 @@ import 'forge-std/Test.sol'; import {SnapshotV4} from 'src/dependencies/v4/SnapshotV4.sol'; import {V4DiffWriter} from 'src/dependencies/v4/V4DiffWriter.sol'; import {Types} from 'src/dependencies/v4/Types.sol'; -import {V4DiffWriterHarness} from './V4DiffWriterHarness.sol'; +import {V4DiffWriterHarness} from 'tests/mocks/v4/V4DiffWriterHarness.sol'; abstract contract V4DiffWriterTestBase is Test { address internal spokeA; diff --git a/tests/dependencies/v4/V4DiffWriterHarness.sol b/tests/mocks/v4/V4DiffWriterHarness.sol similarity index 100% rename from tests/dependencies/v4/V4DiffWriterHarness.sol rename to tests/mocks/v4/V4DiffWriterHarness.sol From 0474ce20a8bb1f2b9a337f99d4d81435884a73bb Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Mon, 11 May 2026 16:23:20 -0500 Subject: [PATCH 24/29] fix: rename struct, obj --- .../__tests__/protocol-diff-v4.spec.ts | 34 +++++----- packages/aave-helpers-js/formatters-v4.ts | 32 ++++----- packages/aave-helpers-js/index.ts | 2 +- packages/aave-helpers-js/protocol-diff-v4.ts | 4 +- .../{spoke-caps.ts => spoke-configs.ts} | 67 ++++++++++--------- packages/aave-helpers-js/snapshot-types-v4.ts | 8 +-- src/ProtocolV4TestBase.sol | 2 +- src/dependencies/v4/SnapshotV4.sol | 12 ++-- src/dependencies/v4/Types.sol | 4 +- src/dependencies/v4/V4DiffWriter.sol | 10 +-- tests/ProtocolV4TestBase.t.sol | 56 ++++++++-------- tests/dependencies/v4/V4DiffWriter.t.sol | 24 +++---- tests/mocks/v4/V4DiffWriterHarness.sol | 7 +- 13 files changed, 133 insertions(+), 129 deletions(-) rename packages/aave-helpers-js/sections/{spoke-caps.ts => spoke-configs.ts} (52%) diff --git a/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts index c2ce71cd..5567d978 100644 --- a/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts +++ b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts @@ -70,7 +70,7 @@ function makeSnapshot(overrides?: Partial): AaveV4Snapshot { }, }, }, - spokeCaps: { + spokeConfigs: { [`${HUB_ADDR}_0_${SPOKE_ADDR}`]: { assetSymbol: 'WETH', addCap: 1000000, @@ -172,8 +172,8 @@ describe('BPS formatting via formatV4Value', () => { it('formats booleans as checkmarks', () => { expect(formatV4Value('spokeReserve', 'paused', true, ctx)).toBe(':white_check_mark:'); expect(formatV4Value('spokeReserve', 'frozen', false, ctx)).toBe(':x:'); - expect(formatV4Value('spokeCap', 'active', true, ctx)).toBe(':white_check_mark:'); - expect(formatV4Value('spokeCap', 'halted', false, ctx)).toBe(':x:'); + expect(formatV4Value('spokeConfig', 'active', true, ctx)).toBe(':white_check_mark:'); + expect(formatV4Value('spokeConfig', 'halted', false, ctx)).toBe(':x:'); }); it('formats addresses as explorer links', () => { @@ -189,7 +189,7 @@ describe('BPS formatting via formatV4Value', () => { it('formats spoke cap uint40 fields with separators, asset symbol, and exponential', () => { const capCtx = { ...ctx, - spokeCap: { + spokeConfig: { assetSymbol: 'USDT', addCap: 0, drawCap: 0, @@ -198,12 +198,12 @@ describe('BPS formatting via formatV4Value', () => { halted: false, }, }; - expect(formatV4Value('spokeCap', 'addCap', 1000000, capCtx)).toBe('1,000,000 (1e6) USDT'); - expect(formatV4Value('spokeCap', 'drawCap', 1880000, capCtx)).toBe('1,880,000 (1.88e6) USDT'); + expect(formatV4Value('spokeConfig', 'addCap', 1000000, capCtx)).toBe('1,000,000 (1e6) USDT'); + expect(formatV4Value('spokeConfig', 'drawCap', 1880000, capCtx)).toBe('1,880,000 (1.88e6) USDT'); // Falls back gracefully when symbol unavailable - expect(formatV4Value('spokeCap', 'addCap', 1000000, ctx)).toBe('1,000,000 (1e6)'); + expect(formatV4Value('spokeConfig', 'addCap', 1000000, ctx)).toBe('1,000,000 (1e6)'); // Small caps (< 1000) skip the exponential - expect(formatV4Value('spokeCap', 'addCap', 500, capCtx)).toBe('500 USDT'); + expect(formatV4Value('spokeConfig', 'addCap', 500, capCtx)).toBe('500 USDT'); }); it('formats numeric-string fields (oraclePrice, swept, premiumShares) with separators + exp', () => { @@ -314,14 +314,14 @@ describe('diffV4Snapshots', () => { const before = makeSnapshot(); const after = makeSnapshot(); const capKey = `${HUB_ADDR}_0_${SPOKE_ADDR}`; - after.spokeCaps[capKey] = { - ...after.spokeCaps[capKey], + after.spokeConfigs[capKey] = { + ...after.spokeConfigs[capKey], addCap: 2000000, halted: true, }; const md = await diffV4Snapshots(before, after); - expect(md).toContain('## Hub Spoke Cap Changes'); + expect(md).toContain('## Hub Spoke Config Changes'); expect(md).toContain('addCap'); expect(md).toContain('halted'); }); @@ -396,7 +396,7 @@ describe('diffV4Snapshots', () => { const before = makeSnapshot(); const after = makeSnapshot(); const newCapKey = `${HUB_ADDR}_1_${SPOKE_ADDR}`; - after.spokeCaps[newCapKey] = { + after.spokeConfigs[newCapKey] = { assetSymbol: 'USDC', addCap: 500000, drawCap: 250000, @@ -406,7 +406,7 @@ describe('diffV4Snapshots', () => { }; const md = await diffV4Snapshots(before, after); - expect(md).toContain('## Hub Spoke Cap Changes'); + expect(md).toContain('## Hub Spoke Config Changes'); expect(md).toContain('NEW SPOKE'); expect(md).toContain('USDC'); }); @@ -415,10 +415,10 @@ describe('diffV4Snapshots', () => { const before = makeSnapshot(); const after = makeSnapshot(); const capKey = `${HUB_ADDR}_0_${SPOKE_ADDR}`; - delete after.spokeCaps[capKey]; + delete after.spokeConfigs[capKey]; const md = await diffV4Snapshots(before, after); - expect(md).toContain('## Hub Spoke Cap Changes'); + expect(md).toContain('## Hub Spoke Config Changes'); expect(md).toContain('REMOVED'); }); @@ -475,8 +475,8 @@ describe('diffV4Snapshots', () => { const before = makeSnapshot(); const after = makeSnapshot(); const capKey = `${HUB_ADDR}_0_${SPOKE_ADDR}`; - after.spokeCaps[capKey] = { - ...after.spokeCaps[capKey], + after.spokeConfigs[capKey] = { + ...after.spokeConfigs[capKey], addCap: 9999999, }; diff --git a/packages/aave-helpers-js/formatters-v4.ts b/packages/aave-helpers-js/formatters-v4.ts index 4afabf93..46a704f9 100644 --- a/packages/aave-helpers-js/formatters-v4.ts +++ b/packages/aave-helpers-js/formatters-v4.ts @@ -4,7 +4,7 @@ import { toAddressLink, boolToMarkdown } from './utils/markdown'; import type { V4SpokeReserve, V4HubAsset, - V4SpokeCap, + V4SpokeConfig, V4SpokeLiquidationConfig, } from './snapshot-types-v4'; @@ -12,7 +12,7 @@ import type { export interface V4FormatterContext { chainId: number; - spokeCap?: V4SpokeCap; + spokeConfig?: V4SpokeConfig; } export type FieldFormatter = (value: T, ctx: V4FormatterContext) => string; @@ -139,27 +139,27 @@ hubAssetFormatters['premiumOffsetRay'] = (value) => `${formatUnits(BigInt(value) // --- Spoke Cap formatters --- -type SpokeCapKey = keyof V4SpokeCap; +type SpokeConfigKey = keyof V4SpokeConfig; -const SPOKE_CAP_BOOL_FIELDS: readonly SpokeCapKey[] = ['active', 'halted'] as const; +const SPOKE_CONFIG_FLAGS: readonly SpokeConfigKey[] = ['active', 'halted'] as const; /** uint40 token-unit cap fields — formatted with thousands separators + asset symbol. */ -const SPOKE_CAP_TOKEN_AMOUNT_FIELDS: readonly SpokeCapKey[] = ['addCap', 'drawCap'] as const; +const SPOKE_CONFIG_TOKEN_AMOUNT_FIELDS: readonly SpokeConfigKey[] = ['addCap', 'drawCap'] as const; -export const spokeCapFormatters: Partial<{ - [K in SpokeCapKey]: FieldFormatter; +export const spokeConfigFormatters: Partial<{ + [K in SpokeConfigKey]: FieldFormatter; }> = {}; -for (const field of SPOKE_CAP_BOOL_FIELDS) { - (spokeCapFormatters[field] as FieldFormatter) = (value) => boolToMarkdown(value); +for (const field of SPOKE_CONFIG_FLAGS) { + (spokeConfigFormatters[field] as FieldFormatter) = (value) => boolToMarkdown(value); } -for (const field of SPOKE_CAP_TOKEN_AMOUNT_FIELDS) { - (spokeCapFormatters[field] as FieldFormatter) = (value, ctx) => - formatBigIntWithExp(BigInt(value), ctx.spokeCap?.assetSymbol); +for (const field of SPOKE_CONFIG_TOKEN_AMOUNT_FIELDS) { + (spokeConfigFormatters[field] as FieldFormatter) = (value, ctx) => + formatBigIntWithExp(BigInt(value), ctx.spokeConfig?.assetSymbol); } -spokeCapFormatters['riskPremiumThreshold'] = (value) => formatBps(value); +spokeConfigFormatters['riskPremiumThreshold'] = (value) => formatBps(value); // --- Spoke Liquidation Config formatters --- @@ -183,14 +183,14 @@ spokeLiqFormatters['liquidationBonusFactor'] = (value) => formatBps(value); type V4SectionFormatters = { spokeReserve: typeof spokeReserveFormatters; hubAsset: typeof hubAssetFormatters; - spokeCap: typeof spokeCapFormatters; + spokeConfig: typeof spokeConfigFormatters; spokeLiq: typeof spokeLiqFormatters; }; const formattersMap: V4SectionFormatters = { spokeReserve: spokeReserveFormatters, hubAsset: hubAssetFormatters, - spokeCap: spokeCapFormatters, + spokeConfig: spokeConfigFormatters, spokeLiq: spokeLiqFormatters, } as const; @@ -208,7 +208,7 @@ export function formatV4Value( if (typeof value === 'number') return value.toLocaleString('en-US'); if (isAddress(value)) return addressLink(value as string, ctx.chainId); // Pure numeric strings (uint serialized as string) — render with thousand separators - // and a parenthetical exponential ("1,500,000 (1.5e6)") so price feeds and large uints + // and exponent in parens ("1,500,000 (1.5e6)") so price feeds and large uints // are easy to scan at both scales. if (typeof value === 'string' && /^-?\d+$/.test(value)) { return formatBigIntWithExp(BigInt(value)); diff --git a/packages/aave-helpers-js/index.ts b/packages/aave-helpers-js/index.ts index 9e2957f6..9fcf47af 100644 --- a/packages/aave-helpers-js/index.ts +++ b/packages/aave-helpers-js/index.ts @@ -19,6 +19,6 @@ export type { AaveV4Snapshot, V4SpokeReserve, V4HubAsset, - V4SpokeCap, + V4SpokeConfig, V4SpokeLiquidationConfig, } from './snapshot-types-v4'; diff --git a/packages/aave-helpers-js/protocol-diff-v4.ts b/packages/aave-helpers-js/protocol-diff-v4.ts index 98b58bd5..5c366ffe 100644 --- a/packages/aave-helpers-js/protocol-diff-v4.ts +++ b/packages/aave-helpers-js/protocol-diff-v4.ts @@ -3,7 +3,7 @@ import type { AaveV4Snapshot } from './snapshot-types-v4'; import type { RawStorage, Log } from './snapshot-types'; import { renderSpokeReservesSection } from './sections/spoke-reserves'; import { renderHubAssetsSection } from './sections/hub-assets'; -import { renderSpokeCapsSection } from './sections/spoke-caps'; +import { renderSpokeConfigsSection } from './sections/spoke-configs'; import { renderSpokeLiquidationSection } from './sections/spoke-liquidation'; import { renderRawSection } from './sections/raw'; import { renderLogsSection } from './sections/logs'; @@ -37,7 +37,7 @@ export async function diffV4Snapshots( md += renderSpokeReservesSection(before, postCopy); md += renderHubAssetsSection(before, postCopy); - md += renderSpokeCapsSection(before, postCopy); + md += renderSpokeConfigsSection(before, postCopy); md += renderSpokeLiquidationSection(before, postCopy); md += await renderLogsSection(logs, after.chainId); md += renderRawSection(raw, after.chainId); diff --git a/packages/aave-helpers-js/sections/spoke-caps.ts b/packages/aave-helpers-js/sections/spoke-configs.ts similarity index 52% rename from packages/aave-helpers-js/sections/spoke-caps.ts rename to packages/aave-helpers-js/sections/spoke-configs.ts index 4b051303..b43b5a97 100644 --- a/packages/aave-helpers-js/sections/spoke-caps.ts +++ b/packages/aave-helpers-js/sections/spoke-configs.ts @@ -1,12 +1,11 @@ import type { Hex } from 'viem'; import { getClient } from '@bgd-labs/toolbox'; -import type { AaveV4Snapshot, V4SpokeCap } from '../snapshot-types-v4'; +import type { AaveV4Snapshot, V4SpokeConfig } from '../snapshot-types-v4'; import { formatV4Value, type V4FormatterContext } from '../formatters-v4'; import { toAddressLink } from '../utils/markdown'; -/** All fields in display order. Every field is compared so unexpected mutations - * are never silently missed. */ -const FIELD_ORDER: (keyof V4SpokeCap)[] = [ +/** All fields in display order. Every field is compared */ +const FIELD_ORDER: (keyof V4SpokeConfig)[] = [ 'assetSymbol', 'addCap', 'drawCap', @@ -17,9 +16,8 @@ const FIELD_ORDER: (keyof V4SpokeCap)[] = [ /** * Parse the composite key "hubAddr_assetId_spokeAddr". - * Ethereum addresses are 42 chars (0x + 40 hex), so the split is unambiguous. */ -function parseCapKey(key: string): { hubAddr: string; assetId: string; spokeAddr: string } { +function parseConfigKey(key: string): { hubAddr: string; assetId: string; spokeAddr: string } { const hubAddr = key.slice(0, 42); // skip the underscore after hub address const rest = key.slice(43); @@ -29,8 +27,8 @@ function parseCapKey(key: string): { hubAddr: string; assetId: string; spokeAddr return { hubAddr, assetId, spokeAddr }; } -function capHeader( - cap: V4SpokeCap, +function configHeader( + cfg: V4SpokeConfig, hubAddr: string, assetId: string, spokeAddr: string, @@ -39,73 +37,76 @@ function capHeader( const client = getClient(chainId, {}); const hubLink = toAddressLink(hubAddr as Hex, true, client); const spokeLink = toAddressLink(spokeAddr as Hex, true, client); - return `### ${cap.assetSymbol} (assetId: ${assetId}) on Hub ${hubLink} / Spoke ${spokeLink}\n\n`; + return `### ${cfg.assetSymbol} (assetId: ${assetId}) on Hub ${hubLink} / Spoke ${spokeLink}\n\n`; } -function renderNewCap( - cap: V4SpokeCap, +function renderNewConfig( + cfg: V4SpokeConfig, hubAddr: string, assetId: string, spokeAddr: string, ctx: V4FormatterContext ): string { - const capCtx: V4FormatterContext = { ...ctx, spokeCap: cap }; - let md = capHeader(cap, hubAddr, assetId, spokeAddr, ctx.chainId); + const cfgCtx: V4FormatterContext = { ...ctx, spokeConfig: cfg }; + let md = configHeader(cfg, hubAddr, assetId, spokeAddr, ctx.chainId); md += '**NEW SPOKE**\n\n'; md += '| description | value |\n| --- | --- |\n'; for (const key of FIELD_ORDER) { - md += `| ${key} | ${formatV4Value('spokeCap', key, cap[key], capCtx)} |\n`; + md += `| ${key} | ${formatV4Value('spokeConfig', key, cfg[key], cfgCtx)} |\n`; } return md + '\n'; } -function renderCapDiff( - before: V4SpokeCap, - after: V4SpokeCap, +function renderConfigDiff( + before: V4SpokeConfig, + after: V4SpokeConfig, hubAddr: string, assetId: string, spokeAddr: string, ctx: V4FormatterContext ): string { - const capCtx: V4FormatterContext = { ...ctx, spokeCap: after }; + const cfgCtx: V4FormatterContext = { ...ctx, spokeConfig: after }; const rows: string[] = []; for (const key of FIELD_ORDER) { const bVal = before[key]; const aVal = after[key]; if (String(bVal) === String(aVal)) continue; - const fromFmt = formatV4Value('spokeCap', key, bVal, capCtx); - const toFmt = formatV4Value('spokeCap', key, aVal, capCtx); + const fromFmt = formatV4Value('spokeConfig', key, bVal, cfgCtx); + const toFmt = formatV4Value('spokeConfig', key, aVal, cfgCtx); rows.push(`| ${key} | ${fromFmt} | ${toFmt} |`); } if (rows.length === 0) return ''; - let md = capHeader(after, hubAddr, assetId, spokeAddr, ctx.chainId); + let md = configHeader(after, hubAddr, assetId, spokeAddr, ctx.chainId); md += '| description | value before | value after |\n| --- | --- | --- |\n'; md += rows.join('\n') + '\n'; return md + '\n'; } -export function renderSpokeCapsSection(before: AaveV4Snapshot, after: AaveV4Snapshot): string { +export function renderSpokeConfigsSection(before: AaveV4Snapshot, after: AaveV4Snapshot): string { const ctx: V4FormatterContext = { chainId: after.chainId }; - const allKeys = new Set([...Object.keys(before.spokeCaps), ...Object.keys(after.spokeCaps)]); + const allKeys = new Set([ + ...Object.keys(before.spokeConfigs), + ...Object.keys(after.spokeConfigs), + ]); let body = ''; for (const key of allKeys) { - const { hubAddr, assetId, spokeAddr } = parseCapKey(key); - const bCap = before.spokeCaps[key]; - const aCap = after.spokeCaps[key]; + const { hubAddr, assetId, spokeAddr } = parseConfigKey(key); + const bCfg = before.spokeConfigs[key]; + const aCfg = after.spokeConfigs[key]; - if (bCap && aCap) { - body += renderCapDiff(bCap, aCap, hubAddr, assetId, spokeAddr, ctx); - } else if (aCap) { - body += renderNewCap(aCap, hubAddr, assetId, spokeAddr, ctx); - } else if (bCap) { - body += capHeader(bCap, hubAddr, assetId, spokeAddr, ctx.chainId) + '**REMOVED**\n\n'; + if (bCfg && aCfg) { + body += renderConfigDiff(bCfg, aCfg, hubAddr, assetId, spokeAddr, ctx); + } else if (aCfg) { + body += renderNewConfig(aCfg, hubAddr, assetId, spokeAddr, ctx); + } else if (bCfg) { + body += configHeader(bCfg, hubAddr, assetId, spokeAddr, ctx.chainId) + '**REMOVED**\n\n'; } } if (!body) return ''; - return `## Hub Spoke Cap Changes\n\n${body}`; + return `## Hub Spoke Config Changes\n\n${body}`; } diff --git a/packages/aave-helpers-js/snapshot-types-v4.ts b/packages/aave-helpers-js/snapshot-types-v4.ts index 591e838f..0f2eeccf 100644 --- a/packages/aave-helpers-js/snapshot-types-v4.ts +++ b/packages/aave-helpers-js/snapshot-types-v4.ts @@ -60,9 +60,9 @@ export const v4HubAssetSchema = z.object({ export type V4HubAsset = z.infer; -// --- Spoke Cap --- +// --- Spoke Config --- -export const v4SpokeCapSchema = z.object({ +export const v4SpokeConfigSchema = z.object({ assetSymbol: z.string(), addCap: z.number(), // uint40 — fits in JS safe int drawCap: z.number(), // uint40 — fits in JS safe int @@ -71,7 +71,7 @@ export const v4SpokeCapSchema = z.object({ halted: z.boolean(), }); -export type V4SpokeCap = z.infer; +export type V4SpokeConfig = z.infer; // --- Full V4 Snapshot --- @@ -80,7 +80,7 @@ export const aaveV4SnapshotSchema = z.object({ spokeReserves: z.record(z.string(), z.record(z.string(), v4SpokeReserveSchema)), spokeLiquidationConfigs: z.record(z.string(), v4SpokeLiquidationConfigSchema), hubAssets: z.record(z.string(), z.record(z.string(), v4HubAssetSchema)), - spokeCaps: z.record(z.string(), v4SpokeCapSchema), + spokeConfigs: z.record(z.string(), v4SpokeConfigSchema), raw: rawStorageSchema.optional(), logs: z.array(logSchema).optional(), }); diff --git a/src/ProtocolV4TestBase.sol b/src/ProtocolV4TestBase.sol index fed882dc..3c1988c1 100644 --- a/src/ProtocolV4TestBase.sol +++ b/src/ProtocolV4TestBase.sol @@ -68,7 +68,7 @@ contract ProtocolV4TestBase is SnapshotV4, Scenarios, TokenizationScenarios, Gat /// @notice Sanity-check post-payload spoke caps for known invariants. /// @dev Liquidity is pooled at the hub, so invariant is per-asset aggregate across all spokes function configChangePlausibilityTest(Types.V4Snapshot memory snapshotAfter) public pure { - Types.SpokeCapSnapshot[] memory caps = snapshotAfter.spokeCaps; + Types.SpokeConfigSnapshot[] memory caps = snapshotAfter.spokeConfigs; for (uint256 i; i < caps.length; i++) { // Skip (hub, assetId) groups already aggregated in a prior iteration. bool alreadyAggregated = false; diff --git a/src/dependencies/v4/SnapshotV4.sol b/src/dependencies/v4/SnapshotV4.sol index 6c2ff547..addc6e88 100644 --- a/src/dependencies/v4/SnapshotV4.sol +++ b/src/dependencies/v4/SnapshotV4.sol @@ -19,7 +19,7 @@ abstract contract SnapshotV4 is Helpers { snapshot.spokeReserves = _snapshotSpokeReserves(spokes); snapshot.spokeLiquidationConfigs = _snapshotSpokeLiqConfigs(spokes); snapshot.hubAssets = _snapshotHubAssets(hubs); - snapshot.spokeCaps = _snapshotSpokeCaps(hubs); + snapshot.spokeConfigs = _snapshotSpokeConfigs(hubs); } /// @notice Write a V4 snapshot to JSON file. @@ -180,16 +180,16 @@ abstract contract SnapshotV4 is Helpers { // Hub spoke caps - function _snapshotSpokeCaps( + function _snapshotSpokeConfigs( IHub[] memory hubs - ) private view returns (Types.SpokeCapSnapshot[] memory) { + ) private view returns (Types.SpokeConfigSnapshot[] memory) { uint256 total; for (uint256 h; h < hubs.length; h++) { uint256 ac = hubs[h].getAssetCount(); for (uint256 a; a < ac; a++) total += hubs[h].getSpokeCount(a); } - Types.SpokeCapSnapshot[] memory result = new Types.SpokeCapSnapshot[](total); + Types.SpokeConfigSnapshot[] memory result = new Types.SpokeConfigSnapshot[](total); uint256 idx; for (uint256 h; h < hubs.length; h++) { idx = _snapshotCapsForHub(hubs[h], result, idx); @@ -199,7 +199,7 @@ abstract contract SnapshotV4 is Helpers { function _snapshotCapsForHub( IHub hub, - Types.SpokeCapSnapshot[] memory result, + Types.SpokeConfigSnapshot[] memory result, uint256 idx ) private view returns (uint256) { uint256 ac = hub.getAssetCount(); @@ -210,7 +210,7 @@ abstract contract SnapshotV4 is Helpers { for (uint256 sp; sp < sc; sp++) { address spokeAddr = hub.getSpokeAddress(a, sp); IHub.SpokeConfig memory cfg = hub.getSpokeConfig(a, spokeAddr); - result[idx++] = Types.SpokeCapSnapshot({ + result[idx++] = Types.SpokeConfigSnapshot({ hubAddress: address(hub), assetId: a, assetSymbol: sym, diff --git a/src/dependencies/v4/Types.sol b/src/dependencies/v4/Types.sol index 4cfbe0ff..b3b5b4fe 100644 --- a/src/dependencies/v4/Types.sol +++ b/src/dependencies/v4/Types.sol @@ -106,7 +106,7 @@ library Types { int200 premiumOffsetRay; } - struct SpokeCapSnapshot { + struct SpokeConfigSnapshot { address hubAddress; uint256 assetId; string assetSymbol; @@ -122,6 +122,6 @@ library Types { SpokeReserveSnapshot[] spokeReserves; SpokeLiquidationSnapshot[] spokeLiquidationConfigs; HubAssetSnapshot[] hubAssets; - SpokeCapSnapshot[] spokeCaps; + SpokeConfigSnapshot[] spokeConfigs; } } diff --git a/src/dependencies/v4/V4DiffWriter.sol b/src/dependencies/v4/V4DiffWriter.sol index c4d96877..b9cf7232 100644 --- a/src/dependencies/v4/V4DiffWriter.sol +++ b/src/dependencies/v4/V4DiffWriter.sol @@ -16,14 +16,14 @@ library V4DiffWriter { string memory path = string.concat('./reports/', reportName, '.json'); vm.writeFile( path, - '{ "spokeReserves": {}, "spokeLiquidationConfigs": {}, "hubAssets": {}, "spokeCaps": {} }' + '{ "spokeReserves": {}, "spokeLiquidationConfigs": {}, "hubAssets": {}, "spokeConfigs": {} }' ); vm.serializeUint('root', 'chainId', block.chainid); _writeSpokeReserves(path, snapshot.spokeReserves); _writeSpokeLiqConfigs(path, snapshot.spokeLiquidationConfigs); _writeHubAssets(path, snapshot.hubAssets); - _writeSpokeCaps(path, snapshot.spokeCaps); + _writeSpokeConfigs(path, snapshot.spokeConfigs); } function _writeSpokeReserves( @@ -147,8 +147,8 @@ library V4DiffWriter { return vm.serializeString(k, 'premiumOffsetRay', vm.toString(a.premiumOffsetRay)); } - function _writeSpokeCaps(string memory path, Types.SpokeCapSnapshot[] memory caps) internal { - string memory sectionKey = 'spokeCaps'; + function _writeSpokeConfigs(string memory path, Types.SpokeConfigSnapshot[] memory caps) internal { + string memory sectionKey = 'spokeConfigs'; string memory content = '{}'; vm.serializeJson(sectionKey, '{}'); @@ -169,6 +169,6 @@ library V4DiffWriter { string memory obj = vm.serializeBool(k, 'halted', caps[i].halted); content = vm.serializeString(sectionKey, k, obj); } - vm.writeJson(vm.serializeString('root', 'spokeCaps', content), path); + vm.writeJson(vm.serializeString('root', 'spokeConfigs', content), path); } } diff --git a/tests/ProtocolV4TestBase.t.sol b/tests/ProtocolV4TestBase.t.sol index dfb44051..eed09c36 100644 --- a/tests/ProtocolV4TestBase.t.sol +++ b/tests/ProtocolV4TestBase.t.sol @@ -240,29 +240,29 @@ contract ProtocolV4TestStorageValidation is ProtocolV4TestBaseTest { } contract ProtocolV4TestPlausibility is Test, ProtocolV4TestBase { - address internal HUB_A = makeAddr('HUB_A'); - address internal HUB_B = makeAddr('HUB_B'); + address internal hubA = makeAddr('hubA'); + address internal hubB = makeAddr('hubB'); function test_emptyCapsPasses() public view { - Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](0); + Types.SpokeConfigSnapshot[] memory caps = new Types.SpokeConfigSnapshot[](0); configChangePlausibilityTest(_snapshot(caps)); } function test_zeroDrawCapSkipped() public view { - Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](1); - caps[0] = _cap(HUB_A, 0, 0, 0); + Types.SpokeConfigSnapshot[] memory caps = new Types.SpokeConfigSnapshot[](1); + caps[0] = _cap(hubA, 0, 0, 0); configChangePlausibilityTest(_snapshot(caps)); } function test_singleSpokeDrawLeAddPasses() public view { - Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](1); - caps[0] = _cap(HUB_A, 0, 1_000_000, 500_000); + Types.SpokeConfigSnapshot[] memory caps = new Types.SpokeConfigSnapshot[](1); + caps[0] = _cap(hubA, 0, 1_000_000, 500_000); configChangePlausibilityTest(_snapshot(caps)); } function test_singleSpokeDrawGtAddReverts() public { - Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](1); - caps[0] = _cap(HUB_A, 0, 500_000, 1_000_000); + Types.SpokeConfigSnapshot[] memory caps = new Types.SpokeConfigSnapshot[](1); + caps[0] = _cap(hubA, 0, 500_000, 1_000_000); vm.expectRevert(bytes('PL_ADD_LT_DRAW')); this.configChangePlausibilityTest(_snapshot(caps)); } @@ -270,37 +270,37 @@ contract ProtocolV4TestPlausibility is Test, ProtocolV4TestBase { function test_borrowOnlySpokePassesWhenAggregateHolds() public view { // Spoke A: borrow-only (addCap=0, drawCap=1M) — invalid per-spoke but valid in V4. // Spoke B: supply-only (addCap=5M, drawCap=0). Aggregate: 1M draw <= 5M add. - Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](2); - caps[0] = _cap(HUB_A, 0, 0, 1_000_000); - caps[1] = _cap(HUB_A, 0, 5_000_000, 0); + Types.SpokeConfigSnapshot[] memory caps = new Types.SpokeConfigSnapshot[](2); + caps[0] = _cap(hubA, 0, 0, 1_000_000); + caps[1] = _cap(hubA, 0, 5_000_000, 0); configChangePlausibilityTest(_snapshot(caps)); } function test_aggregateDrawGtAggregateAddReverts() public { // Same (hub, asset), two spokes: 2M+1M=3M add, 2M+2M=4M draw -> 4M > 3M reverts. - Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](2); - caps[0] = _cap(HUB_A, 0, 2_000_000, 2_000_000); - caps[1] = _cap(HUB_A, 0, 1_000_000, 2_000_000); + Types.SpokeConfigSnapshot[] memory caps = new Types.SpokeConfigSnapshot[](2); + caps[0] = _cap(hubA, 0, 2_000_000, 2_000_000); + caps[1] = _cap(hubA, 0, 1_000_000, 2_000_000); vm.expectRevert(bytes('PL_ADD_LT_DRAW')); this.configChangePlausibilityTest(_snapshot(caps)); } function test_distinctAssetGroupsAreIndependent() public { - // (HUB_A, 0): 1M add, 0 draw -> skipped. (HUB_A, 1): 1M add, 2M draw -> reverts. - Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](2); - caps[0] = _cap(HUB_A, 0, 1_000_000, 0); - caps[1] = _cap(HUB_A, 1, 1_000_000, 2_000_000); + // (hubA, 0): 1M add, 0 draw -> skipped. (hubA, 1): 1M add, 2M draw -> reverts. + Types.SpokeConfigSnapshot[] memory caps = new Types.SpokeConfigSnapshot[](2); + caps[0] = _cap(hubA, 0, 1_000_000, 0); + caps[1] = _cap(hubA, 1, 1_000_000, 2_000_000); vm.expectRevert(bytes('PL_ADD_LT_DRAW')); this.configChangePlausibilityTest(_snapshot(caps)); } function test_distinctHubsAreIndependent() public { // Same assetId on two hubs aggregate separately. - // HUB_A asset 0: 1M add, 500k draw -> passes. - // HUB_B asset 0: 500k add, 1M draw -> reverts. - Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](2); - caps[0] = _cap(HUB_A, 0, 1_000_000, 500_000); - caps[1] = _cap(HUB_B, 0, 500_000, 1_000_000); + // hubA asset 0: 1M add, 500k draw -> passes. + // hubB asset 0: 500k add, 1M draw -> reverts. + Types.SpokeConfigSnapshot[] memory caps = new Types.SpokeConfigSnapshot[](2); + caps[0] = _cap(hubA, 0, 1_000_000, 500_000); + caps[1] = _cap(hubB, 0, 500_000, 1_000_000); vm.expectRevert(bytes('PL_ADD_LT_DRAW')); this.configChangePlausibilityTest(_snapshot(caps)); } @@ -310,9 +310,9 @@ contract ProtocolV4TestPlausibility is Test, ProtocolV4TestBase { uint256 assetId, uint40 addCap, uint40 drawCap - ) internal pure returns (Types.SpokeCapSnapshot memory) { + ) internal pure returns (Types.SpokeConfigSnapshot memory) { return - Types.SpokeCapSnapshot({ + Types.SpokeConfigSnapshot({ hubAddress: hub, assetId: assetId, assetSymbol: 'X', @@ -326,8 +326,8 @@ contract ProtocolV4TestPlausibility is Test, ProtocolV4TestBase { } function _snapshot( - Types.SpokeCapSnapshot[] memory caps + Types.SpokeConfigSnapshot[] memory caps ) internal pure returns (Types.V4Snapshot memory snap) { - snap.spokeCaps = caps; + snap.spokeConfigs = caps; } } diff --git a/tests/dependencies/v4/V4DiffWriter.t.sol b/tests/dependencies/v4/V4DiffWriter.t.sol index bd5f39b7..e0fe2198 100644 --- a/tests/dependencies/v4/V4DiffWriter.t.sol +++ b/tests/dependencies/v4/V4DiffWriter.t.sol @@ -113,7 +113,7 @@ contract V4DiffWriterTest is V4DiffWriterTestBase, SnapshotV4 { assertTrue(vm.contains(json, '"spokeReserves"'), 'spokeReserves missing'); assertTrue(vm.contains(json, '"spokeLiquidationConfigs"'), 'spokeLiqConfigs missing'); assertTrue(vm.contains(json, '"hubAssets"'), 'hubAssets missing'); - assertTrue(vm.contains(json, '"spokeCaps"'), 'spokeCaps missing'); + assertTrue(vm.contains(json, '"spokeConfigs"'), 'spokeConfigs missing'); // Per-entity values assertTrue(vm.contains(json, '"symbol": "USDC"'), 'reserve symbol'); @@ -134,8 +134,8 @@ contract V4DiffWriterTest is V4DiffWriterTestBase, SnapshotV4 { afterSnap.spokeReserves[0].collateralFactor = 8000; afterSnap.spokeReserves[0].paused = true; afterSnap.hubAssets[0].liquidityFee = 50; - afterSnap.spokeCaps[0].addCap = 2_000_000; - afterSnap.spokeCaps[0].halted = true; + afterSnap.spokeConfigs[0].addCap = 2_000_000; + afterSnap.spokeConfigs[0].halted = true; afterSnap.spokeLiquidationConfigs[0].liquidationBonusFactor = 200; writeV4SnapshotJson(string.concat(REPORT, '_before'), before); @@ -184,7 +184,7 @@ contract V4DiffWriterTest is V4DiffWriterTestBase, SnapshotV4 { assertTrue(vm.contains(md, '0.50 % [50]'), 'liqFee after'); // Spoke cap section — uint40s rendered with thousand separators - assertTrue(vm.contains(md, '## Hub Spoke Cap Changes'), 'spoke cap section'); + assertTrue(vm.contains(md, '## Hub Spoke Config Changes'), 'spoke config section'); assertTrue(vm.contains(md, 'addCap'), 'addCap row'); assertTrue(vm.contains(md, '1,000,000'), 'addCap before'); assertTrue(vm.contains(md, '2,000,000'), 'addCap after'); @@ -257,8 +257,8 @@ contract V4DiffWriterTest is V4DiffWriterTestBase, SnapshotV4 { premiumOffsetRay: int200(0) }); - snap.spokeCaps = new Types.SpokeCapSnapshot[](1); - snap.spokeCaps[0] = Types.SpokeCapSnapshot({ + snap.spokeConfigs = new Types.SpokeConfigSnapshot[](1); + snap.spokeConfigs[0] = Types.SpokeConfigSnapshot({ hubAddress: hubX, assetId: 0, assetSymbol: 'USDC', @@ -400,9 +400,9 @@ contract V4DiffWriterHarnessTest is V4DiffWriterTestBase { vm.removeFile(path); } - function test_writeSpokeCaps_writesAllCaps() public { - Types.SpokeCapSnapshot[] memory caps = new Types.SpokeCapSnapshot[](2); - caps[0] = Types.SpokeCapSnapshot({ + function test_writeSpokeConfigs_writesAllConfigs() public { + Types.SpokeConfigSnapshot[] memory caps = new Types.SpokeConfigSnapshot[](2); + caps[0] = Types.SpokeConfigSnapshot({ hubAddress: hubX, assetId: 0, assetSymbol: 'USDC', @@ -413,7 +413,7 @@ contract V4DiffWriterHarnessTest is V4DiffWriterTestBase { active: true, halted: false }); - caps[1] = Types.SpokeCapSnapshot({ + caps[1] = Types.SpokeConfigSnapshot({ hubAddress: hubX, assetId: 0, assetSymbol: 'USDC', @@ -426,10 +426,10 @@ contract V4DiffWriterHarnessTest is V4DiffWriterTestBase { }); string memory path = './reports/harness_spoke_caps.json'; - harness.writeSpokeCaps(path, caps); + harness.writeSpokeConfigs(path, caps); string memory json = vm.readFile(path); - assertTrue(vm.contains(json, '"spokeCaps"'), 'section'); + assertTrue(vm.contains(json, '"spokeConfigs"'), 'section'); assertTrue(vm.contains(json, '"addCap": 1000000'), 'first addCap'); assertTrue(vm.contains(json, '"addCap": 2000000'), 'second addCap'); assertTrue(vm.contains(json, '"drawCap": 500000'), 'first drawCap'); diff --git a/tests/mocks/v4/V4DiffWriterHarness.sol b/tests/mocks/v4/V4DiffWriterHarness.sol index 963c2166..5a5d166d 100644 --- a/tests/mocks/v4/V4DiffWriterHarness.sol +++ b/tests/mocks/v4/V4DiffWriterHarness.sol @@ -37,7 +37,10 @@ contract V4DiffWriterHarness { return V4DiffWriter._serializeHubAsset(a); } - function writeSpokeCaps(string memory path, Types.SpokeCapSnapshot[] memory caps) external { - V4DiffWriter._writeSpokeCaps(path, caps); + function writeSpokeConfigs( + string memory path, + Types.SpokeConfigSnapshot[] memory configs + ) external { + V4DiffWriter._writeSpokeConfigs(path, configs); } } From 1346518e6a5d3c3d293c70e4e46e01fff827e347 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Mon, 11 May 2026 16:30:11 -0500 Subject: [PATCH 25/29] test: split out into separate files, more granular snapshot scenarios --- .../dependencies/v4/SnapshotV4.HubAsset.t.sol | 258 +++++++++++++++++ .../v4/SnapshotV4.LiqConfig.t.sol | 79 +++++ .../dependencies/v4/SnapshotV4.Reserve.t.sol | 170 +++++++++++ .../v4/SnapshotV4.SpokeConfig.t.sol | 94 ++++++ tests/dependencies/v4/SnapshotV4.t.sol | 32 --- tests/dependencies/v4/SnapshotV4Base.t.sol | 61 ++-- .../dependencies/v4/SnapshotV4Combined.t.sol | 272 ++++++++++++++++++ tests/mocks/v4/V4Mocks.sol | 28 +- 8 files changed, 925 insertions(+), 69 deletions(-) create mode 100644 tests/dependencies/v4/SnapshotV4.HubAsset.t.sol create mode 100644 tests/dependencies/v4/SnapshotV4.LiqConfig.t.sol create mode 100644 tests/dependencies/v4/SnapshotV4.Reserve.t.sol create mode 100644 tests/dependencies/v4/SnapshotV4.SpokeConfig.t.sol delete mode 100644 tests/dependencies/v4/SnapshotV4.t.sol create mode 100644 tests/dependencies/v4/SnapshotV4Combined.t.sol diff --git a/tests/dependencies/v4/SnapshotV4.HubAsset.t.sol b/tests/dependencies/v4/SnapshotV4.HubAsset.t.sol new file mode 100644 index 00000000..f07474cd --- /dev/null +++ b/tests/dependencies/v4/SnapshotV4.HubAsset.t.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'tests/dependencies/v4/SnapshotV4Base.t.sol'; + +contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { + // First hub asset: USDC, assetId 0. Used as the mutation target. + uint256 internal constant TARGET_IDX = 0; + + function test_createV4Snapshot_hubAssets() public view { + assertEq(_createV4Snapshot().hubAssets, _hubAssetFixtures); + } + + function test_delta_underlying_andSymbol() public { + CachedHubAssetFixture memory t = _targetHubAsset(); + IHub.Asset memory asset = _buildAssetFrom(t.input); + MockERC20Symbol newToken = new MockERC20Symbol('NEW_SYM'); + asset.underlying = address(newToken); + hub.setAsset(t.assetId, asset); + Types.V4Snapshot memory snap = _createV4Snapshot(); + assertEq(snap.hubAssets[TARGET_IDX].underlying, address(newToken), 'underlying'); + assertEq(snap.hubAssets[TARGET_IDX].symbol, 'NEW_SYM', 'symbol derives from underlying'); + } + + function test_delta_decimals() public { + CachedHubAssetFixture memory t = _targetHubAsset(); + IHub.Asset memory asset = _buildAssetFrom(t.input); + asset.decimals = 18; + hub.setAsset(t.assetId, asset); + assertEq( + uint256(_createV4Snapshot().hubAssets[TARGET_IDX].decimals), + uint256(asset.decimals), + 'decimals' + ); + } + + function test_delta_deficitRay() public { + CachedHubAssetFixture memory t = _targetHubAsset(); + IHub.Asset memory asset = _buildAssetFrom(t.input); + asset.deficitRay = 999_999; + hub.setAsset(t.assetId, asset); + assertEq( + uint256(_createV4Snapshot().hubAssets[TARGET_IDX].deficitRay), + uint256(asset.deficitRay), + 'deficitRay' + ); + } + + function test_delta_swept() public { + CachedHubAssetFixture memory t = _targetHubAsset(); + IHub.Asset memory asset = _buildAssetFrom(t.input); + asset.swept = 12_345; + hub.setAsset(t.assetId, asset); + assertEq( + uint256(_createV4Snapshot().hubAssets[TARGET_IDX].swept), + uint256(asset.swept), + 'swept' + ); + } + + function test_delta_premiumShares() public { + CachedHubAssetFixture memory t = _targetHubAsset(); + IHub.Asset memory asset = _buildAssetFrom(t.input); + asset.premiumShares = 55_555; + hub.setAsset(t.assetId, asset); + assertEq( + uint256(_createV4Snapshot().hubAssets[TARGET_IDX].premiumShares), + uint256(asset.premiumShares), + 'premiumShares' + ); + } + + function test_delta_premiumOffsetRay() public { + CachedHubAssetFixture memory t = _targetHubAsset(); + IHub.Asset memory asset = _buildAssetFrom(t.input); + asset.premiumOffsetRay = int200(-777); + hub.setAsset(t.assetId, asset); + assertEq( + _createV4Snapshot().hubAssets[TARGET_IDX].premiumOffsetRay, + asset.premiumOffsetRay, + 'premiumOffsetRay' + ); + } + + // --- AssetConfig fields --- + + function test_delta_liquidityFee() public { + CachedHubAssetFixture memory t = _targetHubAsset(); + IHub.AssetConfig memory cfg = _buildAssetConfigFrom(t.input); + cfg.liquidityFee = 9_999; + hub.setAssetConfig(t.assetId, cfg); + assertEq( + uint256(_createV4Snapshot().hubAssets[TARGET_IDX].liquidityFee), + uint256(cfg.liquidityFee), + 'liquidityFee' + ); + } + + function test_delta_irStrategy() public { + CachedHubAssetFixture memory t = _targetHubAsset(); + IHub.AssetConfig memory cfg = _buildAssetConfigFrom(t.input); + MockIR newIR = new MockIR(); + newIR.setData( + t.assetId, + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: 1234, + baseDrawnRate: 567, + rateGrowthBeforeOptimal: 89, + rateGrowthAfterOptimal: 10 + }), + 77_777 + ); + cfg.irStrategy = address(newIR); + hub.setAssetConfig(t.assetId, cfg); + Types.V4Snapshot memory snap = _createV4Snapshot(); + assertEq(snap.hubAssets[TARGET_IDX].irStrategy, address(newIR), 'irStrategy'); + // IR fields should now reflect the new strategy + assertEq(snap.hubAssets[TARGET_IDX].optimalUsageRatio, 1234, 'optimalUsageRatio reroute'); + assertEq(snap.hubAssets[TARGET_IDX].maxDrawnRate, 77_777, 'maxDrawnRate reroute'); + } + + function test_delta_feeReceiver() public { + CachedHubAssetFixture memory t = _targetHubAsset(); + IHub.AssetConfig memory cfg = _buildAssetConfigFrom(t.input); + address newReceiver = makeAddr('NEW_FEE_RECEIVER'); + cfg.feeReceiver = newReceiver; + hub.setAssetConfig(t.assetId, cfg); + assertEq(_createV4Snapshot().hubAssets[TARGET_IDX].feeReceiver, newReceiver, 'feeReceiver'); + } + + function test_delta_reinvestmentController() public { + CachedHubAssetFixture memory t = _targetHubAsset(); + IHub.AssetConfig memory cfg = _buildAssetConfigFrom(t.input); + address newReinv = makeAddr('NEW_REINV'); + cfg.reinvestmentController = newReinv; + hub.setAssetConfig(t.assetId, cfg); + assertEq( + _createV4Snapshot().hubAssets[TARGET_IDX].reinvestmentController, + newReinv, + 'reinvestmentController' + ); + } + + // --- IR-driven fields (mutate IR mock directly) --- + + function test_delta_optimalUsageRatio() public { + ir0.setData( + 0, + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: 1111, + baseDrawnRate: 100, + rateGrowthBeforeOptimal: 400, + rateGrowthAfterOptimal: 6000 + }), + 30_000 + ); + assertEq( + _createV4Snapshot().hubAssets[TARGET_IDX].optimalUsageRatio, + 1111, + 'optimalUsageRatio' + ); + } + + function test_delta_baseDrawnRate() public { + ir0.setData( + 0, + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: 8000, + baseDrawnRate: 222, + rateGrowthBeforeOptimal: 400, + rateGrowthAfterOptimal: 6000 + }), + 30_000 + ); + assertEq(_createV4Snapshot().hubAssets[TARGET_IDX].baseDrawnRate, 222, 'baseDrawnRate'); + } + + function test_delta_rateGrowthBeforeOptimal() public { + ir0.setData( + 0, + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: 8000, + baseDrawnRate: 100, + rateGrowthBeforeOptimal: 333, + rateGrowthAfterOptimal: 6000 + }), + 30_000 + ); + assertEq( + _createV4Snapshot().hubAssets[TARGET_IDX].rateGrowthBeforeOptimal, + 333, + 'rateGrowthBeforeOptimal' + ); + } + + function test_delta_rateGrowthAfterOptimal() public { + ir0.setData( + 0, + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: 8000, + baseDrawnRate: 100, + rateGrowthBeforeOptimal: 400, + rateGrowthAfterOptimal: 4444 + }), + 30_000 + ); + assertEq( + _createV4Snapshot().hubAssets[TARGET_IDX].rateGrowthAfterOptimal, + 4444, + 'rateGrowthAfterOptimal' + ); + } + + function test_delta_maxDrawnRate() public { + ir0.setData( + 0, + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: 8000, + baseDrawnRate: 100, + rateGrowthBeforeOptimal: 400, + rateGrowthAfterOptimal: 6000 + }), + 55_555 + ); + assertEq(_createV4Snapshot().hubAssets[TARGET_IDX].maxDrawnRate, 55_555, 'maxDrawnRate'); + } + + function _targetHubAsset() internal view returns (CachedHubAssetFixture memory) { + return _hubAssetFixtures[TARGET_IDX]; + } + + function _buildAssetFrom( + HubAssetFixture memory f + ) internal pure returns (IHub.Asset memory asset) { + asset.underlying = f.underlying; + asset.decimals = f.decimals; + asset.liquidityFee = f.liquidityFee; + asset.irStrategy = f.irStrategy; + asset.reinvestmentController = f.reinvController; + asset.feeReceiver = f.feeReceiver; + asset.deficitRay = f.deficitRay; + asset.swept = f.swept; + asset.premiumShares = f.premiumShares; + asset.premiumOffsetRay = f.premiumOffsetRay; + } + + function _buildAssetConfigFrom( + HubAssetFixture memory f + ) internal pure returns (IHub.AssetConfig memory) { + return + IHub.AssetConfig({ + feeReceiver: f.feeReceiver, + liquidityFee: f.liquidityFee, + irStrategy: f.irStrategy, + reinvestmentController: f.reinvController + }); + } +} diff --git a/tests/dependencies/v4/SnapshotV4.LiqConfig.t.sol b/tests/dependencies/v4/SnapshotV4.LiqConfig.t.sol new file mode 100644 index 00000000..90503908 --- /dev/null +++ b/tests/dependencies/v4/SnapshotV4.LiqConfig.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'tests/dependencies/v4/SnapshotV4Base.t.sol'; + +contract SnapshotV4LiqConfigTest is SnapshotV4BaseTest { + // First liq-config fixture: spokeA. Used as the mutation target. + uint256 internal constant TARGET_IDX = 0; + + function test_createV4Snapshot_liquidationConfigs() public view { + assertEq(_createV4Snapshot().spokeLiquidationConfigs, _liqConfigFixtures); + } + + function test_delta_targetHealthFactor() public { + LiqConfigFixture memory t = _targetLiqConfig(); + uint128 newVal = uint128(uint256(t.targetHealthFactor) * 2 + 1); + t.spoke.setLiquidationConfig( + ISpoke.LiquidationConfig({ + targetHealthFactor: newVal, + healthFactorForMaxBonus: t.healthFactorForMaxBonus, + liquidationBonusFactor: t.liquidationBonusFactor + }) + ); + assertEq( + _createV4Snapshot().spokeLiquidationConfigs[TARGET_IDX].targetHealthFactor, + uint256(newVal), + 'targetHealthFactor' + ); + } + + function test_delta_healthFactorForMaxBonus() public { + LiqConfigFixture memory t = _targetLiqConfig(); + uint64 newVal = uint64(uint256(t.healthFactorForMaxBonus) - 1); + t.spoke.setLiquidationConfig( + ISpoke.LiquidationConfig({ + targetHealthFactor: t.targetHealthFactor, + healthFactorForMaxBonus: newVal, + liquidationBonusFactor: t.liquidationBonusFactor + }) + ); + assertEq( + _createV4Snapshot().spokeLiquidationConfigs[TARGET_IDX].healthFactorForMaxBonus, + uint256(newVal), + 'healthFactorForMaxBonus' + ); + } + + function test_delta_liquidationBonusFactor() public { + LiqConfigFixture memory t = _targetLiqConfig(); + uint16 newVal = t.liquidationBonusFactor + 50; + t.spoke.setLiquidationConfig( + ISpoke.LiquidationConfig({ + targetHealthFactor: t.targetHealthFactor, + healthFactorForMaxBonus: t.healthFactorForMaxBonus, + liquidationBonusFactor: newVal + }) + ); + assertEq( + uint256(_createV4Snapshot().spokeLiquidationConfigs[TARGET_IDX].liquidationBonusFactor), + uint256(newVal), + 'liquidationBonusFactor' + ); + } + + function test_delta_maxUserReservesLimit() public { + LiqConfigFixture memory t = _targetLiqConfig(); + uint16 newVal = t.maxUserReservesLimit + 1; + t.spoke.setMaxUserReservesLimit(newVal); + assertEq( + uint256(_createV4Snapshot().spokeLiquidationConfigs[TARGET_IDX].maxUserReservesLimit), + uint256(newVal), + 'maxUserReservesLimit' + ); + } + + function _targetLiqConfig() internal view returns (LiqConfigFixture memory) { + return _liqConfigFixtures[TARGET_IDX]; + } +} diff --git a/tests/dependencies/v4/SnapshotV4.Reserve.t.sol b/tests/dependencies/v4/SnapshotV4.Reserve.t.sol new file mode 100644 index 00000000..b6e12c35 --- /dev/null +++ b/tests/dependencies/v4/SnapshotV4.Reserve.t.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'tests/dependencies/v4/SnapshotV4Base.t.sol'; + +contract SnapshotV4ReserveTest is SnapshotV4BaseTest { + // First reserve in the fixture set: USDC on spokeA, reserveId 0. Used as the + // mutation target for all per-field delta tests below. + uint256 internal constant TARGET_IDX = 0; + + /// @dev All reserve fixtures match the snapshot array. + function test_createV4Snapshot_spokeReserves() public view { + assertEq(_createV4Snapshot().spokeReserves, _reserveFixtures); + } + + // --- ReserveConfig fields --- + + /// @dev Flipping ReserveConfig.paused propagates to the reserve snapshot. + function test_delta_paused() public { + CachedReserveFixture memory t = _targetReserve(); + ISpoke.ReserveConfig memory cfg = _buildReserveConfigFrom(t.input); + cfg.paused = !cfg.paused; + t.input.spoke.setReserveConfig(t.reserveId, cfg); + assertEq(_createV4Snapshot().spokeReserves[TARGET_IDX].paused, cfg.paused, 'paused'); + } + + /// @dev Flipping ReserveConfig.frozen propagates. + function test_delta_frozen() public { + CachedReserveFixture memory t = _targetReserve(); + ISpoke.ReserveConfig memory cfg = _buildReserveConfigFrom(t.input); + cfg.frozen = !cfg.frozen; + t.input.spoke.setReserveConfig(t.reserveId, cfg); + assertEq(_createV4Snapshot().spokeReserves[TARGET_IDX].frozen, cfg.frozen, 'frozen'); + } + + /// @dev Flipping ReserveConfig.borrowable propagates. + function test_delta_borrowable() public { + CachedReserveFixture memory t = _targetReserve(); + ISpoke.ReserveConfig memory cfg = _buildReserveConfigFrom(t.input); + cfg.borrowable = !cfg.borrowable; + t.input.spoke.setReserveConfig(t.reserveId, cfg); + assertEq(_createV4Snapshot().spokeReserves[TARGET_IDX].borrowable, cfg.borrowable, 'borrowable'); + } + + /// @dev Flipping ReserveConfig.receiveSharesEnabled propagates. + function test_delta_receiveSharesEnabled() public { + CachedReserveFixture memory t = _targetReserve(); + ISpoke.ReserveConfig memory cfg = _buildReserveConfigFrom(t.input); + cfg.receiveSharesEnabled = !cfg.receiveSharesEnabled; + t.input.spoke.setReserveConfig(t.reserveId, cfg); + assertEq( + _createV4Snapshot().spokeReserves[TARGET_IDX].receiveSharesEnabled, + cfg.receiveSharesEnabled, + 'receiveSharesEnabled' + ); + } + + /// @dev Mutating ReserveConfig.collateralRisk propagates. + function test_delta_collateralRisk() public { + CachedReserveFixture memory t = _targetReserve(); + ISpoke.ReserveConfig memory cfg = _buildReserveConfigFrom(t.input); + cfg.collateralRisk = 9_999; + t.input.spoke.setReserveConfig(t.reserveId, cfg); + assertEq( + uint256(_createV4Snapshot().spokeReserves[TARGET_IDX].collateralRisk), + uint256(cfg.collateralRisk), + 'collateralRisk' + ); + } + + // --- DynamicReserveConfig fields --- + + /// @dev Mutating DynamicReserveConfig.collateralFactor propagates. + function test_delta_collateralFactor() public { + CachedReserveFixture memory t = _targetReserve(); + ISpoke.DynamicReserveConfig memory dyn = _buildDynamicReserveConfigFrom(t.input); + dyn.collateralFactor = 9_500; + t.input.spoke.setDynamicReserveConfig(t.reserveId, t.input.dynamicConfigKey, dyn); + assertEq( + uint256(_createV4Snapshot().spokeReserves[TARGET_IDX].collateralFactor), + uint256(dyn.collateralFactor), + 'collateralFactor' + ); + } + + /// @dev Mutating DynamicReserveConfig.maxLiquidationBonus propagates. + function test_delta_maxLiquidationBonus() public { + CachedReserveFixture memory t = _targetReserve(); + ISpoke.DynamicReserveConfig memory dyn = _buildDynamicReserveConfigFrom(t.input); + dyn.maxLiquidationBonus = 12_345; + t.input.spoke.setDynamicReserveConfig(t.reserveId, t.input.dynamicConfigKey, dyn); + assertEq( + uint256(_createV4Snapshot().spokeReserves[TARGET_IDX].maxLiquidationBonus), + uint256(dyn.maxLiquidationBonus), + 'maxLiquidationBonus' + ); + } + + /// @dev Mutating DynamicReserveConfig.liquidationFee propagates. + function test_delta_liquidationFee() public { + CachedReserveFixture memory t = _targetReserve(); + ISpoke.DynamicReserveConfig memory dyn = _buildDynamicReserveConfigFrom(t.input); + dyn.liquidationFee = 250; + t.input.spoke.setDynamicReserveConfig(t.reserveId, t.input.dynamicConfigKey, dyn); + assertEq( + uint256(_createV4Snapshot().spokeReserves[TARGET_IDX].liquidationFee), + uint256(dyn.liquidationFee), + 'liquidationFee' + ); + } + + // --- Oracle-driven fields --- + + /// @dev Updating the oracle's price for a reserve propagates. + function test_delta_oraclePrice() public { + CachedReserveFixture memory t = _targetReserve(); + uint256 newPrice = t.input.oraclePrice * 2 + 1; + t.input.oracle.setReserve(t.reserveId, t.input.priceSource, newPrice); + assertEq(_createV4Snapshot().spokeReserves[TARGET_IDX].oraclePrice, newPrice, 'oraclePrice'); + } + + /// @dev Updating the oracle's price source for a reserve propagates. + function test_delta_priceSource() public { + CachedReserveFixture memory t = _targetReserve(); + address newSource = makeAddr('NEW_PRICE_SOURCE'); + t.input.oracle.setReserve(t.reserveId, newSource, t.input.oraclePrice); + assertEq(_createV4Snapshot().spokeReserves[TARGET_IDX].priceSource, newSource, 'priceSource'); + } + + /// @dev Swapping the spoke's oracle reroutes `oracleAddress` for its reserves. + function test_delta_oracleAddress() public { + CachedReserveFixture memory t = _targetReserve(); + MockOracle newOracle = new MockOracle(); + newOracle.setReserve(t.reserveId, t.input.priceSource, t.input.oraclePrice); + t.input.spoke.setOracle(address(newOracle)); + assertEq( + _createV4Snapshot().spokeReserves[TARGET_IDX].oracleAddress, + address(newOracle), + 'oracleAddress' + ); + } + + function _targetReserve() internal view returns (CachedReserveFixture memory) { + return _reserveFixtures[TARGET_IDX]; + } + + function _buildReserveConfigFrom( + ReserveFixture memory f + ) internal pure returns (ISpoke.ReserveConfig memory) { + return + ISpoke.ReserveConfig({ + collateralRisk: f.collateralRisk, + paused: f.paused, + frozen: f.frozen, + borrowable: f.borrowable, + receiveSharesEnabled: f.receiveSharesEnabled + }); + } + + function _buildDynamicReserveConfigFrom( + ReserveFixture memory f + ) internal pure returns (ISpoke.DynamicReserveConfig memory) { + return + ISpoke.DynamicReserveConfig({ + collateralFactor: f.collateralFactor, + maxLiquidationBonus: f.maxLiquidationBonus, + liquidationFee: f.liquidationFee + }); + } +} diff --git a/tests/dependencies/v4/SnapshotV4.SpokeConfig.t.sol b/tests/dependencies/v4/SnapshotV4.SpokeConfig.t.sol new file mode 100644 index 00000000..0a03e6d3 --- /dev/null +++ b/tests/dependencies/v4/SnapshotV4.SpokeConfig.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'tests/dependencies/v4/SnapshotV4Base.t.sol'; + +contract SnapshotV4SpokeConfigTest is SnapshotV4BaseTest { + // First spoke-config fixture: assetId 0 / spokeA. Used as the mutation target. + uint256 internal constant TARGET_IDX = 0; + + function test_createV4Snapshot_spokeConfigs() public view { + assertEq(_createV4Snapshot().spokeConfigs, _spokeConfigFixtures); + } + + function test_delta_addCap() public { + SpokeConfigFixture memory t = _targetSpokeConfig(); + IHub.SpokeConfig memory cfg = _newConfigFrom(t); + cfg.addCap = t.addCap + 7_000; + hub.addSpokeConfig(t.assetId, address(t.spoke), cfg); + assertEq( + uint256(_createV4Snapshot().spokeConfigs[TARGET_IDX].addCap), + uint256(cfg.addCap), + 'addCap' + ); + } + + function test_delta_drawCap() public { + SpokeConfigFixture memory t = _targetSpokeConfig(); + IHub.SpokeConfig memory cfg = _newConfigFrom(t); + cfg.drawCap = t.drawCap + 3_000; + hub.addSpokeConfig(t.assetId, address(t.spoke), cfg); + assertEq( + uint256(_createV4Snapshot().spokeConfigs[TARGET_IDX].drawCap), + uint256(cfg.drawCap), + 'drawCap' + ); + } + + function test_delta_riskPremiumThreshold() public { + SpokeConfigFixture memory t = _targetSpokeConfig(); + IHub.SpokeConfig memory cfg = _newConfigFrom(t); + cfg.riskPremiumThreshold = 555; + hub.addSpokeConfig(t.assetId, address(t.spoke), cfg); + assertEq( + uint256(_createV4Snapshot().spokeConfigs[TARGET_IDX].riskPremiumThreshold), + uint256(cfg.riskPremiumThreshold), + 'riskPremiumThreshold' + ); + } + + function test_delta_active() public { + SpokeConfigFixture memory t = _targetSpokeConfig(); + IHub.SpokeConfig memory cfg = _newConfigFrom(t); + cfg.active = !cfg.active; + hub.addSpokeConfig(t.assetId, address(t.spoke), cfg); + assertEq(_createV4Snapshot().spokeConfigs[TARGET_IDX].active, cfg.active, 'active'); + } + + function test_delta_halted() public { + SpokeConfigFixture memory t = _targetSpokeConfig(); + IHub.SpokeConfig memory cfg = _newConfigFrom(t); + cfg.halted = !cfg.halted; + hub.addSpokeConfig(t.assetId, address(t.spoke), cfg); + assertEq(_createV4Snapshot().spokeConfigs[TARGET_IDX].halted, cfg.halted, 'halted'); + } + + // Re-calling addSpokeConfig for an existing (assetId, spoke) must NOT + // duplicate the spoke in the array. Protects the `_spokeRegistered` dedup fix. + function test_delta_reAdd_doesNotDuplicate() public { + Types.V4Snapshot memory snapA = _createV4Snapshot(); + SpokeConfigFixture memory t = _targetSpokeConfig(); + IHub.SpokeConfig memory cfg = _newConfigFrom(t); + cfg.halted = !cfg.halted; + hub.addSpokeConfig(t.assetId, address(t.spoke), cfg); + Types.V4Snapshot memory snapB = _createV4Snapshot(); + assertEq(snapA.spokeConfigs.length, snapB.spokeConfigs.length, 'no duplicate push'); + } + + function _targetSpokeConfig() internal view returns (SpokeConfigFixture memory) { + return _spokeConfigFixtures[TARGET_IDX]; + } + + function _newConfigFrom( + SpokeConfigFixture memory f + ) internal pure returns (IHub.SpokeConfig memory) { + return + IHub.SpokeConfig({ + addCap: f.addCap, + drawCap: f.drawCap, + riskPremiumThreshold: f.riskPremiumThreshold, + active: f.active, + halted: f.halted + }); + } +} diff --git a/tests/dependencies/v4/SnapshotV4.t.sol b/tests/dependencies/v4/SnapshotV4.t.sol deleted file mode 100644 index da430b5d..00000000 --- a/tests/dependencies/v4/SnapshotV4.t.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import 'tests/dependencies/v4/SnapshotV4Base.t.sol'; - -contract SnapshotV4Test is SnapshotV4BaseTest { - function test_createV4Snapshot_spokeReserves() public view { - assertEq(_createV4Snapshot().spokeReserves, _reserveFixtures); - } - - function test_createV4Snapshot_liquidationConfigs() public view { - assertEq(_createV4Snapshot().spokeLiquidationConfigs, _liqConfigFixtures); - } - - function test_createV4Snapshot_hubAssets() public view { - assertEq(_createV4Snapshot().hubAssets, _hubAssetFixtures); - } - - function test_createV4Snapshot_spokeCaps() public view { - assertEq(_createV4Snapshot().spokeCaps, _spokeCapFixtures); - } - - function test_createV4Snapshot_emptyInputs() public view { - ISpoke[] memory spokes = new ISpoke[](0); - IHub[] memory hubs = new IHub[](0); - Types.V4Snapshot memory snap = createV4Snapshot(spokes, hubs); - assertEq(snap.spokeReserves.length, 0, 'empty reserves'); - assertEq(snap.spokeLiquidationConfigs.length, 0, 'empty liq'); - assertEq(snap.hubAssets.length, 0, 'empty hubAssets'); - assertEq(snap.spokeCaps.length, 0, 'empty caps'); - } -} diff --git a/tests/dependencies/v4/SnapshotV4Base.t.sol b/tests/dependencies/v4/SnapshotV4Base.t.sol index b315d2aa..1822450f 100644 --- a/tests/dependencies/v4/SnapshotV4Base.t.sol +++ b/tests/dependencies/v4/SnapshotV4Base.t.sol @@ -35,9 +35,6 @@ abstract contract SnapshotV4BaseTest is Test, SnapshotV4 { uint16 reserveId; } - /// @notice Fixtures captured by `_addReserve` - CachedReserveFixture[] internal _reserveFixtures; - struct HubAssetFixture { address underlying; uint8 decimals; @@ -51,16 +48,11 @@ abstract contract SnapshotV4BaseTest is Test, SnapshotV4 { int200 premiumOffsetRay; } - /// @dev Stored alongside the input fixture so tests can match snapshots against - /// the `assetId` returned by `MockHub.addAsset`. struct CachedHubAssetFixture { HubAssetFixture input; uint256 assetId; } - /// @notice Fixtures captured by `_addHubAsset` - CachedHubAssetFixture[] internal _hubAssetFixtures; - struct LiqConfigFixture { MockSpoke spoke; uint128 targetHealthFactor; @@ -69,11 +61,7 @@ abstract contract SnapshotV4BaseTest is Test, SnapshotV4 { uint16 maxUserReservesLimit; } - /// @notice Fixtures captured by `_addLiqConfig`. The spoke's `setLiquidationConfig` - /// and `setMaxUserReservesLimit` calls produce no return value, so no wrapper struct. - LiqConfigFixture[] internal _liqConfigFixtures; - - struct SpokeCapFixture { + struct SpokeConfigFixture { uint256 assetId; MockSpoke spoke; uint40 addCap; @@ -83,9 +71,10 @@ abstract contract SnapshotV4BaseTest is Test, SnapshotV4 { bool halted; } - /// @notice Fixtures captured by `_addSpokeCap`. `MockHub.addSpokeConfig` returns nothing - /// and the snapshot key (hub, assetId, spoke) is already in the input, so no wrapper struct. - SpokeCapFixture[] internal _spokeCapFixtures; + SpokeConfigFixture[] internal _spokeConfigFixtures; + CachedHubAssetFixture[] internal _hubAssetFixtures; + LiqConfigFixture[] internal _liqConfigFixtures; + CachedReserveFixture[] internal _reserveFixtures; MockSpoke internal spokeA; MockSpoke internal spokeB; @@ -108,14 +97,14 @@ abstract contract SnapshotV4BaseTest is Test, SnapshotV4 { address internal reinvA = makeAddr('REINVEST_A'); address internal reinvB = makeAddr('REINVEST_B'); - function setUp() public { + function setUp() public virtual { _deployMocks(); _setSpokeOracles(); _addLiqConfigFixtures(); _addReserveFixtures(); _addHubAssetFixtures(); _configureIRStrategies(); - _addSpokeCapFixtures(); + _addSpokeConfigFixtures(); } function _setSpokeOracles() internal { @@ -271,9 +260,9 @@ abstract contract SnapshotV4BaseTest is Test, SnapshotV4 { ); } - function _addSpokeCapFixtures() internal { - _addSpokeCap( - SpokeCapFixture({ + function _addSpokeConfigFixtures() internal { + _addSpokeConfig( + SpokeConfigFixture({ assetId: 0, spoke: spokeA, addCap: 1_000_000, @@ -283,8 +272,8 @@ abstract contract SnapshotV4BaseTest is Test, SnapshotV4 { halted: false }) ); - _addSpokeCap( - SpokeCapFixture({ + _addSpokeConfig( + SpokeConfigFixture({ assetId: 0, spoke: spokeB, addCap: 2_000_000, @@ -294,8 +283,8 @@ abstract contract SnapshotV4BaseTest is Test, SnapshotV4 { halted: true }) ); - _addSpokeCap( - SpokeCapFixture({ + _addSpokeConfig( + SpokeConfigFixture({ assetId: 1, spoke: spokeA, addCap: 3_000_000, @@ -305,8 +294,8 @@ abstract contract SnapshotV4BaseTest is Test, SnapshotV4 { halted: false }) ); - _addSpokeCap( - SpokeCapFixture({ + _addSpokeConfig( + SpokeConfigFixture({ assetId: 2, spoke: spokeB, addCap: 4_000_000, @@ -318,7 +307,7 @@ abstract contract SnapshotV4BaseTest is Test, SnapshotV4 { ); } - function _addSpokeCap(SpokeCapFixture memory f) internal { + function _addSpokeConfig(SpokeConfigFixture memory f) internal { hub.addSpokeConfig( f.assetId, address(f.spoke), @@ -330,18 +319,18 @@ abstract contract SnapshotV4BaseTest is Test, SnapshotV4 { halted: f.halted }) ); - _spokeCapFixtures.push(f); + _spokeConfigFixtures.push(f); } - /// @notice Assert a `SpokeCapSnapshot` matches the stored fixture inputs. + /// @notice Assert a `SpokeConfigSnapshot` matches the stored fixture inputs. /// `assetSymbol` is resolved from the asset's underlying token, mirroring /// `SnapshotV4._snapshotCapsForHub`. function assertEq( - Types.SpokeCapSnapshot memory snap, - SpokeCapFixture memory expected, + Types.SpokeConfigSnapshot memory snap, + SpokeConfigFixture memory expected, uint256 idx ) internal view { - string memory pfx = string.concat('spokeCap[', vm.toString(idx), '] '); + string memory pfx = string.concat('spokeConfig[', vm.toString(idx), '] '); assertEq(snap.hubAddress, address(hub), string.concat(pfx, 'hub')); assertEq(snap.assetId, expected.assetId, string.concat(pfx, 'assetId')); assertEq(snap.spokeAddress, address(expected.spoke), string.concat(pfx, 'spoke')); @@ -670,10 +659,10 @@ abstract contract SnapshotV4BaseTest is Test, SnapshotV4 { } function assertEq( - Types.SpokeCapSnapshot[] memory snaps, - SpokeCapFixture[] memory expected + Types.SpokeConfigSnapshot[] memory snaps, + SpokeConfigFixture[] memory expected ) internal view { - assertEq(snaps.length, expected.length, 'spokeCaps length'); + assertEq(snaps.length, expected.length, 'spokeConfigs length'); for (uint256 i; i < expected.length; i++) { assertEq(snaps[i], expected[i], i); } diff --git a/tests/dependencies/v4/SnapshotV4Combined.t.sol b/tests/dependencies/v4/SnapshotV4Combined.t.sol new file mode 100644 index 00000000..9c37cbc7 --- /dev/null +++ b/tests/dependencies/v4/SnapshotV4Combined.t.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'tests/dependencies/v4/SnapshotV4Base.t.sol'; + +contract SnapshotV4CombinedTest is SnapshotV4BaseTest { + function setUp() public override { + _deployMocks(); + _setSpokeOracles(); + } + + function _addAllFixtures() internal { + _addLiqConfigFixtures(); + _addReserveFixtures(); + _addHubAssetFixtures(); + _configureIRStrategies(); + _addSpokeConfigFixtures(); + } + + // ------------------------------------------------------------------------- + // Empty inputs + // ------------------------------------------------------------------------- + + function test_createV4Snapshot_emptyInputs() public view { + ISpoke[] memory spokes = new ISpoke[](0); + IHub[] memory hubs = new IHub[](0); + Types.V4Snapshot memory snap = createV4Snapshot(spokes, hubs); + assertEq(snap.spokeReserves.length, 0, 'empty reserves'); + assertEq(snap.spokeLiquidationConfigs.length, 0, 'empty liq'); + assertEq(snap.hubAssets.length, 0, 'empty hubAssets'); + assertEq(snap.spokeConfigs.length, 0, 'empty caps'); + } + + // ------------------------------------------------------------------------- + // Partial-config scenarios — verify each section is independently captured. + // Note: `spokeLiquidationConfigs.length` is driven by the spoke count (always 2 + // here), not by `_addLiqConfigFixtures()`. Without populating liq configs, the + // mock returns a zero-default struct per spoke. + // ------------------------------------------------------------------------- + + function test_partial_reservesOnly() public { + _addReserveFixtures(); + Types.V4Snapshot memory snap = _createV4Snapshot(); + assertEq(snap.spokeReserves, _reserveFixtures); + assertEq(snap.hubAssets.length, 0, 'hub assets should be empty'); + assertEq(snap.spokeConfigs.length, 0, 'spoke caps should be empty'); + } + + function test_partial_hubAssetsOnly() public { + _addHubAssetFixtures(); + _configureIRStrategies(); + Types.V4Snapshot memory snap = _createV4Snapshot(); + assertEq(snap.hubAssets, _hubAssetFixtures); + assertEq(snap.spokeReserves.length, 0, 'reserves should be empty'); + assertEq(snap.spokeConfigs.length, 0, 'spoke caps should be empty'); + } + + function test_partial_hubAssetsNoSpokeConfigs() public { + // Assets present on hub, but no spoke configs registered → spokeConfigs stays empty. + _addHubAssetFixtures(); + _configureIRStrategies(); + Types.V4Snapshot memory snap = _createV4Snapshot(); + assertEq(snap.hubAssets.length, _hubAssetFixtures.length, 'hub assets populated'); + assertEq(snap.spokeConfigs.length, 0, 'no spoke configs registered'); + } + + function test_partial_liqConfigsOnly() public { + _addLiqConfigFixtures(); + Types.V4Snapshot memory snap = _createV4Snapshot(); + assertEq(snap.spokeLiquidationConfigs, _liqConfigFixtures); + assertEq(snap.spokeReserves.length, 0, 'reserves should be empty'); + assertEq(snap.hubAssets.length, 0, 'hub assets should be empty'); + assertEq(snap.spokeConfigs.length, 0, 'spoke caps should be empty'); + } + + function test_partial_liqConfigsSpokeDriven() public view { + // Without `_addLiqConfigFixtures`, MockSpoke returns its zero-default config — + // so the snapshot still emits one entry per spoke. + Types.V4Snapshot memory snap = _createV4Snapshot(); + assertEq(snap.spokeLiquidationConfigs.length, 2, '2 spokes -> 2 liq configs'); + assertEq( + snap.spokeLiquidationConfigs[0].targetHealthFactor, + 0, + 'default targetHealthFactor is zero' + ); + assertEq( + snap.spokeLiquidationConfigs[0].liquidationBonusFactor, + 0, + 'default liquidationBonusFactor is zero' + ); + } + + function test_delta_oraclePrice() public { + _addAllFixtures(); + + Types.V4Snapshot memory snapA = _createV4Snapshot(); + + // Mutate oracle price for the first reserve (USDC on spokeA, reserveId 0). + CachedReserveFixture memory target = _reserveFixtures[0]; + uint256 newPrice = target.input.oraclePrice * 2 + 1; + target.input.oracle.setReserve(target.reserveId, target.input.priceSource, newPrice); + + Types.V4Snapshot memory snapB = _createV4Snapshot(); + + // Length and structure unchanged + assertEq(snapA.spokeReserves.length, snapB.spokeReserves.length, 'reserves length moved'); + // Target field moved + assertEq(snapB.spokeReserves[0].oraclePrice, newPrice, 'price did not update'); + assertTrue( + snapA.spokeReserves[0].oraclePrice != snapB.spokeReserves[0].oraclePrice, + 'price unchanged' + ); + // Other reserves' prices unchanged + for (uint256 i = 1; i < snapB.spokeReserves.length; i++) { + assertEq( + snapA.spokeReserves[i].oraclePrice, + snapB.spokeReserves[i].oraclePrice, + 'unrelated price moved' + ); + } + // Sibling fields on the mutated reserve unchanged + assertEq( + snapA.spokeReserves[0].underlying, + snapB.spokeReserves[0].underlying, + 'underlying moved' + ); + assertEq(snapA.spokeReserves[0].decimals, snapB.spokeReserves[0].decimals, 'decimals moved'); + } + + function test_delta_spokeConfigFlag() public { + _addAllFixtures(); + + Types.V4Snapshot memory snapA = _createV4Snapshot(); + + // Flip `halted` on the first spoke-config fixture; re-call addSpokeConfig overwrites + // in place (this is what the MockHub dedup fix protects). + SpokeConfigFixture memory orig = _spokeConfigFixtures[0]; + hub.addSpokeConfig( + orig.assetId, + address(orig.spoke), + IHub.SpokeConfig({ + addCap: orig.addCap, + drawCap: orig.drawCap, + riskPremiumThreshold: orig.riskPremiumThreshold, + active: orig.active, + halted: !orig.halted + }) + ); + + Types.V4Snapshot memory snapB = _createV4Snapshot(); + + // Critical: length unchanged — proves no duplicate spoke push. + assertEq(snapA.spokeConfigs.length, snapB.spokeConfigs.length, 'spokeConfigs length moved'); + // Target moved + assertEq(snapB.spokeConfigs[0].halted, !orig.halted, 'halted did not flip'); + assertTrue(snapA.spokeConfigs[0].halted != snapB.spokeConfigs[0].halted, 'halted unchanged'); + // Sibling fields on the same entry unchanged + assertEq(snapA.spokeConfigs[0].addCap, snapB.spokeConfigs[0].addCap, 'addCap moved'); + assertEq(snapA.spokeConfigs[0].drawCap, snapB.spokeConfigs[0].drawCap, 'drawCap moved'); + assertEq(snapA.spokeConfigs[0].active, snapB.spokeConfigs[0].active, 'active moved'); + } + + function test_delta_irData() public { + _addAllFixtures(); + + Types.V4Snapshot memory snapA = _createV4Snapshot(); + + // Bump maxDrawnRate on ir0 (which serves hub asset 0 — USDC). + uint256 newMaxDrawnRate = 99_999; + ir0.setData( + 0, + IAssetInterestRateStrategy.InterestRateData({ + optimalUsageRatio: 8000, + baseDrawnRate: 100, + rateGrowthBeforeOptimal: 400, + rateGrowthAfterOptimal: 6000 + }), + newMaxDrawnRate + ); + + Types.V4Snapshot memory snapB = _createV4Snapshot(); + + assertEq(snapA.hubAssets.length, snapB.hubAssets.length, 'hubAssets length moved'); + assertEq(snapB.hubAssets[0].maxDrawnRate, newMaxDrawnRate, 'maxDrawnRate did not update'); + assertTrue( + snapA.hubAssets[0].maxDrawnRate != snapB.hubAssets[0].maxDrawnRate, + 'maxDrawnRate unchanged' + ); + // Other IR-bound asset (WETH on ir1) unchanged + assertEq( + snapA.hubAssets[1].maxDrawnRate, + snapB.hubAssets[1].maxDrawnRate, + 'unrelated IR moved' + ); + // Non-IR fields on the mutated asset unchanged + assertEq(snapA.hubAssets[0].underlying, snapB.hubAssets[0].underlying, 'underlying moved'); + assertEq( + snapA.hubAssets[0].liquidityFee, + snapB.hubAssets[0].liquidityFee, + 'liquidityFee moved' + ); + } + + function test_multiHub_aggregates() public { + // Populate the canonical hub fully. + _addAllFixtures(); + + // Deploy a second hub and add one asset + one spoke config to it. + MockHub hub2 = new MockHub(); + { + IHub.Asset memory asset; + asset.underlying = address(usdc); + asset.decimals = 6; + asset.liquidityFee = 7; + asset.irStrategy = address(0); + asset.reinvestmentController = address(0); + asset.feeReceiver = feeReceiverA; + IHub.AssetConfig memory config = IHub.AssetConfig({ + feeReceiver: feeReceiverA, + liquidityFee: 7, + irStrategy: address(0), + reinvestmentController: address(0) + }); + hub2.addAsset(asset, config); + } + hub2.addSpokeConfig( + 0, + address(spokeA), + IHub.SpokeConfig({ + addCap: 7_000_000, + drawCap: 6_000_000, + riskPremiumThreshold: 700, + active: true, + halted: false + }) + ); + + // Snapshot with both hubs in order [hub, hub2]. + ISpoke[] memory spokes = new ISpoke[](2); + spokes[0] = ISpoke(address(spokeA)); + spokes[1] = ISpoke(address(spokeB)); + IHub[] memory hubs = new IHub[](2); + hubs[0] = IHub(address(hub)); + hubs[1] = IHub(address(hub2)); + Types.V4Snapshot memory snap = createV4Snapshot(spokes, hubs); + + // hub has 3 assets + 4 spoke configs; hub2 contributes 1 + 1. + assertEq(snap.hubAssets.length, _hubAssetFixtures.length + 1, 'hubAssets aggregated'); + assertEq(snap.spokeConfigs.length, _spokeConfigFixtures.length + 1, 'spokeConfigs aggregated'); + + // Order: hub1 entries first, then hub2. + assertEq(snap.hubAssets[0].hubAddress, address(hub), 'first asset from hub1'); + assertEq( + snap.hubAssets[snap.hubAssets.length - 1].hubAddress, + address(hub2), + 'last asset from hub2' + ); + assertEq(snap.spokeConfigs[0].hubAddress, address(hub), 'first cap from hub1'); + assertEq( + snap.spokeConfigs[snap.spokeConfigs.length - 1].hubAddress, + address(hub2), + 'last cap from hub2' + ); + + // hub2's spoke config values flow through correctly. + Types.SpokeConfigSnapshot memory hub2Cap = snap.spokeConfigs[snap.spokeConfigs.length - 1]; + assertEq(hub2Cap.assetId, 0, 'hub2 cap assetId'); + assertEq(hub2Cap.spokeAddress, address(spokeA), 'hub2 cap spoke'); + assertEq(uint256(hub2Cap.addCap), 7_000_000, 'hub2 addCap'); + assertEq(uint256(hub2Cap.drawCap), 6_000_000, 'hub2 drawCap'); + } +} diff --git a/tests/mocks/v4/V4Mocks.sol b/tests/mocks/v4/V4Mocks.sol index e840fa07..5e52d6c5 100644 --- a/tests/mocks/v4/V4Mocks.sol +++ b/tests/mocks/v4/V4Mocks.sol @@ -92,6 +92,19 @@ contract MockSpoke { _dynConfigs[reserveId][reserve.dynamicConfigKey] = dyn; } + // Per-field mutators for already-added reserves, used by snapshot delta tests. + function setReserveConfig(uint256 reserveId, ISpoke.ReserveConfig memory config) external { + _reserveConfigs[reserveId] = config; + } + + function setDynamicReserveConfig( + uint256 reserveId, + uint32 dynamicConfigKey, + ISpoke.DynamicReserveConfig memory dyn + ) external { + _dynConfigs[reserveId][dynamicConfigKey] = dyn; + } + function getReserveCount() external view returns (uint256) { return _reserves.length; } @@ -122,6 +135,7 @@ contract MockHub { mapping(uint256 => IHub.AssetConfig) private _assetConfigs; mapping(uint256 => address[]) private _spokesByAsset; mapping(uint256 => mapping(address => IHub.SpokeConfig)) private _spokeConfigs; + mapping(uint256 => mapping(address => bool)) private _spokeRegistered; function addAsset( IHub.Asset memory asset, @@ -132,8 +146,20 @@ contract MockHub { _assetConfigs[assetId] = config; } + // Per-asset mutators for already-added assets, used by snapshot delta tests. + function setAsset(uint256 assetId, IHub.Asset memory asset) external { + _assets[assetId] = asset; + } + + function setAssetConfig(uint256 assetId, IHub.AssetConfig memory config) external { + _assetConfigs[assetId] = config; + } + function addSpokeConfig(uint256 assetId, address spoke, IHub.SpokeConfig memory config) external { - _spokesByAsset[assetId].push(spoke); + if (!_spokeRegistered[assetId][spoke]) { + _spokesByAsset[assetId].push(spoke); + _spokeRegistered[assetId][spoke] = true; + } _spokeConfigs[assetId][spoke] = config; } From 616312623265a338f73a4146274db3bb60dc4c6e Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Mon, 11 May 2026 16:31:29 -0500 Subject: [PATCH 26/29] test: snapshot scenarios comments --- .../dependencies/v4/SnapshotV4.HubAsset.t.sol | 17 +++++- .../v4/SnapshotV4.LiqConfig.t.sol | 5 ++ .../v4/SnapshotV4.SpokeConfig.t.sol | 9 ++- .../dependencies/v4/SnapshotV4Combined.t.sol | 61 ++++++------------- 4 files changed, 48 insertions(+), 44 deletions(-) diff --git a/tests/dependencies/v4/SnapshotV4.HubAsset.t.sol b/tests/dependencies/v4/SnapshotV4.HubAsset.t.sol index f07474cd..883c15e2 100644 --- a/tests/dependencies/v4/SnapshotV4.HubAsset.t.sol +++ b/tests/dependencies/v4/SnapshotV4.HubAsset.t.sol @@ -7,10 +7,12 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { // First hub asset: USDC, assetId 0. Used as the mutation target. uint256 internal constant TARGET_IDX = 0; + /// @dev All hub asset fixtures match the snapshot array. function test_createV4Snapshot_hubAssets() public view { assertEq(_createV4Snapshot().hubAssets, _hubAssetFixtures); } + /// @dev Swapping the underlying token updates both underlying and the derived symbol. Cannot happen in practice. function test_delta_underlying_andSymbol() public { CachedHubAssetFixture memory t = _targetHubAsset(); IHub.Asset memory asset = _buildAssetFrom(t.input); @@ -22,6 +24,7 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { assertEq(snap.hubAssets[TARGET_IDX].symbol, 'NEW_SYM', 'symbol derives from underlying'); } + /// @dev Mutating Asset.decimals propagates. Cannot happen in practice. function test_delta_decimals() public { CachedHubAssetFixture memory t = _targetHubAsset(); IHub.Asset memory asset = _buildAssetFrom(t.input); @@ -34,6 +37,7 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { ); } + /// @dev Mutating Asset.deficitRay propagates. function test_delta_deficitRay() public { CachedHubAssetFixture memory t = _targetHubAsset(); IHub.Asset memory asset = _buildAssetFrom(t.input); @@ -46,6 +50,7 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { ); } + /// @dev Mutating Asset.swept propagates. function test_delta_swept() public { CachedHubAssetFixture memory t = _targetHubAsset(); IHub.Asset memory asset = _buildAssetFrom(t.input); @@ -58,6 +63,7 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { ); } + /// @dev Mutating Asset.premiumShares propagates. function test_delta_premiumShares() public { CachedHubAssetFixture memory t = _targetHubAsset(); IHub.Asset memory asset = _buildAssetFrom(t.input); @@ -70,6 +76,7 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { ); } + /// @dev Mutating Asset.premiumOffsetRay (signed) propagates. function test_delta_premiumOffsetRay() public { CachedHubAssetFixture memory t = _targetHubAsset(); IHub.Asset memory asset = _buildAssetFrom(t.input); @@ -84,6 +91,7 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { // --- AssetConfig fields --- + /// @dev Mutating AssetConfig.liquidityFee propagates. function test_delta_liquidityFee() public { CachedHubAssetFixture memory t = _targetHubAsset(); IHub.AssetConfig memory cfg = _buildAssetConfigFrom(t.input); @@ -96,6 +104,7 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { ); } + /// @dev Swapping the IR strategy reroutes IR-derived snapshot fields. function test_delta_irStrategy() public { CachedHubAssetFixture memory t = _targetHubAsset(); IHub.AssetConfig memory cfg = _buildAssetConfigFrom(t.input); @@ -114,11 +123,11 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { hub.setAssetConfig(t.assetId, cfg); Types.V4Snapshot memory snap = _createV4Snapshot(); assertEq(snap.hubAssets[TARGET_IDX].irStrategy, address(newIR), 'irStrategy'); - // IR fields should now reflect the new strategy assertEq(snap.hubAssets[TARGET_IDX].optimalUsageRatio, 1234, 'optimalUsageRatio reroute'); assertEq(snap.hubAssets[TARGET_IDX].maxDrawnRate, 77_777, 'maxDrawnRate reroute'); } + /// @dev Mutating AssetConfig.feeReceiver propagates. function test_delta_feeReceiver() public { CachedHubAssetFixture memory t = _targetHubAsset(); IHub.AssetConfig memory cfg = _buildAssetConfigFrom(t.input); @@ -128,6 +137,7 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { assertEq(_createV4Snapshot().hubAssets[TARGET_IDX].feeReceiver, newReceiver, 'feeReceiver'); } + /// @dev Mutating AssetConfig.reinvestmentController propagates. function test_delta_reinvestmentController() public { CachedHubAssetFixture memory t = _targetHubAsset(); IHub.AssetConfig memory cfg = _buildAssetConfigFrom(t.input); @@ -143,6 +153,7 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { // --- IR-driven fields (mutate IR mock directly) --- + /// @dev Mutating IR data optimalUsageRatio propagates. function test_delta_optimalUsageRatio() public { ir0.setData( 0, @@ -161,6 +172,7 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { ); } + /// @dev Mutating IR data baseDrawnRate propagates. function test_delta_baseDrawnRate() public { ir0.setData( 0, @@ -175,6 +187,7 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { assertEq(_createV4Snapshot().hubAssets[TARGET_IDX].baseDrawnRate, 222, 'baseDrawnRate'); } + /// @dev Mutating IR data rateGrowthBeforeOptimal propagates. function test_delta_rateGrowthBeforeOptimal() public { ir0.setData( 0, @@ -193,6 +206,7 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { ); } + /// @dev Mutating IR data rateGrowthAfterOptimal propagates. function test_delta_rateGrowthAfterOptimal() public { ir0.setData( 0, @@ -211,6 +225,7 @@ contract SnapshotV4HubAssetTest is SnapshotV4BaseTest { ); } + /// @dev Mutating IR data maxDrawnRate propagates. function test_delta_maxDrawnRate() public { ir0.setData( 0, diff --git a/tests/dependencies/v4/SnapshotV4.LiqConfig.t.sol b/tests/dependencies/v4/SnapshotV4.LiqConfig.t.sol index 90503908..b83a7abe 100644 --- a/tests/dependencies/v4/SnapshotV4.LiqConfig.t.sol +++ b/tests/dependencies/v4/SnapshotV4.LiqConfig.t.sol @@ -7,10 +7,12 @@ contract SnapshotV4LiqConfigTest is SnapshotV4BaseTest { // First liq-config fixture: spokeA. Used as the mutation target. uint256 internal constant TARGET_IDX = 0; + /// @dev All liq config fixtures match the snapshot array. function test_createV4Snapshot_liquidationConfigs() public view { assertEq(_createV4Snapshot().spokeLiquidationConfigs, _liqConfigFixtures); } + /// @dev Mutating targetHealthFactor propagates. function test_delta_targetHealthFactor() public { LiqConfigFixture memory t = _targetLiqConfig(); uint128 newVal = uint128(uint256(t.targetHealthFactor) * 2 + 1); @@ -28,6 +30,7 @@ contract SnapshotV4LiqConfigTest is SnapshotV4BaseTest { ); } + /// @dev Mutating healthFactorForMaxBonus propagates. function test_delta_healthFactorForMaxBonus() public { LiqConfigFixture memory t = _targetLiqConfig(); uint64 newVal = uint64(uint256(t.healthFactorForMaxBonus) - 1); @@ -45,6 +48,7 @@ contract SnapshotV4LiqConfigTest is SnapshotV4BaseTest { ); } + /// @dev Mutating liquidationBonusFactor propagates. function test_delta_liquidationBonusFactor() public { LiqConfigFixture memory t = _targetLiqConfig(); uint16 newVal = t.liquidationBonusFactor + 50; @@ -62,6 +66,7 @@ contract SnapshotV4LiqConfigTest is SnapshotV4BaseTest { ); } + /// @dev Mutating maxUserReservesLimit propagates. function test_delta_maxUserReservesLimit() public { LiqConfigFixture memory t = _targetLiqConfig(); uint16 newVal = t.maxUserReservesLimit + 1; diff --git a/tests/dependencies/v4/SnapshotV4.SpokeConfig.t.sol b/tests/dependencies/v4/SnapshotV4.SpokeConfig.t.sol index 0a03e6d3..20ae3dba 100644 --- a/tests/dependencies/v4/SnapshotV4.SpokeConfig.t.sol +++ b/tests/dependencies/v4/SnapshotV4.SpokeConfig.t.sol @@ -7,10 +7,12 @@ contract SnapshotV4SpokeConfigTest is SnapshotV4BaseTest { // First spoke-config fixture: assetId 0 / spokeA. Used as the mutation target. uint256 internal constant TARGET_IDX = 0; + /// @dev All spoke config fixtures match the snapshot array. function test_createV4Snapshot_spokeConfigs() public view { assertEq(_createV4Snapshot().spokeConfigs, _spokeConfigFixtures); } + /// @dev Updating SpokeConfig.addCap propagates. function test_delta_addCap() public { SpokeConfigFixture memory t = _targetSpokeConfig(); IHub.SpokeConfig memory cfg = _newConfigFrom(t); @@ -23,6 +25,7 @@ contract SnapshotV4SpokeConfigTest is SnapshotV4BaseTest { ); } + /// @dev Updating SpokeConfig.drawCap propagates. function test_delta_drawCap() public { SpokeConfigFixture memory t = _targetSpokeConfig(); IHub.SpokeConfig memory cfg = _newConfigFrom(t); @@ -35,6 +38,7 @@ contract SnapshotV4SpokeConfigTest is SnapshotV4BaseTest { ); } + /// @dev Updating SpokeConfig.riskPremiumThreshold propagates. function test_delta_riskPremiumThreshold() public { SpokeConfigFixture memory t = _targetSpokeConfig(); IHub.SpokeConfig memory cfg = _newConfigFrom(t); @@ -47,6 +51,7 @@ contract SnapshotV4SpokeConfigTest is SnapshotV4BaseTest { ); } + /// @dev Flipping SpokeConfig.active propagates. function test_delta_active() public { SpokeConfigFixture memory t = _targetSpokeConfig(); IHub.SpokeConfig memory cfg = _newConfigFrom(t); @@ -55,6 +60,7 @@ contract SnapshotV4SpokeConfigTest is SnapshotV4BaseTest { assertEq(_createV4Snapshot().spokeConfigs[TARGET_IDX].active, cfg.active, 'active'); } + /// @dev Flipping SpokeConfig.halted propagates. function test_delta_halted() public { SpokeConfigFixture memory t = _targetSpokeConfig(); IHub.SpokeConfig memory cfg = _newConfigFrom(t); @@ -63,8 +69,7 @@ contract SnapshotV4SpokeConfigTest is SnapshotV4BaseTest { assertEq(_createV4Snapshot().spokeConfigs[TARGET_IDX].halted, cfg.halted, 'halted'); } - // Re-calling addSpokeConfig for an existing (assetId, spoke) must NOT - // duplicate the spoke in the array. Protects the `_spokeRegistered` dedup fix. + /// @dev Re-calling addSpokeConfig for an existing (assetId, spoke) doesn't duplicate the entry. function test_delta_reAdd_doesNotDuplicate() public { Types.V4Snapshot memory snapA = _createV4Snapshot(); SpokeConfigFixture memory t = _targetSpokeConfig(); diff --git a/tests/dependencies/v4/SnapshotV4Combined.t.sol b/tests/dependencies/v4/SnapshotV4Combined.t.sol index 9c37cbc7..2e02dc57 100644 --- a/tests/dependencies/v4/SnapshotV4Combined.t.sol +++ b/tests/dependencies/v4/SnapshotV4Combined.t.sol @@ -4,23 +4,14 @@ pragma solidity ^0.8.0; import 'tests/dependencies/v4/SnapshotV4Base.t.sol'; contract SnapshotV4CombinedTest is SnapshotV4BaseTest { + // Override the base setUp: deploy mocks only and let each test selectively + // call `_addFixtures()` for partial-config and delta scenarios. function setUp() public override { _deployMocks(); _setSpokeOracles(); } - function _addAllFixtures() internal { - _addLiqConfigFixtures(); - _addReserveFixtures(); - _addHubAssetFixtures(); - _configureIRStrategies(); - _addSpokeConfigFixtures(); - } - - // ------------------------------------------------------------------------- - // Empty inputs - // ------------------------------------------------------------------------- - + /// @dev Empty spokes and hubs produce an all-empty snapshot. function test_createV4Snapshot_emptyInputs() public view { ISpoke[] memory spokes = new ISpoke[](0); IHub[] memory hubs = new IHub[](0); @@ -31,13 +22,7 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { assertEq(snap.spokeConfigs.length, 0, 'empty caps'); } - // ------------------------------------------------------------------------- - // Partial-config scenarios — verify each section is independently captured. - // Note: `spokeLiquidationConfigs.length` is driven by the spoke count (always 2 - // here), not by `_addLiqConfigFixtures()`. Without populating liq configs, the - // mock returns a zero-default struct per spoke. - // ------------------------------------------------------------------------- - + /// @dev Adding only reserves leaves hub assets and spoke configs empty. function test_partial_reservesOnly() public { _addReserveFixtures(); Types.V4Snapshot memory snap = _createV4Snapshot(); @@ -46,6 +31,7 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { assertEq(snap.spokeConfigs.length, 0, 'spoke caps should be empty'); } + /// @dev Adding only hub assets leaves reserves and spoke configs empty. function test_partial_hubAssetsOnly() public { _addHubAssetFixtures(); _configureIRStrategies(); @@ -55,8 +41,8 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { assertEq(snap.spokeConfigs.length, 0, 'spoke caps should be empty'); } + /// @dev Hub assets without registered spoke configs produce empty spokeConfigs. function test_partial_hubAssetsNoSpokeConfigs() public { - // Assets present on hub, but no spoke configs registered → spokeConfigs stays empty. _addHubAssetFixtures(); _configureIRStrategies(); Types.V4Snapshot memory snap = _createV4Snapshot(); @@ -64,6 +50,7 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { assertEq(snap.spokeConfigs.length, 0, 'no spoke configs registered'); } + /// @dev Adding only liq configs leaves other sections empty. function test_partial_liqConfigsOnly() public { _addLiqConfigFixtures(); Types.V4Snapshot memory snap = _createV4Snapshot(); @@ -73,9 +60,8 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { assertEq(snap.spokeConfigs.length, 0, 'spoke caps should be empty'); } + /// @dev Liq configs are emitted per-spoke regardless of fixture state (zero-default). function test_partial_liqConfigsSpokeDriven() public view { - // Without `_addLiqConfigFixtures`, MockSpoke returns its zero-default config — - // so the snapshot still emits one entry per spoke. Types.V4Snapshot memory snap = _createV4Snapshot(); assertEq(snap.spokeLiquidationConfigs.length, 2, '2 spokes -> 2 liq configs'); assertEq( @@ -90,27 +76,24 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { ); } + /// @dev Mutating one oracle price moves only that reserve's price; other reserves stable. function test_delta_oraclePrice() public { _addAllFixtures(); Types.V4Snapshot memory snapA = _createV4Snapshot(); - // Mutate oracle price for the first reserve (USDC on spokeA, reserveId 0). CachedReserveFixture memory target = _reserveFixtures[0]; uint256 newPrice = target.input.oraclePrice * 2 + 1; target.input.oracle.setReserve(target.reserveId, target.input.priceSource, newPrice); Types.V4Snapshot memory snapB = _createV4Snapshot(); - // Length and structure unchanged assertEq(snapA.spokeReserves.length, snapB.spokeReserves.length, 'reserves length moved'); - // Target field moved assertEq(snapB.spokeReserves[0].oraclePrice, newPrice, 'price did not update'); assertTrue( snapA.spokeReserves[0].oraclePrice != snapB.spokeReserves[0].oraclePrice, 'price unchanged' ); - // Other reserves' prices unchanged for (uint256 i = 1; i < snapB.spokeReserves.length; i++) { assertEq( snapA.spokeReserves[i].oraclePrice, @@ -118,7 +101,6 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { 'unrelated price moved' ); } - // Sibling fields on the mutated reserve unchanged assertEq( snapA.spokeReserves[0].underlying, snapB.spokeReserves[0].underlying, @@ -127,13 +109,12 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { assertEq(snapA.spokeReserves[0].decimals, snapB.spokeReserves[0].decimals, 'decimals moved'); } + /// @dev Re-calling addSpokeConfig to flip `halted` moves only that field; array length stable. function test_delta_spokeConfigFlag() public { _addAllFixtures(); Types.V4Snapshot memory snapA = _createV4Snapshot(); - // Flip `halted` on the first spoke-config fixture; re-call addSpokeConfig overwrites - // in place (this is what the MockHub dedup fix protects). SpokeConfigFixture memory orig = _spokeConfigFixtures[0]; hub.addSpokeConfig( orig.assetId, @@ -149,23 +130,20 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { Types.V4Snapshot memory snapB = _createV4Snapshot(); - // Critical: length unchanged — proves no duplicate spoke push. assertEq(snapA.spokeConfigs.length, snapB.spokeConfigs.length, 'spokeConfigs length moved'); - // Target moved assertEq(snapB.spokeConfigs[0].halted, !orig.halted, 'halted did not flip'); assertTrue(snapA.spokeConfigs[0].halted != snapB.spokeConfigs[0].halted, 'halted unchanged'); - // Sibling fields on the same entry unchanged assertEq(snapA.spokeConfigs[0].addCap, snapB.spokeConfigs[0].addCap, 'addCap moved'); assertEq(snapA.spokeConfigs[0].drawCap, snapB.spokeConfigs[0].drawCap, 'drawCap moved'); assertEq(snapA.spokeConfigs[0].active, snapB.spokeConfigs[0].active, 'active moved'); } + /// @dev Mutating IR data moves only the corresponding hub asset's IR fields. function test_delta_irData() public { _addAllFixtures(); Types.V4Snapshot memory snapA = _createV4Snapshot(); - // Bump maxDrawnRate on ir0 (which serves hub asset 0 — USDC). uint256 newMaxDrawnRate = 99_999; ir0.setData( 0, @@ -186,13 +164,11 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { snapA.hubAssets[0].maxDrawnRate != snapB.hubAssets[0].maxDrawnRate, 'maxDrawnRate unchanged' ); - // Other IR-bound asset (WETH on ir1) unchanged assertEq( snapA.hubAssets[1].maxDrawnRate, snapB.hubAssets[1].maxDrawnRate, 'unrelated IR moved' ); - // Non-IR fields on the mutated asset unchanged assertEq(snapA.hubAssets[0].underlying, snapB.hubAssets[0].underlying, 'underlying moved'); assertEq( snapA.hubAssets[0].liquidityFee, @@ -201,11 +177,10 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { ); } + /// @dev Snapshot aggregates assets and configs across multiple hubs in pass order. function test_multiHub_aggregates() public { - // Populate the canonical hub fully. _addAllFixtures(); - // Deploy a second hub and add one asset + one spoke config to it. MockHub hub2 = new MockHub(); { IHub.Asset memory asset; @@ -235,7 +210,6 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { }) ); - // Snapshot with both hubs in order [hub, hub2]. ISpoke[] memory spokes = new ISpoke[](2); spokes[0] = ISpoke(address(spokeA)); spokes[1] = ISpoke(address(spokeB)); @@ -244,11 +218,9 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { hubs[1] = IHub(address(hub2)); Types.V4Snapshot memory snap = createV4Snapshot(spokes, hubs); - // hub has 3 assets + 4 spoke configs; hub2 contributes 1 + 1. assertEq(snap.hubAssets.length, _hubAssetFixtures.length + 1, 'hubAssets aggregated'); assertEq(snap.spokeConfigs.length, _spokeConfigFixtures.length + 1, 'spokeConfigs aggregated'); - // Order: hub1 entries first, then hub2. assertEq(snap.hubAssets[0].hubAddress, address(hub), 'first asset from hub1'); assertEq( snap.hubAssets[snap.hubAssets.length - 1].hubAddress, @@ -262,11 +234,18 @@ contract SnapshotV4CombinedTest is SnapshotV4BaseTest { 'last cap from hub2' ); - // hub2's spoke config values flow through correctly. Types.SpokeConfigSnapshot memory hub2Cap = snap.spokeConfigs[snap.spokeConfigs.length - 1]; assertEq(hub2Cap.assetId, 0, 'hub2 cap assetId'); assertEq(hub2Cap.spokeAddress, address(spokeA), 'hub2 cap spoke'); assertEq(uint256(hub2Cap.addCap), 7_000_000, 'hub2 addCap'); assertEq(uint256(hub2Cap.drawCap), 6_000_000, 'hub2 drawCap'); } + + function _addAllFixtures() internal { + _addLiqConfigFixtures(); + _addReserveFixtures(); + _addHubAssetFixtures(); + _configureIRStrategies(); + _addSpokeConfigFixtures(); + } } From c4629029b913c4d1f8e6f4fde056772b41445a95 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Mon, 11 May 2026 21:34:59 -0500 Subject: [PATCH 27/29] chore: lint --- packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts | 4 +++- src/dependencies/v4/V4DiffWriter.sol | 5 ++++- tests/dependencies/v4/SnapshotV4.Reserve.t.sol | 6 +++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts index 5567d978..e3768c47 100644 --- a/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts +++ b/packages/aave-helpers-js/__tests__/protocol-diff-v4.spec.ts @@ -199,7 +199,9 @@ describe('BPS formatting via formatV4Value', () => { }, }; expect(formatV4Value('spokeConfig', 'addCap', 1000000, capCtx)).toBe('1,000,000 (1e6) USDT'); - expect(formatV4Value('spokeConfig', 'drawCap', 1880000, capCtx)).toBe('1,880,000 (1.88e6) USDT'); + expect(formatV4Value('spokeConfig', 'drawCap', 1880000, capCtx)).toBe( + '1,880,000 (1.88e6) USDT' + ); // Falls back gracefully when symbol unavailable expect(formatV4Value('spokeConfig', 'addCap', 1000000, ctx)).toBe('1,000,000 (1e6)'); // Small caps (< 1000) skip the exponential diff --git a/src/dependencies/v4/V4DiffWriter.sol b/src/dependencies/v4/V4DiffWriter.sol index b9cf7232..c6eefaee 100644 --- a/src/dependencies/v4/V4DiffWriter.sol +++ b/src/dependencies/v4/V4DiffWriter.sol @@ -147,7 +147,10 @@ library V4DiffWriter { return vm.serializeString(k, 'premiumOffsetRay', vm.toString(a.premiumOffsetRay)); } - function _writeSpokeConfigs(string memory path, Types.SpokeConfigSnapshot[] memory caps) internal { + function _writeSpokeConfigs( + string memory path, + Types.SpokeConfigSnapshot[] memory caps + ) internal { string memory sectionKey = 'spokeConfigs'; string memory content = '{}'; vm.serializeJson(sectionKey, '{}'); diff --git a/tests/dependencies/v4/SnapshotV4.Reserve.t.sol b/tests/dependencies/v4/SnapshotV4.Reserve.t.sol index b6e12c35..d6435585 100644 --- a/tests/dependencies/v4/SnapshotV4.Reserve.t.sol +++ b/tests/dependencies/v4/SnapshotV4.Reserve.t.sol @@ -39,7 +39,11 @@ contract SnapshotV4ReserveTest is SnapshotV4BaseTest { ISpoke.ReserveConfig memory cfg = _buildReserveConfigFrom(t.input); cfg.borrowable = !cfg.borrowable; t.input.spoke.setReserveConfig(t.reserveId, cfg); - assertEq(_createV4Snapshot().spokeReserves[TARGET_IDX].borrowable, cfg.borrowable, 'borrowable'); + assertEq( + _createV4Snapshot().spokeReserves[TARGET_IDX].borrowable, + cfg.borrowable, + 'borrowable' + ); } /// @dev Flipping ReserveConfig.receiveSharesEnabled propagates. From 3339333799f1b312bee74065d4a6c227d5ae370c Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Mon, 11 May 2026 21:43:16 -0500 Subject: [PATCH 28/29] fix: address pr comment --- src/ProtocolV4TestBase.sol | 58 +++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/ProtocolV4TestBase.sol b/src/ProtocolV4TestBase.sol index 3c1988c1..d45c6546 100644 --- a/src/ProtocolV4TestBase.sol +++ b/src/ProtocolV4TestBase.sol @@ -246,35 +246,6 @@ contract ProtocolV4TestBase is SnapshotV4, Scenarios, TokenizationScenarios, Gat } } - /// @notice Test that a frozen reserve correctly reverts on supply and borrow. - function e2eTestFrozenAsset(ISpoke spoke, Types.ReserveInfo memory frozenAsset) public { - console.log('E2E: Testing frozen reserve %s (should revert)', frozenAsset.symbol); - - address oracleAddr = spoke.ORACLE(); - address user = vm.randomAddress(); - uint256 amount = _getTokenAmountByDollarValue({ - oracleAddr: oracleAddr, - reserveInfo: frozenAsset, - dollarValue: 1_000 - }); - - deal2(frozenAsset.underlying, user, amount); - - // Supply should revert with ReserveFrozen - vm.startPrank(user); - IERC20(frozenAsset.underlying).forceApprove(address(spoke), amount); - vm.expectRevert(ISpoke.ReserveFrozen.selector); - spoke.supply({reserveId: frozenAsset.reserveId, amount: amount, onBehalfOf: user}); - vm.stopPrank(); - - // Borrow should revert with ReserveFrozen (if borrowable) - if (frozenAsset.borrowable) { - vm.prank(user); - vm.expectRevert(ISpoke.ReserveFrozen.selector); - spoke.borrow({reserveId: frozenAsset.reserveId, amount: amount, onBehalfOf: user}); - } - } - /// @notice Test all regular position managers on a spoke. function e2eTestRegularPositionManagers(ISpoke spoke) public { _setCapsToMax(spoke); @@ -638,6 +609,35 @@ contract ProtocolV4TestBase is SnapshotV4, Scenarios, TokenizationScenarios, Gat }); } + /// @notice Test that a frozen reserve correctly reverts on supply and borrow. + function e2eTestFrozenAsset(ISpoke spoke, Types.ReserveInfo memory frozenAsset) public { + console.log('E2E: Testing frozen reserve %s (should revert)', frozenAsset.symbol); + + address oracleAddr = spoke.ORACLE(); + address user = vm.randomAddress(); + uint256 amount = _getTokenAmountByDollarValue({ + oracleAddr: oracleAddr, + reserveInfo: frozenAsset, + dollarValue: 1_000 + }); + + deal2(frozenAsset.underlying, user, amount); + + // Supply should revert with ReserveFrozen + vm.startPrank(user); + IERC20(frozenAsset.underlying).forceApprove(address(spoke), amount); + vm.expectRevert(ISpoke.ReserveFrozen.selector); + spoke.supply({reserveId: frozenAsset.reserveId, amount: amount, onBehalfOf: user}); + vm.stopPrank(); + + // Borrow should revert with ReserveFrozen (if borrowable) + if (frozenAsset.borrowable) { + vm.prank(user); + vm.expectRevert(ISpoke.ReserveFrozen.selector); + spoke.borrow({reserveId: frozenAsset.reserveId, amount: amount, onBehalfOf: user}); + } + } + /// @notice Per-asset e2e test with randomized amounts and extra collaterals. function e2eTestAsset( ISpoke spoke, From 87e297d4e70cbe04c424f4e4b2bba4e10920c976 Mon Sep 17 00:00:00 2001 From: YBM <31329384+yan-man@users.noreply.github.com> Date: Thu, 14 May 2026 15:31:06 -0500 Subject: [PATCH 29/29] fix: expand bounds for asset amt --- src/dependencies/v4/GatewayScenarios.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dependencies/v4/GatewayScenarios.sol b/src/dependencies/v4/GatewayScenarios.sol index aa3c8914..ed24f585 100644 --- a/src/dependencies/v4/GatewayScenarios.sol +++ b/src/dependencies/v4/GatewayScenarios.sol @@ -551,7 +551,7 @@ abstract contract GatewayScenarios is Helpers { assertApproxEqAbs( spoke.getUserSuppliedAssets(reserveInfo.reserveId, user) - userAssetsBefore, amountSupplied, - 1, + 2, 'SIG_SUPPLY: user assets mismatch' ); assertEq( @@ -563,7 +563,7 @@ abstract contract GatewayScenarios is Helpers { IHubBase(reserveInfo.hub).getSpokeAddedAssets(reserveInfo.assetId, address(spoke)) - hubAssetsBefore, amountSupplied, - 1, + 2, 'SIG_SUPPLY: hub assets mismatch' ); }