From 6a74798010a7f83f32022f23c73b3938db87a88d Mon Sep 17 00:00:00 2001 From: makemake Date: Thu, 30 Jul 2026 17:45:08 +0200 Subject: [PATCH 1/9] fix(aave): harden v3 Horizon oracle guard --- examples/aave/README.md | 18 + examples/aave/src/AaveV3HorizonHelpers.sol | 22 +- examples/aave/src/AaveV3HorizonInterfaces.sol | 5 + .../aave/src/AaveV3HorizonOracleAssertion.sol | 338 ++++++---- .../aave/test/AaveV3AdversarialResearch.t.sol | 626 ++++++++++++++++++ .../test/AaveV3HorizonOracleAssertion.t.sol | 403 +++++++++++ .../AaveV3OperationBoundaryResearch.t.sol | 181 +++++ .../lending/examples/AaveV3LikeInterfaces.sol | 10 + 8 files changed, 1455 insertions(+), 148 deletions(-) create mode 100644 examples/aave/test/AaveV3AdversarialResearch.t.sol create mode 100644 examples/aave/test/AaveV3HorizonOracleAssertion.t.sol create mode 100644 examples/aave/test/AaveV3OperationBoundaryResearch.t.sol diff --git a/examples/aave/README.md b/examples/aave/README.md index 4e96ef6..644a50b 100644 --- a/examples/aave/README.md +++ b/examples/aave/README.md @@ -18,3 +18,21 @@ FOUNDRY_PROFILE=aave forge build - AaveV4HubAccountingAssertion.sol - AaveV4Interfaces.sol - AaveV4SpokeRiskAssertion.sol + +## Aave v3 Horizon oracle guard + +`AaveV3HorizonOracleAssertion` checks the exact `AaveOracle.getAssetPrice` +returns consumed by successful risk-sensitive Pool operations against per-asset +PreTx baselines. It also rejects temporary provider, source, and fallback +changes, including configuration writes restored before transaction end. + +Deployment must configure an `AssetPolicy` for every active reserve whose price +the Pool can consume. Policies with zero deviation require exact same-transaction +price stability; nonzero tolerances should be asset-specific and empirically +calibrated. `maxTraceCalls` is a fail-closed bound on traced provider, oracle, and +source calls. + +The source/fallback storage guards are pinned to the Aave v3.3 `AaveOracle` +layout (`assetsSources` mapping slot 0 and `_fallbackOracle` slot 1). Re-verify +those slots before adopting the assertion against a different oracle +implementation or storage layout. diff --git a/examples/aave/src/AaveV3HorizonHelpers.sol b/examples/aave/src/AaveV3HorizonHelpers.sol index 86dde30..cc57447 100644 --- a/examples/aave/src/AaveV3HorizonHelpers.sol +++ b/examples/aave/src/AaveV3HorizonHelpers.sol @@ -5,7 +5,11 @@ import {Assertion} from "credible-std/Assertion.sol"; import {PhEvm} from "credible-std/PhEvm.sol"; import {AssertionSpec} from "credible-std/SpecRecorder.sol"; -import {AaveV3LikeTypes, IAaveV3LikeAddressesProvider, IAaveV3LikePool} from "credible-std/protection/lending/examples/AaveV3LikeInterfaces.sol"; +import { + AaveV3LikeTypes, + IAaveV3LikeAddressesProvider, + IAaveV3LikePool +} from "credible-std/protection/lending/examples/AaveV3LikeInterfaces.sol"; import {IAaveV3HorizonOracle} from "./AaveV3HorizonInterfaces.sol"; /// @title AaveV3HorizonHelpers @@ -64,22 +68,6 @@ abstract contract AaveV3HorizonHelpers is Assertion { return _readAddressAt(oracle, abi.encodeCall(IAaveV3HorizonOracle.getSourceOfAsset, (asset)), fork); } - function _assertPriceBounded( - address oracle, - address asset, - PhEvm.ForkId memory pre, - PhEvm.ForkId memory post, - uint256 deviationBps - ) internal view { - uint256 prePrice = _assetPriceAt(oracle, asset, pre); - uint256 postPrice = _assetPriceAt(oracle, asset, post); - require(prePrice > 0 && postPrice > 0, "AaveV3Horizon: reserve oracle price invalid"); - require( - ph.ratioGe(postPrice, 1, prePrice, 1, deviationBps) && ph.ratioGe(prePrice, 1, postPrice, 1, deviationBps), - "AaveV3Horizon: reserve oracle price drift" - ); - } - function _requireAdopter(address expected, string memory message) internal view { require(ph.getAssertionAdopter() == expected, message); } diff --git a/examples/aave/src/AaveV3HorizonInterfaces.sol b/examples/aave/src/AaveV3HorizonInterfaces.sol index e5cb685..e4d6bf9 100644 --- a/examples/aave/src/AaveV3HorizonInterfaces.sol +++ b/examples/aave/src/AaveV3HorizonInterfaces.sol @@ -10,6 +10,11 @@ interface IAaveV3HorizonOracle { function setFallbackOracle(address fallbackOracle) external; } +/// @notice Chainlink-compatible source surface consumed by the Horizon AaveOracle. +interface IAaveV3HorizonSource { + function latestAnswer() external view returns (int256); +} + /// @notice Minimal ERC20/accounting-token surface used by Horizon reserve backing checks. interface IAaveV3HorizonToken { function balanceOf(address account) external view returns (uint256); diff --git a/examples/aave/src/AaveV3HorizonOracleAssertion.sol b/examples/aave/src/AaveV3HorizonOracleAssertion.sol index 0c0284f..6e32db0 100644 --- a/examples/aave/src/AaveV3HorizonOracleAssertion.sol +++ b/examples/aave/src/AaveV3HorizonOracleAssertion.sol @@ -3,189 +3,265 @@ pragma solidity ^0.8.13; import {PhEvm} from "credible-std/PhEvm.sol"; -import {AaveV3LikeTypes, IAaveV3LikePool} from "credible-std/protection/lending/examples/AaveV3LikeInterfaces.sol"; +import { + IAaveV3LikeAddressesProvider, + IAaveV3LikePool +} from "credible-std/protection/lending/examples/AaveV3LikeInterfaces.sol"; import {AaveV3HorizonHelpers} from "./AaveV3HorizonHelpers.sol"; +import {IAaveV3HorizonOracle, IAaveV3HorizonSource} from "./AaveV3HorizonInterfaces.sol"; /// @title AaveV3HorizonOracleAssertion /// @author Phylax Systems -/// @notice Protects Horizon's oracle-backed lending risk state. -/// @dev This assertion targets properties that are not local `require` checks: -/// - Risk-changing Pool operations must not share a transaction envelope with material -/// PreTx/PostTx oracle drift for active collateral, debt reserves, or touched assets. -/// - The active oracle source for those reserves must not be swapped earlier or later in the -/// same transaction while the Pool consumes the resulting risk state. -/// - The checks span Pool reserve/user bitmaps, the AddressesProvider, AaveOracle, and -/// Chainlink-compatible source contracts across the whole transaction, not one call frame. +/// @notice Prevents Horizon risk operations from consuming a same-transaction manipulated price. +/// @dev The assertion: +/// - Fires once after a transaction containing successful risk-sensitive Pool operations. +/// - Inspects exact AaveOracle returns consumed by the Pool, including intermediate values +/// that were restored before PostTx. +/// - Compares each consumed return with its configured asset's PreTx oracle value. +/// - Rejects writes to the pinned AaveOracle source/fallback slots, including write-restore. +/// - Inspects the Pool's actual provider return so temporary provider swaps cannot hide. +/// This is a same-transaction consumption guard. It does not prove that the PreTx price was +/// independently correct or detect manipulation completed in an earlier transaction. contract AaveV3HorizonOracleAssertion is AaveV3HorizonHelpers { + uint256 internal constant ASSET_SOURCES_MAPPING_SLOT = 0; + bytes32 internal constant FALLBACK_ORACLE_SLOT = bytes32(uint256(1)); + + struct AssetPolicy { + address asset; + uint256 deviationBps; + } + address internal immutable POOL; address internal immutable ADDRESSES_PROVIDER; - uint256 internal immutable MAX_RESERVES_TO_SCAN; - uint256 internal immutable ORACLE_DEVIATION_BPS; + uint256 internal immutable MAX_TRACE_CALLS; + + AssetPolicy[] internal assetPolicies; - constructor(address pool_, address addressesProvider_, uint256 maxReservesToScan_, uint256 oracleDeviationBps_) { + /// @param pool_ Horizon Pool that adopts the assertion. + /// @param addressesProvider_ PoolAddressesProvider used by that Pool. + /// @param maxTraceCalls_ Fail-closed bound for matching provider/oracle/source calls. + /// @param assetPolicies_ Complete active-reserve list with per-asset deviation tolerances. + constructor( + address pool_, + address addressesProvider_, + uint256 maxTraceCalls_, + AssetPolicy[] memory assetPolicies_ + ) { require(pool_ != address(0), "AaveV3Horizon: pool zero"); require(addressesProvider_ != address(0), "AaveV3Horizon: provider zero"); - require(maxReservesToScan_ != 0, "AaveV3Horizon: max reserves zero"); - require(oracleDeviationBps_ <= BPS, "AaveV3Horizon: bad oracle tolerance"); + require(maxTraceCalls_ != 0, "AaveV3Horizon: max trace calls zero"); + require(maxTraceCalls_ < type(uint256).max, "AaveV3Horizon: max trace calls too large"); + require(assetPolicies_.length != 0, "AaveV3Horizon: empty asset policies"); POOL = pool_; ADDRESSES_PROVIDER = addressesProvider_; - MAX_RESERVES_TO_SCAN = maxReservesToScan_; - ORACLE_DEVIATION_BPS = oracleDeviationBps_; + MAX_TRACE_CALLS = maxTraceCalls_; + + for (uint256 i; i < assetPolicies_.length; ++i) { + AssetPolicy memory policy = assetPolicies_[i]; + require(policy.asset != address(0), "AaveV3Horizon: policy asset zero"); + require(policy.deviationBps <= BPS, "AaveV3Horizon: bad asset tolerance"); + + for (uint256 j; j < i; ++j) { + require(assetPolicies_[j].asset != policy.asset, "AaveV3Horizon: duplicate asset policy"); + } + + assetPolicies.push(policy); + } } - /// @notice Registers one transaction-end check for Horizon oracle/risk coupling. - /// @dev This intentionally runs after the whole transaction, so it catches bundled oracle - /// changes that happened before or after a Pool risk operation. The Pool function itself - /// cannot reproduce the PreTx oracle/source baseline with a local require. + /// @notice Registers one transaction-end trace check for Pool-consumed prices. + /// @dev The assertion executes once for multicalls but examines intermediate call inputs and + /// outputs, so a manipulated price restored before PostTx remains observable. function triggers() external view override { - registerTxEndTrigger(this.assertRiskOperationOracleEnvelope.selector); + registerTxEndTrigger(this.assertConsumedOraclePricesSafe.selector); } - /// @notice Bounds oracle/source movement across any transaction that includes Pool risk operations. - /// @dev Scans successful Horizon Pool calls in the transaction, resolves affected users/assets, - /// and compares oracle prices and source addresses between PreTx and PostTx. A failure - /// means the transaction consumed lending risk state while also changing the oracle basis - /// used to value that risk. - function assertRiskOperationOracleEnvelope() external view { + /// @notice Verifies exact oracle values consumed during successful risk-sensitive Pool calls. + /// @dev Resolves configured assets and their sources at PreTx, rejects any source/fallback + /// storage writes in the transaction, and maps each source `latestAnswer` call to its + /// parent AaveOracle call. The parent call's actual return is compared with the PreTx price. + /// Every Pool-originated AaveOracle call must map to a configured source; unknown or + /// fallback-only price paths fail closed. + function assertConsumedOraclePricesSafe() external view { _requireAdopter(POOL, "AaveV3Horizon: configured pool is not adopter"); + if (!_hasRiskOperation()) { + return; + } - PhEvm.ForkId memory pre = _preTx(); - PhEvm.ForkId memory post = _postTx(); - address oracle = _oracleAt(ADDRESSES_PROVIDER, post); + PhEvm.ForkId memory preTx = _preTx(); + PhEvm.ForkId memory postTx = _postTx(); - _assertCallGroupOracleEnvelope(oracle, IAaveV3LikePool.borrow.selector, pre, post); - _assertCallGroupOracleEnvelope(oracle, IAaveV3LikePool.withdraw.selector, pre, post); - _assertCallGroupOracleEnvelope(oracle, IAaveV3LikePool.setUserUseReserveAsCollateral.selector, pre, post); - _assertCallGroupOracleEnvelope(oracle, IAaveV3LikePool.finalizeTransfer.selector, pre, post); - _assertCallGroupOracleEnvelope(oracle, IAaveV3LikePool.setUserEMode.selector, pre, post); - _assertCallGroupOracleEnvelope(oracle, IAaveV3LikePool.liquidationCall.selector, pre, post); - } + address oracle = _oracleAt(ADDRESSES_PROVIDER, preTx); + require(oracle != address(0), "AaveV3Horizon: oracle zero"); + require( + _oracleAt(ADDRESSES_PROVIDER, postTx) == oracle, "AaveV3Horizon: provider oracle changed during transaction" + ); - function _assertCallGroupOracleEnvelope( - address oracle, - bytes4 selector, - PhEvm.ForkId memory pre, - PhEvm.ForkId memory post - ) internal view { - PhEvm.CallInputs[] memory calls = ph.getAllCallInputs(POOL, selector); - - for (uint256 i; i < calls.length; ++i) { - address account = _operationAccount(selector, calls[i].input, calls[i].caller); - if (account != address(0)) { - _assertAccountReservePricesBounded(account, oracle, pre, post); - } + _assertAssetPoliciesComplete(preTx); + _assertAssetPoliciesComplete(postTx); - _assertTouchedAssetPricesBounded(selector, calls[i].input, oracle, pre, post); + uint256 policyCount = assetPolicies.length; + address[] memory sources = new address[](policyCount); + uint256[] memory baselinePrices = new uint256[](policyCount); + + for (uint256 i; i < policyCount; ++i) { + AssetPolicy memory policy = assetPolicies[i]; + address source = _sourceOfAssetAt(oracle, policy.asset, preTx); + require(source != address(0), "AaveV3Horizon: fallback-only asset unsupported"); + + uint256 baselinePrice = _assetPriceAt(oracle, policy.asset, preTx); + require(baselinePrice != 0, "AaveV3Horizon: baseline oracle price invalid"); + + sources[i] = source; + baselinePrices[i] = baselinePrice; } + + _assertOracleConfigurationUnchanged(oracle); + _assertPoolProviderReturns(oracle); + _assertConsumedPrices(oracle, sources, baselinePrices); } - function _assertAccountReservePricesBounded( - address account, - address oracle, - PhEvm.ForkId memory pre, - PhEvm.ForkId memory post - ) internal view { - address[] memory reserves = _reservesListAt(POOL, post); - require(reserves.length <= MAX_RESERVES_TO_SCAN, "AaveV3Horizon: too many reserves"); + function assetPolicyCount() external view returns (uint256) { + return assetPolicies.length; + } - uint256 preConfig = _userConfigDataAt(POOL, account, pre); - uint256 postConfig = _userConfigDataAt(POOL, account, post); + function assetPolicy(uint256 index) external view returns (AssetPolicy memory) { + return assetPolicies[index]; + } - for (uint256 i; i < reserves.length; ++i) { - AaveV3LikeTypes.ReserveData memory reserveData = _reserveDataAt(POOL, reserves[i], post); - bool activeBefore = - _isBorrowing(preConfig, reserveData.id) || _isUsingAsCollateral(preConfig, reserveData.id); - bool activeAfter = - _isBorrowing(postConfig, reserveData.id) || _isUsingAsCollateral(postConfig, reserveData.id); + function _assertAssetPoliciesComplete(PhEvm.ForkId memory fork) internal view { + address[] memory reserves = _reservesListAt(POOL, fork); + require(reserves.length == assetPolicies.length, "AaveV3Horizon: unrecognized Pool oracle price path"); - if (!activeBefore && !activeAfter) { - continue; + for (uint256 i; i < reserves.length; ++i) { + bool configured; + for (uint256 j; j < assetPolicies.length; ++j) { + if (reserves[i] == assetPolicies[j].asset) { + configured = true; + break; + } } - - _assertSourceStable(oracle, reserves[i], pre, post); - _assertPriceBounded(oracle, reserves[i], pre, post, ORACLE_DEVIATION_BPS); + require(configured, "AaveV3Horizon: unrecognized Pool oracle price path"); } } - function _assertTouchedAssetPricesBounded( - bytes4 selector, - bytes memory input, - address oracle, - PhEvm.ForkId memory pre, - PhEvm.ForkId memory post - ) internal view { - if (selector == IAaveV3LikePool.borrow.selector) { - (address asset,,,,) = abi.decode(input, (address, uint256, uint256, uint16, address)); - _assertSourceStable(oracle, asset, pre, post); - _assertPriceBounded(oracle, asset, pre, post, ORACLE_DEVIATION_BPS); - return; - } + function _assertOracleConfigurationUnchanged(address oracle) internal view { + require( + ph.getStateChanges(oracle, FALLBACK_ORACLE_SLOT).length == 0, + "AaveV3Horizon: fallback oracle changed during transaction" + ); - if (selector == IAaveV3LikePool.withdraw.selector) { - (address asset,,) = abi.decode(input, (address, uint256, address)); - _assertSourceStable(oracle, asset, pre, post); - _assertPriceBounded(oracle, asset, pre, post, ORACLE_DEVIATION_BPS); - return; + for (uint256 i; i < assetPolicies.length; ++i) { + bytes32 sourceSlot = keccak256(abi.encode(assetPolicies[i].asset, ASSET_SOURCES_MAPPING_SLOT)); + require( + ph.getStateChanges(oracle, sourceSlot).length == 0, + "AaveV3Horizon: reserve oracle source changed during transaction" + ); } + } - if (selector == IAaveV3LikePool.setUserUseReserveAsCollateral.selector) { - (address asset,) = abi.decode(input, (address, bool)); - _assertSourceStable(oracle, asset, pre, post); - _assertPriceBounded(oracle, asset, pre, post, ORACLE_DEVIATION_BPS); - return; - } + function _assertPoolProviderReturns(address expectedOracle) internal view { + PhEvm.CallInputs[] memory providerCalls = + ph.getStaticCallInputs(ADDRESSES_PROVIDER, IAaveV3LikeAddressesProvider.getPriceOracle.selector); + require(providerCalls.length <= MAX_TRACE_CALLS, "AaveV3Horizon: too many provider calls"); - if (selector == IAaveV3LikePool.finalizeTransfer.selector) { - (address asset,,,,,) = abi.decode(input, (address, address, address, uint256, uint256, uint256)); - _assertSourceStable(oracle, asset, pre, post); - _assertPriceBounded(oracle, asset, pre, post, ORACLE_DEVIATION_BPS); - return; - } + for (uint256 i; i < providerCalls.length; ++i) { + if (providerCalls[i].caller != POOL) { + continue; + } - if (selector == IAaveV3LikePool.liquidationCall.selector) { - (address collateralAsset, address debtAsset,,,) = - abi.decode(input, (address, address, address, uint256, bool)); - _assertSourceStable(oracle, collateralAsset, pre, post); - _assertSourceStable(oracle, debtAsset, pre, post); - _assertPriceBounded(oracle, collateralAsset, pre, post, ORACLE_DEVIATION_BPS); - _assertPriceBounded(oracle, debtAsset, pre, post, ORACLE_DEVIATION_BPS); + bytes memory output = ph.callOutputAt(providerCalls[i].id); + require(output.length == 32, "AaveV3Horizon: malformed provider output"); + require(abi.decode(output, (address)) == expectedOracle, "AaveV3Horizon: Pool consumed a different oracle"); } } - function _assertSourceStable(address oracle, address asset, PhEvm.ForkId memory pre, PhEvm.ForkId memory post) + function _assertConsumedPrices(address oracle, address[] memory sources, uint256[] memory baselinePrices) internal view { - address preSource = _sourceOfAssetAt(oracle, asset, pre); - address postSource = _sourceOfAssetAt(oracle, asset, post); - require(preSource == postSource, "AaveV3Horizon: reserve oracle source changed"); - } + PhEvm.CallInputs[] memory priceCalls = + ph.getStaticCallInputs(oracle, IAaveV3HorizonOracle.getAssetPrice.selector); + require(priceCalls.length <= MAX_TRACE_CALLS, "AaveV3Horizon: too many oracle calls"); + + bool[] memory mappedPriceCalls = new bool[](priceCalls.length); + PhEvm.CallFilter memory filter = PhEvm.CallFilter({ + callType: 2, minDepth: 0, maxDepth: type(uint32).max, topLevelOnly: false, successOnly: true + }); + + for (uint256 i; i < assetPolicies.length; ++i) { + PhEvm.TriggerCall[] memory sourceCalls = + ph.matchingCalls(sources[i], IAaveV3HorizonSource.latestAnswer.selector, filter, MAX_TRACE_CALLS + 1); + require(sourceCalls.length <= MAX_TRACE_CALLS, "AaveV3Horizon: too many source calls"); + + for (uint256 j; j < sourceCalls.length; ++j) { + if (sourceCalls[j].caller != oracle) { + continue; + } + + for (uint256 k; k < priceCalls.length; ++k) { + if (priceCalls[k].caller != POOL || priceCalls[k].id != sourceCalls[j].parentCallId) { + continue; + } + (bool assetAvailable, address priceCallAsset) = _priceCallAsset(priceCalls[k].input); + if (assetAvailable && priceCallAsset != assetPolicies[i].asset) { + continue; + } + + bytes memory output = ph.callOutputAt(priceCalls[k].id); + require(output.length == 32, "AaveV3Horizon: malformed oracle output"); + uint256 consumedPrice = abi.decode(output, (uint256)); - function _operationAccount(bytes4 selector, bytes memory input, address caller) internal pure returns (address) { - if (selector == IAaveV3LikePool.borrow.selector) { - (,,,, address onBehalfOf) = abi.decode(input, (address, uint256, uint256, uint16, address)); - return onBehalfOf; + _requirePriceWithinPolicy(baselinePrices[i], consumedPrice, assetPolicies[i].deviationBps); + mappedPriceCalls[k] = true; + } + } } - if ( - selector == IAaveV3LikePool.withdraw.selector - || selector == IAaveV3LikePool.setUserUseReserveAsCollateral.selector - || selector == IAaveV3LikePool.setUserEMode.selector - ) { - return caller; + for (uint256 i; i < priceCalls.length; ++i) { + if (priceCalls[i].caller == POOL) { + require(mappedPriceCalls[i], "AaveV3Horizon: unrecognized Pool oracle price path"); + } } + } - if (selector == IAaveV3LikePool.finalizeTransfer.selector) { - (, address from,,,,) = abi.decode(input, (address, address, address, uint256, uint256, uint256)); - return from; + function _priceCallAsset(bytes memory priceCallInput) internal pure returns (bool available, address asset) { + if (priceCallInput.length != 0) { + require(priceCallInput.length == 32, "AaveV3Horizon: malformed oracle input"); + return (true, abi.decode(priceCallInput, (address))); } - if (selector == IAaveV3LikePool.liquidationCall.selector) { - (,, address user,,) = abi.decode(input, (address, address, address, uint256, bool)); - return user; + // PCL 1.6 omits nested STATICCALL input. Reserve-policy completeness above keeps + // source-only matching fail closed until that runtime exposes the documented calldata. + return (false, address(0)); + } + + function _requirePriceWithinPolicy(uint256 baselinePrice, uint256 consumedPrice, uint256 deviationBps) + internal + view + { + require(consumedPrice != 0, "AaveV3Horizon: consumed oracle price invalid"); + + if (deviationBps == 0) { + require(consumedPrice == baselinePrice, "AaveV3Horizon: consumed oracle price deviated"); + return; } - return address(0); + uint256 lowerBound = ph.mulDivDown(baselinePrice, BPS - deviationBps, BPS); + uint256 upperBound = ph.mulDivUp(baselinePrice, BPS + deviationBps, BPS); + require( + consumedPrice >= lowerBound && consumedPrice <= upperBound, "AaveV3Horizon: consumed oracle price deviated" + ); + } + + function _hasRiskOperation() internal view returns (bool) { + return _matchingCalls(POOL, IAaveV3LikePool.borrow.selector, 1).length != 0 + || _matchingCalls(POOL, IAaveV3LikePool.withdraw.selector, 1).length != 0 + || _matchingCalls(POOL, IAaveV3LikePool.setUserUseReserveAsCollateral.selector, 1).length != 0 + || _matchingCalls(POOL, IAaveV3LikePool.finalizeTransfer.selector, 1).length != 0 + || _matchingCalls(POOL, IAaveV3LikePool.setUserEMode.selector, 1).length != 0 + || _matchingCalls(POOL, IAaveV3LikePool.liquidationCall.selector, 1).length != 0 + || _matchingCalls(POOL, IAaveV3LikePool.flashLoan.selector, 1).length != 0; } } diff --git a/examples/aave/test/AaveV3AdversarialResearch.t.sol b/examples/aave/test/AaveV3AdversarialResearch.t.sol new file mode 100644 index 0000000..769e74e --- /dev/null +++ b/examples/aave/test/AaveV3AdversarialResearch.t.sol @@ -0,0 +1,626 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {CredibleTest} from "credible-std/CredibleTest.sol"; +import { + AaveV3HorizonOperationSafetyAssertion +} from "credible-std/protection/lending/examples/AaveV3PostOperationSolvency.sol"; +import {LendingBaseAssertion} from "credible-std/protection/lending/LendingBaseAssertion.sol"; +import {AaveV3LikeTypes, IAaveV3LikePool} from "credible-std/protection/lending/examples/AaveV3LikeInterfaces.sol"; +import {AaveV3HorizonOracleAssertion} from "../src/AaveV3HorizonOracleAssertion.sol"; +import {AaveV3HorizonReserveBackingAssertion} from "../src/AaveV3HorizonReserveBackingAssertion.sol"; +import {AaveV3HorizonHelpers} from "../src/AaveV3HorizonHelpers.sol"; +import {IAaveV3HorizonOracle} from "../src/AaveV3HorizonInterfaces.sol"; + +/// @notice Research-only mocks. They deliberately expose mutations that ordinary Aave entrypoints +/// do not expose so the assertions can be tested as independent failure detectors. +contract ResearchAccountingToken { + uint8 public immutable decimals; + uint256 public totalSupply; + mapping(address => uint256) public balanceOf; + + event Transfer(address indexed from, address indexed to, uint256 amount); + + constructor(uint8 decimals_) { + decimals = decimals_; + } + + function mint(address to, uint256 amount) external { + totalSupply += amount; + balanceOf[to] += amount; + emit Transfer(address(0), to, amount); + } + + function seize(address from, address to, uint256 amount) external { + balanceOf[from] -= amount; + balanceOf[to] += amount; + emit Transfer(from, to, amount); + } +} + +contract ResearchPriceSource { + int256 internal answer; + + constructor(int256 answer_) { + answer = answer_; + } + + function setAnswer(int256 answer_) external { + answer = answer_; + } + + function latestAnswer() external view returns (int256) { + return answer; + } +} + +contract ResearchFallbackOracle { + mapping(address => uint256) public prices; + + function setPrice(address asset, uint256 price) external { + prices[asset] = price; + } + + function getAssetPrice(address asset) external view returns (uint256) { + return prices[asset]; + } +} + +contract ResearchOracle is IAaveV3HorizonOracle { + mapping(address => address) internal sources; + address internal fallbackOracle; + + constructor(address fallbackOracle_) { + fallbackOracle = fallbackOracle_; + } + + function setAssetSources(address[] calldata assets, address[] calldata newSources) external { + require(assets.length == newSources.length, "length"); + for (uint256 i; i < assets.length; ++i) { + sources[assets[i]] = newSources[i]; + } + } + + function setFallbackOracle(address fallbackOracle_) external { + fallbackOracle = fallbackOracle_; + } + + function getAssetPrice(address asset) external view returns (uint256) { + address source = sources[asset]; + if (source == address(0)) { + return ResearchFallbackOracle(fallbackOracle).getAssetPrice(asset); + } + + int256 answer = ResearchPriceSource(source).latestAnswer(); + if (answer <= 0) { + return ResearchFallbackOracle(fallbackOracle).getAssetPrice(asset); + } + return uint256(answer); + } + + function getSourceOfAsset(address asset) external view returns (address) { + return sources[asset]; + } + + function getFallbackOracle() external view returns (address) { + return fallbackOracle; + } +} + +contract ResearchAddressesProvider { + address internal oracle; + + constructor(address oracle_) { + oracle = oracle_; + } + + function setPriceOracle(address oracle_) external { + oracle = oracle_; + } + + function getPriceOracle() external view returns (address) { + return oracle; + } +} + +contract ResearchPool is IAaveV3LikePool { + address public immutable override ADDRESSES_PROVIDER; + + struct AccountData { + uint256 totalCollateralBase; + uint256 totalDebtBase; + uint256 availableBorrowsBase; + uint256 currentLiquidationThreshold; + uint256 ltv; + uint256 healthFactor; + } + + address[] internal reserves; + mapping(address => bool) internal listed; + mapping(address => AaveV3LikeTypes.ReserveData) internal reserveData; + mapping(address => AaveV3LikeTypes.UserConfigurationMap) internal userConfig; + mapping(address => AccountData) internal accounts; + mapping(address => uint256) internal deficits; + + constructor(address provider_) { + ADDRESSES_PROVIDER = provider_; + } + + function setReserve(address asset, uint16 id, address aToken, address variableDebtToken) external { + if (!listed[asset]) { + listed[asset] = true; + reserves.push(asset); + } + reserveData[asset] = AaveV3LikeTypes.ReserveData({ + configurationData: 0, + liquidityIndex: 1e27, + currentLiquidityRate: 0, + variableBorrowIndex: 1e27, + currentVariableBorrowRate: 0, + currentStableBorrowRate: 0, + lastUpdateTimestamp: 0, + id: id, + aTokenAddress: aToken, + stableDebtTokenAddress: address(0), + variableDebtTokenAddress: variableDebtToken, + interestRateStrategyAddress: address(0), + accruedToTreasury: 0, + unbacked: 0, + isolationModeTotalDebt: 0 + }); + } + + function setAccruedToTreasury(address asset, uint128 scaledAmount) external { + reserveData[asset].accruedToTreasury = scaledAmount; + } + + function setUnbacked(address asset, uint128 amount) external { + reserveData[asset].unbacked = amount; + } + + function setReserveDeficit(address asset, uint256 amount) external { + deficits[asset] = amount; + } + + function getReserveDeficit(address asset) external view returns (uint256) { + return deficits[asset]; + } + + function setUserConfig(address user, uint256 data) external { + userConfig[user].data = data; + } + + function setAccount(address user, uint256 collateral, uint256 debt, uint256 healthFactor) external { + accounts[user] = AccountData({ + totalCollateralBase: collateral, + totalDebtBase: debt, + availableBorrowsBase: collateral > debt ? collateral - debt : 0, + currentLiquidationThreshold: 8000, + ltv: 7000, + healthFactor: healthFactor + }); + } + + function borrow(address, uint256 amount, uint256, uint16, address onBehalfOf) external override { + require(amount != 0, "forced borrow failure"); + IAaveV3HorizonOracle activeOracle = + IAaveV3HorizonOracle(ResearchAddressesProvider(ADDRESSES_PROVIDER).getPriceOracle()); + for (uint256 i; i < reserves.length; ++i) { + activeOracle.getAssetPrice(reserves[i]); + } + accounts[onBehalfOf].totalDebtBase += amount; + } + + function withdraw(address, uint256 amount, address) external pure override returns (uint256) { + return amount; + } + + function liquidationCall(address, address, address, uint256, bool) external pure override {} + + function flashLoan( + address, + address[] calldata, + uint256[] calldata, + uint256[] calldata, + address, + bytes calldata, + uint16 + ) external pure override {} + + function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external override { + uint256 bit = 1 << (reserveData[asset].id * 2 + 1); + if (useAsCollateral) { + userConfig[msg.sender].data |= bit; + } else { + userConfig[msg.sender].data &= ~bit; + } + } + + function setUserEMode(uint8) external pure override {} + + function finalizeTransfer(address, address, address, uint256, uint256, uint256) external pure override {} + + function getUserAccountData(address user) + external + view + override + returns ( + uint256 totalCollateralBase, + uint256 totalDebtBase, + uint256 availableBorrowsBase, + uint256 currentLiquidationThreshold, + uint256 ltv, + uint256 healthFactor + ) + { + AccountData memory a = accounts[user]; + return ( + a.totalCollateralBase, + a.totalDebtBase, + a.availableBorrowsBase, + a.currentLiquidationThreshold, + a.ltv, + a.healthFactor + ); + } + + function getUserConfiguration(address user) + external + view + override + returns (AaveV3LikeTypes.UserConfigurationMap memory) + { + return userConfig[user]; + } + + function getReserveData(address asset) external view override returns (AaveV3LikeTypes.ReserveData memory) { + return reserveData[asset]; + } + + function getReservesList() external view override returns (address[] memory) { + return reserves; + } +} + + contract ResearchBundle { + function priceThenBorrow( + ResearchPriceSource source, + int256 answer, + ResearchPool pool, + address asset, + address user + ) external { + source.setAnswer(answer); + pool.borrow(asset, 1, 2, 0, user); + } + + function priceBorrowRestore( + ResearchPriceSource source, + int256 temporaryAnswer, + int256 restoredAnswer, + ResearchPool pool, + address asset, + address user + ) external { + source.setAnswer(temporaryAnswer); + pool.borrow(asset, 1, 2, 0, user); + source.setAnswer(restoredAnswer); + } + + function sourceBorrowRestore( + ResearchOracle oracle, + address oracleAsset, + address temporarySource, + address restoredSource, + ResearchPool pool, + address borrowAsset, + address user + ) external { + _setSource(oracle, oracleAsset, temporarySource); + pool.borrow(borrowAsset, 1, 2, 0, user); + _setSource(oracle, oracleAsset, restoredSource); + } + + function providerThenBorrow( + ResearchAddressesProvider provider, + address newOracle, + ResearchPool pool, + address borrowAsset, + address user + ) external { + provider.setPriceOracle(newOracle); + pool.borrow(borrowAsset, 1, 2, 0, user); + } + + function providerBorrowRestore( + ResearchAddressesProvider provider, + address temporaryOracle, + address restoredOracle, + ResearchPool pool, + address borrowAsset, + address user + ) external { + provider.setPriceOracle(temporaryOracle); + pool.borrow(borrowAsset, 1, 2, 0, user); + provider.setPriceOracle(restoredOracle); + } + + function seizeThenPoolCall( + ResearchAccountingToken underlying, + address aToken, + address recipient, + uint256 amount, + ResearchPool pool, + address borrowAsset, + address user + ) external { + underlying.seize(aToken, recipient, amount); + pool.borrow(borrowAsset, 1, 2, 0, user); + } + + function seizePoolCallRestore( + ResearchAccountingToken underlying, + address aToken, + address recipient, + uint256 amount, + ResearchPool pool, + address borrowAsset, + address user + ) external { + underlying.seize(aToken, recipient, amount); + pool.borrow(borrowAsset, 1, 2, 0, user); + underlying.seize(recipient, aToken, amount); + } + + function twoBorrows(ResearchPool pool, address asset, address user) external { + pool.borrow(asset, 1, 2, 0, user); + pool.borrow(asset, 1, 2, 0, user); + } + + function successfulThenFailedBorrow(ResearchPool pool, address asset, address user) external { + pool.borrow(asset, 1, 2, 0, user); + (bool ok,) = address(pool).call(abi.encodeCall(ResearchPool.borrow, (asset, 0, 2, 0, user))); + require(!ok, "borrow unexpectedly succeeded"); + } + + function _setSource(ResearchOracle oracle, address asset, address source) internal { + address[] memory assets = new address[](1); + assets[0] = asset; + address[] memory sources = new address[](1); + sources[0] = source; + oracle.setAssetSources(assets, sources); + } + } + + /// @notice Research-only trace probe. A successful Pool call selects TxEnd; the probe then checks + /// whether getAllCallInputs also returns a caught, reverted call with the same selector. + contract SuccessfulCallTraceAssertion is AaveV3HorizonHelpers { + address internal immutable POOL; + + constructor(address pool_) { + POOL = pool_; + } + + function triggers() external view override { + registerTxEndTrigger(this.assertOnlySuccessfulBorrowSeen.selector); + } + + function assertOnlySuccessfulBorrowSeen() external view { + require( + ph.getAllCallInputs(POOL, IAaveV3LikePool.borrow.selector).length == 1, + "research: reverted call included" + ); + } + } + + contract AaveV3AdversarialResearchTest is Test, CredibleTest { + uint256 internal constant MAX_RESERVES = 8; + uint256 internal constant ORACLE_TOLERANCE_BPS = 100; + int256 internal constant PRICE = 100_00000000; + + address internal alice = makeAddr("alice"); + address internal recipient = makeAddr("recipient"); + + ResearchAccountingToken internal debtAsset; + ResearchAccountingToken internal collateralAsset; + ResearchAccountingToken internal debtAToken; + ResearchAccountingToken internal collateralAToken; + ResearchAccountingToken internal variableDebtToken; + ResearchFallbackOracle internal fallbackOracle; + ResearchOracle internal oracle; + ResearchOracle internal secondOracle; + ResearchAddressesProvider internal provider; + ResearchPriceSource internal debtSource; + ResearchPriceSource internal collateralSource; + ResearchPriceSource internal temporarySource; + ResearchPool internal pool; + ResearchBundle internal bundle; + + function setUp() public { + debtAsset = new ResearchAccountingToken(18); + collateralAsset = new ResearchAccountingToken(18); + debtAToken = new ResearchAccountingToken(18); + collateralAToken = new ResearchAccountingToken(18); + variableDebtToken = new ResearchAccountingToken(18); + fallbackOracle = new ResearchFallbackOracle(); + oracle = new ResearchOracle(address(fallbackOracle)); + secondOracle = new ResearchOracle(address(fallbackOracle)); + provider = new ResearchAddressesProvider(address(oracle)); + debtSource = new ResearchPriceSource(PRICE); + collateralSource = new ResearchPriceSource(PRICE); + temporarySource = new ResearchPriceSource(2 * PRICE); + pool = new ResearchPool(address(provider)); + bundle = new ResearchBundle(); + + _setSources(oracle, address(debtSource), address(collateralSource)); + _setSources(secondOracle, address(debtSource), address(collateralSource)); + pool.setReserve(address(debtAsset), 0, address(debtAToken), address(variableDebtToken)); + pool.setReserve(address(collateralAsset), 1, address(collateralAToken), address(0)); + pool.setUserConfig(alice, (1 << 0) | (1 << 3)); + pool.setAccount(alice, 200 ether, 100 ether, 2 ether); + + debtAToken.mint(alice, 1_000 ether); + debtAsset.mint(address(debtAToken), 600 ether); + variableDebtToken.mint(alice, 400 ether); + collateralAToken.mint(alice, 1_000 ether); + collateralAsset.mint(address(collateralAToken), 1_000 ether); + } + + function testBackingHonestPoolTransactionPasses() public { + _armBacking(); + pool.borrow(address(debtAsset), 1, 2, 0, alice); + } + + function testBackingSameSeizureTripsWhenTransactionAlsoTouchesPool() public { + _armBacking(); + vm.expectRevert(bytes("AaveV3Horizon: reserve backing deficit")); + bundle.seizeThenPoolCall( + collateralAsset, address(collateralAToken), recipient, 1 ether, pool, address(debtAsset), alice + ); + } + + /// @dev The upstream liability is aToken supply plus scaled accrued treasury; the assertion omits the latter. + function testBackingOmittedTreasuryClaimPassesDespiteEconomicShortfall() public { + pool.setAccruedToTreasury(address(collateralAsset), uint128(10 ether)); + + _armBacking(); + pool.borrow(address(debtAsset), 1, 2, 0, alice); + } + + function testBackingDeficitHasCorrectPositiveSign() public { + collateralAsset.seize(address(collateralAToken), recipient, 10 ether); + pool.setReserveDeficit(address(collateralAsset), 10 ether); + + _armBacking(); + pool.borrow(address(debtAsset), 1, 2, 0, alice); + } + + /// @dev PostTx-only backing accepts a temporary transaction-intermediate custody shortfall. + function testBackingTemporarySeizureRestoredBeforeTxEndPasses() public { + _armBacking(); + bundle.seizePoolCallRestore( + collateralAsset, address(collateralAToken), recipient, 1 ether, pool, address(debtAsset), alice + ); + } + + function testOracleStableBorrowPasses() public { + _armOracle(); + pool.borrow(address(debtAsset), 1, 2, 0, alice); + } + + function testOracleMultipleStableCallsPass() public { + _armOracle(); + bundle.twoBorrows(pool, address(debtAsset), alice); + } + + function testOraclePersistentPriceMutationTrips() public { + _armOracle(); + vm.expectRevert(bytes("AaveV3Horizon: consumed oracle price deviated")); + bundle.priceThenBorrow(collateralSource, 2 * PRICE, pool, address(debtAsset), alice); + } + + function testOracleTemporaryPriceManipulationAndRestoreTrips() public { + _armOracle(); + vm.expectRevert(bytes("AaveV3Horizon: consumed oracle price deviated")); + bundle.priceBorrowRestore(collateralSource, 2 * PRICE, PRICE, pool, address(debtAsset), alice); + } + + function testOracleTemporarySourceSwapAndRestoreTrips() public { + _armOracle(); + vm.expectRevert(bytes("AaveV3Horizon: reserve oracle source changed during transaction")); + bundle.sourceBorrowRestore( + oracle, + address(collateralAsset), + address(temporarySource), + address(collateralSource), + pool, + address(debtAsset), + alice + ); + } + + function testOraclePermanentProviderSwitchToExistingOracleTrips() public { + _armOracle(); + vm.expectRevert(bytes("AaveV3Horizon: provider oracle changed during transaction")); + bundle.providerThenBorrow(provider, address(secondOracle), pool, address(debtAsset), alice); + } + + function testOracleTemporaryProviderSwitchAndRestoreTrips() public { + _armOracle(); + vm.expectRevert(bytes("AaveV3Horizon: Pool consumed a different oracle")); + bundle.providerBorrowRestore( + provider, address(secondOracle), address(oracle), pool, address(debtAsset), alice + ); + } + + /// @dev This models a normal feed update bundled by an automation/governance transaction. + function testOracleLegitimateLargePriceUpdateBundledWithBorrowTrips() public { + _armOracle(); + vm.expectRevert(bytes("AaveV3Horizon: consumed oracle price deviated")); + bundle.priceThenBorrow(collateralSource, 102_00000000, pool, address(debtAsset), alice); + } + + function testGetAllCallInputsExcludesCaughtRevertedBorrow() public { + bytes memory createData = + abi.encodePacked(type(SuccessfulCallTraceAssertion).creationCode, abi.encode(address(pool))); + cl.assertion( + address(pool), createData, SuccessfulCallTraceAssertion.assertOnlySuccessfulBorrowSeen.selector + ); + + bundle.successfulThenFailedBorrow(pool, address(debtAsset), alice); + } + + /// @dev The production wrapper creates its suite in constructor initcode. The assertion runtime + /// currently cannot call that child contract when registering monitored selectors. + function testProductionOperationSafetyBundleChildSuiteIsUnavailable() public { + bytes memory createData = abi.encodePacked( + type(AaveV3HorizonOperationSafetyAssertion).creationCode, abi.encode(address(pool), address(provider)) + ); + cl.assertion(address(pool), createData, LendingBaseAssertion.assertOperationSafety.selector); + + vm.expectRevert(); + pool.borrow(address(debtAsset), 1, 2, 0, alice); + } + + function _armBacking() internal { + bytes memory createData = abi.encodePacked( + type(AaveV3HorizonReserveBackingAssertion).creationCode, abi.encode(address(pool), _assets(), 0) + ); + cl.assertion(address(pool), createData, AaveV3HorizonReserveBackingAssertion.assertReserveBacking.selector); + } + + function _armOracle() internal { + AaveV3HorizonOracleAssertion.AssetPolicy[] memory policies = new AaveV3HorizonOracleAssertion + .AssetPolicy[](2); + policies[0] = + AaveV3HorizonOracleAssertion.AssetPolicy({ + asset: address(debtAsset), deviationBps: ORACLE_TOLERANCE_BPS + }); + policies[1] = AaveV3HorizonOracleAssertion.AssetPolicy({ + asset: address(collateralAsset), deviationBps: ORACLE_TOLERANCE_BPS + }); + bytes memory createData = abi.encodePacked( + type(AaveV3HorizonOracleAssertion).creationCode, + abi.encode(address(pool), address(provider), MAX_RESERVES, policies) + ); + cl.assertion( + address(pool), createData, AaveV3HorizonOracleAssertion.assertConsumedOraclePricesSafe.selector + ); + } + + function _assets() internal view returns (address[] memory assets) { + assets = new address[](2); + assets[0] = address(debtAsset); + assets[1] = address(collateralAsset); + } + + function _setSources(ResearchOracle targetOracle, address debtSource_, address collateralSource_) internal { + address[] memory assets = _assets(); + address[] memory sources = new address[](2); + sources[0] = debtSource_; + sources[1] = collateralSource_; + targetOracle.setAssetSources(assets, sources); + } + } diff --git a/examples/aave/test/AaveV3HorizonOracleAssertion.t.sol b/examples/aave/test/AaveV3HorizonOracleAssertion.t.sol new file mode 100644 index 0000000..c7c1c32 --- /dev/null +++ b/examples/aave/test/AaveV3HorizonOracleAssertion.t.sol @@ -0,0 +1,403 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {CredibleTest} from "credible-std/CredibleTest.sol"; +import {AaveV3HorizonOracleAssertion} from "../src/AaveV3HorizonOracleAssertion.sol"; + +contract OracleGuardSource { + int256 internal answer; + + constructor(int256 answer_) { + answer = answer_; + } + + function setAnswer(int256 answer_) external { + answer = answer_; + } + + function latestAnswer() external view returns (int256) { + return answer; + } +} + +contract OracleGuardFallback { + mapping(address => uint256) internal prices; + + function setPrice(address asset, uint256 price) external { + prices[asset] = price; + } + + function getAssetPrice(address asset) external view returns (uint256) { + return prices[asset]; + } +} + +/// @dev Storage order intentionally matches the pinned AaveOracle layout: +/// assetsSources mapping at slot 0 and fallback oracle at slot 1. +contract OracleGuardOracle { + mapping(address => address) internal assetsSources; + address internal fallbackOracle; + + constructor(address fallbackOracle_) { + fallbackOracle = fallbackOracle_; + } + + function setSource(address asset, address source) external { + assetsSources[asset] = source; + } + + function setFallbackOracle(address fallbackOracle_) external { + fallbackOracle = fallbackOracle_; + } + + function getSourceOfAsset(address asset) external view returns (address) { + return assetsSources[asset]; + } + + function getFallbackOracle() external view returns (address) { + return fallbackOracle; + } + + function getAssetPrice(address asset) external view returns (uint256) { + address source = assetsSources[asset]; + if (source == address(0)) { + return OracleGuardFallback(fallbackOracle).getAssetPrice(asset); + } + + int256 answer = OracleGuardSource(source).latestAnswer(); + if (answer <= 0) { + return OracleGuardFallback(fallbackOracle).getAssetPrice(asset); + } + return uint256(answer); + } +} + +contract OracleGuardProvider { + address internal oracle; + + constructor(address oracle_) { + oracle = oracle_; + } + + function setPriceOracle(address oracle_) external { + oracle = oracle_; + } + + function getPriceOracle() external view returns (address) { + return oracle; + } +} + +interface IOracleGuardFlashReceiver { + function executeOperation() external; +} + +contract OracleGuardPool { + OracleGuardProvider internal immutable provider; + address[] internal assets; + + constructor(OracleGuardProvider provider_, address[] memory assets_) { + provider = provider_; + assets = assets_; + } + + function addAsset(address asset) external { + assets.push(asset); + } + + function getReservesList() external view returns (address[] memory) { + return assets; + } + + function borrow(address, uint256 amount, uint256, uint16, address) external { + require(amount != 0, "zero borrow"); + _consumePrices(); + require(amount != type(uint256).max, "forced post-price revert"); + } + + function withdraw(address, uint256 amount, address) external returns (uint256) { + _consumePrices(); + return amount; + } + + function liquidationCall(address, address, address, uint256, bool) external { + _consumePrices(); + } + + function setUserUseReserveAsCollateral(address, bool) external { + _consumePrices(); + } + + function setUserEMode(uint8) external { + _consumePrices(); + } + + function finalizeTransfer(address, address, address, uint256, uint256, uint256) external { + _consumePrices(); + } + + function flashLoan( + address receiverAddress, + address[] calldata, + uint256[] calldata, + uint256[] calldata, + address, + bytes calldata, + uint16 + ) external { + IOracleGuardFlashReceiver(receiverAddress).executeOperation(); + _consumePrices(); + } + + function _consumePrices() internal view { + OracleGuardOracle oracle = OracleGuardOracle(provider.getPriceOracle()); + for (uint256 i; i < assets.length; ++i) { + oracle.getAssetPrice(assets[i]); + } + } +} + +contract OracleGuardFlashReceiver is IOracleGuardFlashReceiver { + OracleGuardSource internal immutable source; + int256 internal immutable temporaryAnswer; + + constructor(OracleGuardSource source_, int256 temporaryAnswer_) { + source = source_; + temporaryAnswer = temporaryAnswer_; + } + + function executeOperation() external { + source.setAnswer(temporaryAnswer); + } +} + +contract OracleGuardBundle { + function priceBorrowRestore( + OracleGuardSource source, + int256 temporaryAnswer, + int256 restoredAnswer, + OracleGuardPool pool, + address asset + ) external { + source.setAnswer(temporaryAnswer); + pool.borrow(asset, 1, 2, 0, address(this)); + source.setAnswer(restoredAnswer); + } + + function sourceBorrowRestore( + OracleGuardOracle oracle, + address asset, + address temporarySource, + address restoredSource, + OracleGuardPool pool + ) external { + oracle.setSource(asset, temporarySource); + pool.borrow(asset, 1, 2, 0, address(this)); + oracle.setSource(asset, restoredSource); + } + + function fallbackBorrowRestore( + OracleGuardOracle oracle, + address temporaryFallback, + address restoredFallback, + OracleGuardPool pool, + address asset + ) external { + oracle.setFallbackOracle(temporaryFallback); + pool.borrow(asset, 1, 2, 0, address(this)); + oracle.setFallbackOracle(restoredFallback); + } + + function providerBorrowRestore( + OracleGuardProvider provider, + address temporaryOracle, + address restoredOracle, + OracleGuardPool pool, + address asset + ) external { + provider.setPriceOracle(temporaryOracle); + pool.borrow(asset, 1, 2, 0, address(this)); + provider.setPriceOracle(restoredOracle); + } + + function twoBorrows(OracleGuardPool pool, address asset) external { + pool.borrow(asset, 1, 2, 0, address(this)); + pool.borrow(asset, 1, 2, 0, address(this)); + } + + function successfulThenCaughtManipulatedBorrow( + OracleGuardPool pool, + OracleGuardSource source, + int256 temporaryAnswer, + int256 restoredAnswer, + address asset + ) external { + pool.borrow(asset, 1, 2, 0, address(this)); + source.setAnswer(temporaryAnswer); + (bool ok,) = + address(pool).call(abi.encodeCall(OracleGuardPool.borrow, (asset, type(uint256).max, 2, 0, address(this)))); + require(!ok, "forced borrow unexpectedly succeeded"); + source.setAnswer(restoredAnswer); + } + + function flashManipulateRestore( + OracleGuardPool pool, + OracleGuardFlashReceiver receiver, + OracleGuardSource source, + int256 restoredAnswer, + address asset + ) external { + address[] memory assets = new address[](1); + assets[0] = asset; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1; + uint256[] memory modes = new uint256[](1); + modes[0] = 2; + + pool.flashLoan(address(receiver), assets, amounts, modes, address(this), "", 0); + source.setAnswer(restoredAnswer); + } +} + +contract AaveV3HorizonOracleAssertionTest is Test, CredibleTest { + uint256 internal constant MAX_TRACE_CALLS = 16; + uint256 internal constant DEVIATION_BPS = 100; + int256 internal constant PRICE = 100_00000000; + + address internal asset0 = makeAddr("asset0"); + address internal asset1 = makeAddr("asset1"); + + OracleGuardSource internal source0; + OracleGuardSource internal source1; + OracleGuardSource internal temporarySource; + OracleGuardFallback internal fallbackOracle; + OracleGuardFallback internal secondFallbackOracle; + OracleGuardOracle internal oracle; + OracleGuardOracle internal secondOracle; + OracleGuardProvider internal provider; + OracleGuardPool internal pool; + OracleGuardBundle internal bundle; + + function setUp() public { + source0 = new OracleGuardSource(PRICE); + source1 = new OracleGuardSource(PRICE); + temporarySource = new OracleGuardSource(2 * PRICE); + fallbackOracle = new OracleGuardFallback(); + secondFallbackOracle = new OracleGuardFallback(); + oracle = new OracleGuardOracle(address(fallbackOracle)); + secondOracle = new OracleGuardOracle(address(fallbackOracle)); + provider = new OracleGuardProvider(address(oracle)); + + oracle.setSource(asset0, address(source0)); + oracle.setSource(asset1, address(source1)); + secondOracle.setSource(asset0, address(source0)); + secondOracle.setSource(asset1, address(source1)); + + address[] memory assets = new address[](2); + assets[0] = asset0; + assets[1] = asset1; + pool = new OracleGuardPool(provider, assets); + bundle = new OracleGuardBundle(); + } + + function testStableBorrowPassesBelowThreeMillionGas() public { + _arm(MAX_TRACE_CALLS); + pool.borrow(asset0, 1, 2, 0, address(this)); + } + + function testStableMulticallPassesBelowThreeMillionGas() public { + _arm(MAX_TRACE_CALLS); + bundle.twoBorrows(pool, asset0); + } + + function testCaughtRevertedPriceConsumptionIsExcluded() public { + _arm(MAX_TRACE_CALLS); + bundle.successfulThenCaughtManipulatedBorrow(pool, source1, 2 * PRICE, PRICE, asset0); + } + + function testTemporaryPriceManipulationAndRestoreTrips() public { + _arm(MAX_TRACE_CALLS); + vm.expectRevert(bytes("AaveV3Horizon: consumed oracle price deviated")); + bundle.priceBorrowRestore(source1, 2 * PRICE, PRICE, pool, asset0); + } + + function testPriceMovementInsideConfiguredTolerancePasses() public { + _arm(MAX_TRACE_CALLS); + bundle.priceBorrowRestore(source1, 100_50000000, PRICE, pool, asset0); + } + + function testTemporarySourceSwapAndRestoreTrips() public { + _arm(MAX_TRACE_CALLS); + vm.expectRevert(bytes("AaveV3Horizon: reserve oracle source changed during transaction")); + bundle.sourceBorrowRestore(oracle, asset1, address(temporarySource), address(source1), pool); + } + + function testTemporaryFallbackSwapAndRestoreTrips() public { + _arm(MAX_TRACE_CALLS); + vm.expectRevert(bytes("AaveV3Horizon: fallback oracle changed during transaction")); + bundle.fallbackBorrowRestore(oracle, address(secondFallbackOracle), address(fallbackOracle), pool, asset0); + } + + function testTemporaryProviderSwapAndRestoreTrips() public { + _arm(MAX_TRACE_CALLS); + vm.expectRevert(bytes("AaveV3Horizon: Pool consumed a different oracle")); + bundle.providerBorrowRestore(provider, address(secondOracle), address(oracle), pool, asset0); + } + + function testFlashLoanCallbackManipulationAndRestoreTrips() public { + OracleGuardFlashReceiver receiver = new OracleGuardFlashReceiver(source1, 2 * PRICE); + + _arm(MAX_TRACE_CALLS); + vm.expectRevert(bytes("AaveV3Horizon: consumed oracle price deviated")); + bundle.flashManipulateRestore(pool, receiver, source1, PRICE, asset0); + } + + function testIncompleteAssetConfigurationFailsClosed() public { + address asset2 = makeAddr("asset2"); + OracleGuardSource source2 = new OracleGuardSource(PRICE); + oracle.setSource(asset2, address(source2)); + pool.addAsset(asset2); + + _arm(MAX_TRACE_CALLS); + vm.expectRevert(bytes("AaveV3Horizon: unrecognized Pool oracle price path")); + pool.borrow(asset0, 1, 2, 0, address(this)); + } + + function testIncompleteAssetConfigurationWithSharedSourceFailsClosed() public { + address asset2 = makeAddr("asset2"); + oracle.setSource(asset2, address(source1)); + pool.addAsset(asset2); + + _arm(MAX_TRACE_CALLS); + vm.expectRevert(bytes("AaveV3Horizon: unrecognized Pool oracle price path")); + pool.borrow(asset0, 1, 2, 0, address(this)); + } + + function testTraceCallBoundFailsClosed() public { + _arm(1); + vm.expectRevert(bytes("AaveV3Horizon: too many oracle calls")); + pool.borrow(asset0, 1, 2, 0, address(this)); + } + + /// @dev Same-transaction comparison cannot detect a baseline already corrupted before PreTx. + function testPreExistingManipulationRemainsOutOfScope() public { + source1.setAnswer(2 * PRICE); + + _arm(MAX_TRACE_CALLS); + pool.borrow(asset0, 1, 2, 0, address(this)); + } + + function _arm(uint256 maxTraceCalls) internal { + AaveV3HorizonOracleAssertion.AssetPolicy[] memory policies = new AaveV3HorizonOracleAssertion.AssetPolicy[](2); + policies[0] = AaveV3HorizonOracleAssertion.AssetPolicy({asset: asset0, deviationBps: DEVIATION_BPS}); + policies[1] = AaveV3HorizonOracleAssertion.AssetPolicy({asset: asset1, deviationBps: DEVIATION_BPS}); + + bytes memory createData = abi.encodePacked( + type(AaveV3HorizonOracleAssertion).creationCode, + abi.encode(address(pool), address(provider), maxTraceCalls, policies) + ); + cl.assertion(address(pool), createData, AaveV3HorizonOracleAssertion.assertConsumedOraclePricesSafe.selector); + } +} diff --git a/examples/aave/test/AaveV3OperationBoundaryResearch.t.sol b/examples/aave/test/AaveV3OperationBoundaryResearch.t.sol new file mode 100644 index 0000000..070807a --- /dev/null +++ b/examples/aave/test/AaveV3OperationBoundaryResearch.t.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {CredibleTest} from "credible-std/CredibleTest.sol"; +import {ForkUtils} from "credible-std/utils/ForkUtils.sol"; +import {PhEvm} from "credible-std/PhEvm.sol"; +import {ILendingProtectionSuite} from "credible-std/protection/lending/ILendingProtectionSuite.sol"; +import { + LendingBaseAssertion, + LendingProtectionSuiteBase +} from "credible-std/protection/lending/LendingBaseAssertion.sol"; + +interface IBoundaryPool { + function finalizeTransfer( + address asset, + address from, + address to, + uint256 amount, + uint256 balanceFromBefore, + uint256 balanceToBefore + ) external; + + function healthOf(address account) external view returns (int256); +} + +/// @notice A minimal Pool that can either change health inside finalizeTransfer or merely validate +/// state already changed by its aToken caller. +contract BoundaryPool is IBoundaryPool { + mapping(address => int256) internal health; + address internal pendingAccount; + int256 internal pendingHealth; + bool internal applyPending; + + function setHealth(address account, int256 value) external { + health[account] = value; + } + + function setPending(address account, int256 value, bool applyInside) external { + pendingAccount = account; + pendingHealth = value; + applyPending = applyInside; + } + + function finalizeTransfer(address, address, address, uint256, uint256, uint256) external { + if (applyPending) { + health[pendingAccount] = pendingHealth; + } + } + + function healthOf(address account) external view returns (int256) { + return health[account]; + } +} + +/// @notice Models AToken._transfer: balances change before Pool.finalizeTransfer is entered. +contract BoundaryAToken { + function transferThenFinalize(BoundaryPool pool, address from, address to, int256 healthAfterTransfer) external { + pool.setHealth(from, healthAfterTransfer); + pool.finalizeTransfer(address(this), from, to, 1, 1, 0); + } +} + +/// @notice Flat research suite, avoiding the production child-contract runtime issue so this test +/// isolates the pre-call boundary selected for finalizeTransfer. +contract BoundaryTransferAssertion is LendingProtectionSuiteBase, LendingBaseAssertion { + address internal immutable POOL; + + constructor(address pool_) { + POOL = pool_; + } + + function _suite() internal view override returns (ILendingProtectionSuite) { + return ILendingProtectionSuite(address(this)); + } + + function getMonitoredSelectors() external pure override returns (bytes4[] memory selectors) { + selectors = new bytes4[](1); + selectors[0] = IBoundaryPool.finalizeTransfer.selector; + } + + function decodeOperation(TriggeredCall calldata triggered) + external + pure + override + returns (OperationContext memory operation) + { + (, address from,,,,) = abi.decode(triggered.input[4:], (address, address, address, uint256, uint256, uint256)); + operation.selector = triggered.selector; + operation.caller = triggered.caller; + operation.kind = OperationKind.TransferCollateral; + operation.account = from; + operation.reducesEffectiveCollateral = true; + } + + function shouldCheckPostOperationSolvency(OperationContext calldata operation) + external + pure + override + returns (bool) + { + return operation.account != address(0) && operation.reducesEffectiveCollateral; + } + + function getAccountSnapshot(address account, PhEvm.ForkId calldata fork) + external + view + override + returns (AccountSnapshot memory snapshot) + { + int256 health = abi.decode(_viewAt(POOL, abi.encodeCall(IBoundaryPool.healthOf, (account)), fork), (int256)); + snapshot.solvency.isSolvent = health >= 0; + snapshot.solvency.metricName = "health"; + snapshot.solvency.metric = health; + snapshot.solvency.threshold = 0; + } + + function getAccountState(address, PhEvm.ForkId calldata) external pure override returns (AccountState memory) {} + + function getAccountBalances(address, PhEvm.ForkId calldata) + external + pure + override + returns (AccountBalance[] memory) + {} + + function evaluateSolvency(AccountState calldata, AccountBalance[] calldata, PhEvm.ForkId calldata) + external + pure + override + returns (SolvencyState memory) + {} + + function _viewFailureMessage() + internal + pure + override(ForkUtils, LendingProtectionSuiteBase) + returns (string memory) + { + return "boundary research staticcall failed"; + } +} + +contract AaveV3OperationBoundaryResearchTest is Test, CredibleTest { + BoundaryPool internal pool; + BoundaryAToken internal aToken; + address internal alice = makeAddr("alice"); + address internal bob = makeAddr("bob"); + + function setUp() public { + pool = new BoundaryPool(); + aToken = new BoundaryAToken(); + pool.setHealth(alice, 1); + } + + /// @dev Control: a health change made by finalizeTransfer itself is observed and rejected. + function testHealthBreakInsideFinalizeTransferTrips() public { + pool.setPending(alice, -1, true); + _arm(); + + vm.expectRevert(); + pool.finalizeTransfer(address(aToken), alice, bob, 1, 1, 0); + } + + /// @dev Actual AToken ordering: the assertion's "pre-call" snapshot is already insolvent, so + /// LendingBaseAssertion deliberately skips the post-operation check. + function testHealthBreakBeforeFinalizeTransferIsSkipped() public { + pool.setPending(alice, -1, false); + _arm(); + + aToken.transferThenFinalize(pool, alice, bob, -1); + assertEq(pool.healthOf(alice), -1); + } + + function _arm() internal { + bytes memory createData = + abi.encodePacked(type(BoundaryTransferAssertion).creationCode, abi.encode(address(pool))); + cl.assertion(address(pool), createData, LendingBaseAssertion.assertOperationSafety.selector); + } +} diff --git a/src/protection/lending/examples/AaveV3LikeInterfaces.sol b/src/protection/lending/examples/AaveV3LikeInterfaces.sol index 5cb4a70..ce633ba 100644 --- a/src/protection/lending/examples/AaveV3LikeInterfaces.sol +++ b/src/protection/lending/examples/AaveV3LikeInterfaces.sol @@ -47,6 +47,16 @@ interface IAaveV3LikePool { bool receiveAToken ) external; + function flashLoan( + address receiverAddress, + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata interestRateModes, + address onBehalfOf, + bytes calldata params, + uint16 referralCode + ) external; + function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external; function setUserEMode(uint8 categoryId) external; From b658045b890210a62632188306d8379a27dce2b7 Mon Sep 17 00:00:00 2001 From: makemake Date: Fri, 31 Jul 2026 19:12:00 +0200 Subject: [PATCH 2/9] feat(aave): add v4 protection assertions --- .../src/AaveV4ExternalCollateralHelpers.sol | 150 ++++ ...rnalCollateralTransferabilityAssertion.sol | 217 +++++ .../src/AaveV4HubFlowRateCircuitBreaker.sol | 168 ++++ examples/aave/src/AaveV4Interfaces.sol | 11 + .../src/AaveV4OracleConsumptionAssertion.sol | 409 ++++++++++ .../src/AaveV4OracleConsumptionHelpers.sol | 80 ++ .../src/AaveV4PTUSDGRedemptionAssertion.sol | 116 +++ .../AaveV4ExternalCollateralAssertions.t.sol | 754 ++++++++++++++++++ .../AaveV4HubFlowRateCircuitBreaker.t.sol | 174 ++++ .../AaveV4OracleConsumptionAssertion.t.sol | 717 +++++++++++++++++ 10 files changed, 2796 insertions(+) create mode 100644 examples/aave/src/AaveV4ExternalCollateralHelpers.sol create mode 100644 examples/aave/src/AaveV4ExternalCollateralTransferabilityAssertion.sol create mode 100644 examples/aave/src/AaveV4HubFlowRateCircuitBreaker.sol create mode 100644 examples/aave/src/AaveV4OracleConsumptionAssertion.sol create mode 100644 examples/aave/src/AaveV4OracleConsumptionHelpers.sol create mode 100644 examples/aave/src/AaveV4PTUSDGRedemptionAssertion.sol create mode 100644 examples/aave/test/AaveV4ExternalCollateralAssertions.t.sol create mode 100644 examples/aave/test/AaveV4HubFlowRateCircuitBreaker.t.sol create mode 100644 examples/aave/test/AaveV4OracleConsumptionAssertion.t.sol diff --git a/examples/aave/src/AaveV4ExternalCollateralHelpers.sol b/examples/aave/src/AaveV4ExternalCollateralHelpers.sol new file mode 100644 index 0000000..74b709b --- /dev/null +++ b/examples/aave/src/AaveV4ExternalCollateralHelpers.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {PhEvm} from "credible-std/PhEvm.sol"; + +import {AaveV4Helpers} from "./AaveV4Helpers.sol"; +import {IAaveV4Spoke} from "./AaveV4Interfaces.sol"; + +/// @notice ERC20 pause surface used by independently administered collateral and redemption tokens. +interface IExternalPausable { + function paused() external view returns (bool); +} + +/// @notice ether.fi timed-pause surface used by weETH. +interface IExternalTimedPausable { + function pausedUntil() external view returns (uint256); +} + +/// @notice ether.fi Blacklister surface used by weETH transfer hooks. +interface IExternalTimedBlacklist { + function blacklistedUntil(address account) external view returns (uint256); +} + +/// @notice Circle/Coinbase-style blacklist surface. +interface IExternalBlacklist { + function isBlacklisted(address account) external view returns (bool); +} + +/// @notice Tether-style blacklist surface. The capital `L` is part of the deployed ABI. +interface IExternalTetherBlacklist { + function isBlackListed(address account) external view returns (bool); +} + +/// @notice Tether Gold-style blocked-account surface. +interface IExternalBlockedAccount { + function isBlocked(address account) external view returns (bool); +} + +/// @notice AccessControl surface used by sUSDe transfer restrictions. +interface IExternalAccessControl { + function hasRole(bytes32 role, address account) external view returns (bool); +} + +/// @notice Paxos freeze surface used by USDG. +interface IExternalFrozenAccount { + function isFrozen(address account) external view returns (bool); +} + +/// @title AaveV4ExternalCollateralHelpers +/// @author Phylax Systems +/// @notice Shared call decoding and Aave exposure-scoping for external collateral assertions. +/// @dev Aave state is read only to decide whether the triggering user is increasing risk and +/// still relies on a configured collateral reserve. The protected facts themselves come +/// from independently administered token, issuer, and redemption contracts. +abstract contract AaveV4ExternalCollateralHelpers is AaveV4Helpers { + /// @notice Decodes the affected user and rejects operations that do not increase collateral risk. + /// @dev Borrow always increases debt. Withdraw and collateral-toggle paths are relevant only + /// when the user retains debt; non-collateral withdrawals are skipped. Collateral disable + /// remains checked because disabling good collateral can leave debt relying on an externally + /// frozen asset. A complete disable/exit of the frozen asset is allowed by the later + /// post-call reliance check. + function _riskIncreasingUser(address spoke, PhEvm.TriggerContext memory ctx) + internal + view + returns (address user, bool riskIncreasing) + { + bytes memory input = ph.callinputAt(ctx.callStart); + PhEvm.ForkId memory preCall = _preCall(ctx.callStart); + PhEvm.ForkId memory postCall = _postCall(ctx.callEnd); + + if (ctx.selector == IAaveV4Spoke.borrow.selector) { + (,, user) = abi.decode(_args(input), (uint256, uint256, address)); + return (user, true); + } + + if (ctx.selector == IAaveV4Spoke.withdraw.selector) { + uint256 reserveId; + (reserveId,, user) = abi.decode(_args(input), (uint256, uint256, address)); + if (!_hasDebtAt(spoke, user, postCall)) { + return (user, false); + } + return (user, _isActiveCollateralAt(spoke, reserveId, user, preCall)); + } + + if (ctx.selector == IAaveV4Spoke.setUsingAsCollateral.selector) { + uint256 reserveId; + bool usingAsCollateral; + (reserveId, usingAsCollateral, user) = abi.decode(_args(input), (uint256, bool, address)); + if (!_hasDebtAt(spoke, user, postCall)) { + return (user, false); + } + + (bool wasUsingAsCollateral,) = _spokeUserReserveStatusAt(spoke, reserveId, user, preCall); + if (wasUsingAsCollateral == usingAsCollateral) { + return (user, false); + } + + // Enabling collateral with debt can make a restricted asset part of the solvency + // calculation. Disabling is relevant only if the reserve was active before the call. + return (user, usingAsCollateral || _hasPositiveCollateralFactorAt(spoke, reserveId, user, preCall)); + } + + revert("AaveV4External: unsupported trigger"); + } + + /// @notice Returns whether Aave counts a reserve toward the user's collateral at `fork`. + /// @dev The user flag alone is insufficient: zero shares and a zero dynamic collateral factor + /// do not economically support debt. + function _isActiveCollateralAt(address spoke, uint256 reserveId, address user, PhEvm.ForkId memory fork) + internal + view + returns (bool) + { + (bool usingAsCollateral,) = _spokeUserReserveStatusAt(spoke, reserveId, user, fork); + if (!usingAsCollateral) { + return false; + } + + IAaveV4Spoke.UserPosition memory position = _spokeUserPositionAt(spoke, reserveId, user, fork); + return _hasPositiveCollateralFactorAt(spoke, reserveId, fork, position); + } + + function _hasPositiveCollateralFactorAt(address spoke, uint256 reserveId, address user, PhEvm.ForkId memory fork) + internal + view + returns (bool) + { + IAaveV4Spoke.UserPosition memory position = _spokeUserPositionAt(spoke, reserveId, user, fork); + return _hasPositiveCollateralFactorAt(spoke, reserveId, fork, position); + } + + function _hasPositiveCollateralFactorAt( + address spoke, + uint256 reserveId, + PhEvm.ForkId memory fork, + IAaveV4Spoke.UserPosition memory position + ) internal view returns (bool) { + if (position.suppliedShares == 0) { + return false; + } + + IAaveV4Spoke.DynamicReserveConfig memory config = + _spokeDynamicConfigAt(spoke, reserveId, position.dynamicConfigKey, fork); + return config.collateralFactor != 0; + } + + function _hasDebtAt(address spoke, address user, PhEvm.ForkId memory fork) internal view returns (bool) { + return _spokeAccountDataAt(spoke, user, fork).totalDebtValueRay != 0; + } +} diff --git a/examples/aave/src/AaveV4ExternalCollateralTransferabilityAssertion.sol b/examples/aave/src/AaveV4ExternalCollateralTransferabilityAssertion.sol new file mode 100644 index 0000000..26b370e --- /dev/null +++ b/examples/aave/src/AaveV4ExternalCollateralTransferabilityAssertion.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {PhEvm} from "credible-std/PhEvm.sol"; + +import { + AaveV4ExternalCollateralHelpers, + IExternalAccessControl, + IExternalBlacklist, + IExternalBlockedAccount, + IExternalPausable, + IExternalTetherBlacklist, + IExternalTimedBlacklist, + IExternalTimedPausable +} from "./AaveV4ExternalCollateralHelpers.sol"; +import {IAaveV4Spoke} from "./AaveV4Interfaces.sol"; + +/// @title AaveV4ExternalCollateralTransferabilityAssertion +/// @author Phylax Systems +/// @notice Makes an Aave v4 position reduce-only when its external collateral cannot be seized. +/// @dev Protects against an issuer or protocol independently pausing a token, blacklisting the +/// Aave Hub, blocking the Hub, or assigning it a full transfer-restriction role. Aave's +/// borrow path does not transfer the collateral token, so it can otherwise add debt while +/// the token's native transfer revert is deferred until liquidation. Repay, supply, +/// liquidation, debt-free exits, and complete removal of the impaired collateral remain open. +contract AaveV4ExternalCollateralTransferabilityAssertion is AaveV4ExternalCollateralHelpers { + enum AdapterKind { + Unsupported, + Paused, + PausedAndBlacklisted, + PausedAndBlackListed, + WeEth, + Blocked, + FullRestrictedRole + } + + struct CollateralPolicy { + uint256 reserveId; + address token; + address hub; + address statusSource; + AdapterKind adapter; + } + + // Snapshot reads are deliberately bounded to one external collateral per assertion instance. + // A five-policy Main Spoke fixture consumed about 575k assertion gas, above the current 300k + // local executor limit. Deploy one instance per reserve instead of creating an unsafe bundle. + uint256 internal constant MAX_POLICY_COUNT = 1; + bytes32 internal constant FULL_RESTRICTED_STAKER_ROLE = keccak256("FULL_RESTRICTED_STAKER_ROLE"); + + address internal immutable SPOKE; + CollateralPolicy[] internal collateralPolicies; + + /// @param spoke_ The exact Aave v4 risk Spoke adopting the assertion. + /// @param policies_ Exactly one reserve/token/Hub policy for collateral enabled on that Spoke. + /// `statusSource` is the token itself except for weETH, where it is the Blacklister. + constructor(address spoke_, CollateralPolicy[] memory policies_) { + require(spoke_ != address(0), "AaveV4Transferability: spoke zero"); + require(policies_.length == MAX_POLICY_COUNT, "AaveV4Transferability: one policy required"); + + SPOKE = spoke_; + for (uint256 i; i < policies_.length; ++i) { + CollateralPolicy memory policy = policies_[i]; + require(policy.token != address(0), "AaveV4Transferability: token zero"); + require(policy.hub != address(0), "AaveV4Transferability: Hub zero"); + require(policy.statusSource != address(0), "AaveV4Transferability: status source zero"); + require(policy.adapter != AdapterKind.Unsupported, "AaveV4Transferability: unsupported adapter"); + if (policy.adapter != AdapterKind.WeEth) { + require(policy.statusSource == policy.token, "AaveV4Transferability: status source must be token"); + } + for (uint256 j; j < i; ++j) { + require( + collateralPolicies[j].reserveId != policy.reserveId, + "AaveV4Transferability: duplicate reserve policy" + ); + } + collateralPolicies.push(policy); + } + } + + /// @notice Registers only Aave operations that can add debt or remove effective collateral. + /// @dev Function-call triggers are required to decode `onBehalfOf` and bind the check to that + /// operation's PostCall position. ERC20-change triggers would miss the failure because an + /// Aave borrow against disabled collateral does not move that collateral token. + function triggers() external view override { + registerFnCallTrigger(this.assertExternalCollateralTransferable.selector, IAaveV4Spoke.borrow.selector); + registerFnCallTrigger(this.assertExternalCollateralTransferable.selector, IAaveV4Spoke.withdraw.selector); + registerFnCallTrigger( + this.assertExternalCollateralTransferable.selector, IAaveV4Spoke.setUsingAsCollateral.selector + ); + } + + /// @notice Requires every covered collateral still supporting the affected user's debt to be transferable. + /// @dev Reads Aave only to scope exposure, then reads independent issuer state at PreCall and + /// PostTx. A failure means the triggering operation would leave more debt, or less good + /// collateral, while seizure of a relied-on external token can revert. Checking both + /// snapshots also rejects pause/blacklist changes wrapped around the Aave call in one tx. + function assertExternalCollateralTransferable() external view { + _requireAdopter(SPOKE, "AaveV4Transferability: configured Spoke is not adopter"); + + PhEvm.TriggerContext memory ctx = ph.context(); + (address user, bool riskIncreasing) = _riskIncreasingUser(SPOKE, ctx); + if (!riskIncreasing) { + return; + } + + PhEvm.ForkId memory postCall = _postCall(ctx.callEnd); + PhEvm.ForkId memory preCall = _preCall(ctx.callStart); + PhEvm.ForkId memory postTx = _postTx(); + + for (uint256 i; i < collateralPolicies.length; ++i) { + CollateralPolicy memory policy = collateralPolicies[i]; + if (!_isActiveCollateralAt(SPOKE, policy.reserveId, user, postCall)) { + continue; + } + + IAaveV4Spoke.Reserve memory reserve = _spokeReserveAt(SPOKE, policy.reserveId, postCall); + require(reserve.underlying == policy.token, "AaveV4Transferability: reserve token changed"); + require(reserve.hub == policy.hub, "AaveV4Transferability: reserve Hub changed"); + + require( + _isTransferableAt(policy, preCall), "AaveV4Transferability: collateral restricted before risk increase" + ); + require( + _isTransferableAt(policy, postTx), "AaveV4Transferability: collateral restricted at transaction end" + ); + } + } + + function collateralPolicyCount() external view returns (uint256) { + return collateralPolicies.length; + } + + function collateralPolicy(uint256 index) external view returns (CollateralPolicy memory) { + return collateralPolicies[index]; + } + + function _isTransferableAt(CollateralPolicy memory policy, PhEvm.ForkId memory fork) internal view returns (bool) { + if (policy.adapter == AdapterKind.Paused) { + return !_pausedAt(policy.token, fork); + } + + if (policy.adapter == AdapterKind.PausedAndBlacklisted) { + return !_pausedAt(policy.token, fork) + && !_readBoolAt(policy.token, abi.encodeCall(IExternalBlacklist.isBlacklisted, (policy.hub)), fork); + } + + if (policy.adapter == AdapterKind.PausedAndBlackListed) { + return !_pausedAt(policy.token, fork) + && !_readBoolAt( + policy.token, abi.encodeCall(IExternalTetherBlacklist.isBlackListed, (policy.hub)), fork + ); + } + + if (policy.adapter == AdapterKind.WeEth) { + bool indefinitePause = _pausedAt(policy.token, fork); + uint256 timedPause = _readUintAt(policy.token, abi.encodeCall(IExternalTimedPausable.pausedUntil, ()), fork); + uint256 hubBlacklist = _readUintAt( + policy.statusSource, abi.encodeCall(IExternalTimedBlacklist.blacklistedUntil, (policy.hub)), fork + ); + return !indefinitePause && timedPause < block.timestamp && hubBlacklist <= block.timestamp; + } + + if (policy.adapter == AdapterKind.Blocked) { + return !_readBoolAt(policy.token, abi.encodeCall(IExternalBlockedAccount.isBlocked, (policy.hub)), fork); + } + + if (policy.adapter == AdapterKind.FullRestrictedRole) { + return !_readBoolAt( + policy.token, + abi.encodeCall(IExternalAccessControl.hasRole, (FULL_RESTRICTED_STAKER_ROLE, policy.hub)), + fork + ); + } + + revert("AaveV4Transferability: unsupported adapter"); + } + + function _pausedAt(address target, PhEvm.ForkId memory fork) internal view returns (bool) { + return _readBoolAt(target, abi.encodeCall(IExternalPausable.paused, ()), fork); + } +} + +/// @title AaveV4EthereumMainSpokeWeETHTransferabilityAssertion +/// @notice Production-configured weETH reduce-only gate for the Ethereum Main Spoke. +/// @dev Constants were verified at Ethereum block 25,653,183. Revalidate the Spoke reserve, +/// Hub, weETH implementation, and Blacklister before adopting after any protocol upgrade. +contract AaveV4EthereumMainSpokeWeETHTransferabilityAssertion is AaveV4ExternalCollateralTransferabilityAssertion { + address public constant MAIN_SPOKE = 0x94e7A5dCbE816e498b89aB752661904E2F56c485; + address public constant CORE_HUB = 0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9; + address public constant WEETH = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee; + address public constant WEETH_BLACKLISTER = 0x5585996E7cFE95f2D99e61168B8b35C66Ff99B18; + + constructor() AaveV4ExternalCollateralTransferabilityAssertion(MAIN_SPOKE, _policies(2)) {} + + function _policies(uint256 reserveId) private pure returns (CollateralPolicy[] memory policies) { + policies = new CollateralPolicy[](1); + policies[0] = CollateralPolicy(reserveId, WEETH, CORE_HUB, WEETH_BLACKLISTER, AdapterKind.WeEth); + } +} + +/// @title AaveV4EthereumEtherFiSpokeWeETHTransferabilityAssertion +/// @notice Production-configured weETH reduce-only gate for the Ethereum ether.fi eSpoke. +/// @dev This protects the same Core Hub custody path as the Main Spoke wrapper, with reserve id 0. +contract AaveV4EthereumEtherFiSpokeWeETHTransferabilityAssertion is AaveV4ExternalCollateralTransferabilityAssertion { + address public constant ETHERFI_ESPOKE = 0xbF10BDfE177dE0336aFD7fcCF80A904E15386219; + address public constant CORE_HUB = 0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9; + address public constant WEETH = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee; + address public constant WEETH_BLACKLISTER = 0x5585996E7cFE95f2D99e61168B8b35C66Ff99B18; + + constructor() AaveV4ExternalCollateralTransferabilityAssertion(ETHERFI_ESPOKE, _policies(0)) {} + + function _policies(uint256 reserveId) private pure returns (CollateralPolicy[] memory policies) { + policies = new CollateralPolicy[](1); + policies[0] = CollateralPolicy(reserveId, WEETH, CORE_HUB, WEETH_BLACKLISTER, AdapterKind.WeEth); + } +} diff --git a/examples/aave/src/AaveV4HubFlowRateCircuitBreaker.sol b/examples/aave/src/AaveV4HubFlowRateCircuitBreaker.sol new file mode 100644 index 0000000..abaac83 --- /dev/null +++ b/examples/aave/src/AaveV4HubFlowRateCircuitBreaker.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Assertion} from "credible-std/Assertion.sol"; +import {PhEvm} from "credible-std/PhEvm.sol"; +import {AssertionSpec} from "credible-std/SpecRecorder.sol"; + +/// @title AaveV4HubFlowRateCircuitBreaker +/// @author Phylax Systems +/// @notice Shared policy for rate-limiting high-TVL assets held by an Aave v4 Hub. +/// @dev The assertion protects the Hub's external ERC20 custody rather than individual Spoke +/// selectors, so supplies, borrows, withdrawals, liquidations, sweeps, and future paths are +/// measured through the same balance signal. +/// +/// Each configured asset has two independent hard limits in each direction: +/// - rolling 24-hour NET flow as bps of the Hub balance snapshotted at window start +/// - peak 10-second-bucket net-flow rate as bps of that snapshot per second +/// +/// A 1 bps cumulative watcher is only the dispatch floor. Once dispatched, the assertion +/// rejects a transaction when EITHER the calibrated 24-hour limit or the calibrated peak-rate +/// limit is breached. The rate signal never suppresses a cumulative-flow breach. +abstract contract AaveV4HubFlowRateCircuitBreaker is Assertion { + struct FlowLimits { + uint256 inflowWindowBps; + uint256 outflowWindowBps; + uint256 inflowPeakRateBps; + uint256 outflowPeakRateBps; + } + + uint256 public constant FLOW_WINDOW = 24 hours; + uint256 public constant DISPATCH_THRESHOLD_BPS = 1; + + address public immutable HUB; + + constructor(address hub_) { + require(hub_ != address(0), "AaveV4Flow: zero hub"); + HUB = hub_; + + // inflowRate() and outflowRate() are currently experimental PhEVM precompiles. + registerAssertionSpec(AssertionSpec.Experimental); + } + + /// @notice Hard-stops excessive rolling inflow or inflow acceleration into the configured Hub. + /// @dev Invoked after the low dispatch floor is crossed. A failure means the transaction pushed + /// either 24-hour net inflow or the peak per-second inflow rate above its calibrated limit. + function assertInflowWithinRateLimits() external view { + require(ph.getAssertionAdopter() == HUB, "AaveV4Flow: configured hub is not adopter"); + + PhEvm.InflowContext memory flow = ph.inflowContext(); + PhEvm.FlowRateContext memory rate = ph.inflowRate(); + require(flow.token != address(0) && rate.token == flow.token, "AaveV4Flow: bad inflow context"); + + FlowLimits memory limits = _flowLimits(flow.token); + require(flow.currentBps <= limits.inflowWindowBps, "AaveV4Flow: 24h inflow limit"); + require(rate.peakRateBps <= limits.inflowPeakRateBps, "AaveV4Flow: inflow rate limit"); + } + + /// @notice Hard-stops excessive rolling outflow or outflow acceleration from the configured Hub. + /// @dev Invoked after the low dispatch floor is crossed. A failure means the transaction pushed + /// either 24-hour net outflow or the peak per-second outflow rate above its calibrated limit. + function assertOutflowWithinRateLimits() external view { + require(ph.getAssertionAdopter() == HUB, "AaveV4Flow: configured hub is not adopter"); + + PhEvm.OutflowContext memory flow = ph.outflowContext(); + PhEvm.FlowRateContext memory rate = ph.outflowRate(); + require(flow.token != address(0) && rate.token == flow.token, "AaveV4Flow: bad outflow context"); + + FlowLimits memory limits = _flowLimits(flow.token); + require(flow.currentBps <= limits.outflowWindowBps, "AaveV4Flow: 24h outflow limit"); + require(rate.peakRateBps <= limits.outflowPeakRateBps, "AaveV4Flow: outflow rate limit"); + } + + function _watchAsset(address token) internal view { + watchCumulativeInflow(token, DISPATCH_THRESHOLD_BPS, FLOW_WINDOW, this.assertInflowWithinRateLimits.selector); + watchCumulativeOutflow(token, DISPATCH_THRESHOLD_BPS, FLOW_WINDOW, this.assertOutflowWithinRateLimits.selector); + } + + function _inflowTrips(address token, uint256 currentBps, uint256 peakRateBps) internal pure returns (bool) { + FlowLimits memory limits = _flowLimits(token); + return currentBps > limits.inflowWindowBps || peakRateBps > limits.inflowPeakRateBps; + } + + function _outflowTrips(address token, uint256 currentBps, uint256 peakRateBps) internal pure returns (bool) { + FlowLimits memory limits = _flowLimits(token); + return currentBps > limits.outflowWindowBps || peakRateBps > limits.outflowPeakRateBps; + } + + function _flowLimits(address token) internal pure virtual returns (FlowLimits memory); +} + +/// @title AaveV4EthereumCoreHubFlowRateCircuitBreaker +/// @author Phylax Systems +/// @notice Ready-to-adopt Core Hub breaker for Aave v4's three highest-TVL Ethereum assets. +/// @dev Asset ranking comes from DefiLlama's aggregate Aave v4 token TVL on 2026-07-30: +/// WBTC ($52.35m), USDG ($30.84m), and wstETH ($29.40m). Limits are 120% of each +/// Core Hub asset's maximum observed rolling 24-hour net flow and 10-second peak flow rate +/// during Ethereum blocks 25,430,974 through 25,646,159. +contract AaveV4EthereumCoreHubFlowRateCircuitBreaker is AaveV4HubFlowRateCircuitBreaker { + address public constant CORE_HUB = 0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9; + + address public constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; + address public constant USDG = 0xe343167631d89B6Ffc58B88d6b7fB0228795491D; + address public constant WSTETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; + + constructor() AaveV4HubFlowRateCircuitBreaker(CORE_HUB) {} + + function triggers() external view virtual override { + _watchAsset(WBTC); + _watchAsset(USDG); + _watchAsset(WSTETH); + } + + function _flowLimits(address token) internal pure override returns (FlowLimits memory limits) { + if (token == WBTC) { + return + FlowLimits({ + inflowWindowBps: 1_184, outflowWindowBps: 110, inflowPeakRateBps: 48, outflowPeakRateBps: 9 + }); + } + if (token == USDG) { + return FlowLimits({ + inflowWindowBps: 5_196, outflowWindowBps: 6_438, inflowPeakRateBps: 527, outflowPeakRateBps: 154 + }); + } + if (token == WSTETH) { + return + FlowLimits({ + inflowWindowBps: 1_906, outflowWindowBps: 932, inflowPeakRateBps: 58, outflowPeakRateBps: 94 + }); + } + revert("AaveV4Flow: unsupported Core asset"); + } +} + +/// @title AaveV4EthereumPrimeHubFlowRateCircuitBreaker +/// @author Phylax Systems +/// @notice Companion breaker for top-three assets whose custody is also split into the Prime Hub. +/// @dev WBTC and wstETH are present in both Core and Prime. A flow watcher measures only its +/// assertion adopter, so this companion assertion must be adopted by Prime to avoid leaving +/// that portion of the two assets unprotected. USDG is held only by Core. +contract AaveV4EthereumPrimeHubFlowRateCircuitBreaker is AaveV4HubFlowRateCircuitBreaker { + address public constant PRIME_HUB = 0x943827DCA022D0F354a8a8c332dA1e5Eb9f9F931; + + address public constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; + address public constant WSTETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; + + constructor() AaveV4HubFlowRateCircuitBreaker(PRIME_HUB) {} + + function triggers() external view virtual override { + _watchAsset(WBTC); + _watchAsset(WSTETH); + } + + function _flowLimits(address token) internal pure override returns (FlowLimits memory limits) { + if (token == WBTC) { + return FlowLimits({ + inflowWindowBps: 2_102, outflowWindowBps: 2_367, inflowPeakRateBps: 149, outflowPeakRateBps: 178 + }); + } + if (token == WSTETH) { + return + FlowLimits({ + inflowWindowBps: 3_485, outflowWindowBps: 909, inflowPeakRateBps: 211, outflowPeakRateBps: 73 + }); + } + revert("AaveV4Flow: unsupported Prime asset"); + } +} diff --git a/examples/aave/src/AaveV4Interfaces.sol b/examples/aave/src/AaveV4Interfaces.sol index 537660f..9f64dcd 100644 --- a/examples/aave/src/AaveV4Interfaces.sol +++ b/examples/aave/src/AaveV4Interfaces.sol @@ -171,5 +171,16 @@ interface IAaveV4Spoke { /// @notice Minimal reserve-price oracle surface used by Aave v4 Spokes. interface IAaveV4Oracle { + function spoke() external view returns (address); + function decimals() external view returns (uint8); function getReservePrice(uint256 reserveId) external view returns (uint256); + function getReservesPrices(uint256[] calldata reserveIds) external view returns (uint256[] memory); + function getReserveSource(uint256 reserveId) external view returns (address); + function setReserveSource(uint256 reserveId, address source) external; +} + +/// @notice Exact price-feed surface called by the pinned Aave v4 AaveOracle. +/// @dev Aave v4 release v0.5.11 calls `latestAnswer()` and accepts only positive answers. +interface IAaveV4PriceFeed { + function latestAnswer() external view returns (int256); } diff --git a/examples/aave/src/AaveV4OracleConsumptionAssertion.sol b/examples/aave/src/AaveV4OracleConsumptionAssertion.sol new file mode 100644 index 0000000..61cd36b --- /dev/null +++ b/examples/aave/src/AaveV4OracleConsumptionAssertion.sol @@ -0,0 +1,409 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {PhEvm} from "credible-std/PhEvm.sol"; + +import {AaveV4OracleConsumptionHelpers} from "./AaveV4OracleConsumptionHelpers.sol"; +import {IAaveV4Oracle, IAaveV4Spoke} from "./AaveV4Interfaces.sol"; + +/// @title AaveV4OracleConsumptionAssertion +/// @author Phylax Systems +/// @notice Prevents an Aave v4 Spoke from consuming a same-transaction manipulated oracle price. +/// @dev The assertion: +/// - Executes once at transaction end and scans all committed nested price reads. +/// - Compares exact `AaveOracle.getReservePrice` returns with PreTx batch baselines. +/// - Maps every return through its direct configured `IPriceFeed.latestAnswer()` child. +/// - Rejects source, Spoke implementation, source proxy, and configured adapter-slot writes, +/// including writes restored before transaction end. +/// - Fails closed on incomplete reserve policy, malformed traces, unknown reserve IDs, or +/// trace limits. +/// It does not prove that a PreTx price is economically correct. Manipulation completed +/// before the protected transaction is outside this same-transaction invariant. +contract AaveV4OracleConsumptionAssertion is AaveV4OracleConsumptionHelpers { + struct ReservePolicy { + uint256 reserveId; + address asset; + address source; + uint256 deviationBps; + } + + struct ConfigSlotGuard { + address target; + bytes32 slot; + } + + uint256 internal constant MAX_POLICY_COUNT = 64; + uint256 internal constant MAX_CONFIG_GUARD_COUNT = 128; + uint256 internal constant MAX_ALLOWED_TRACE_CALLS = 512; + + address internal immutable SPOKE; + address internal immutable ORACLE; + address internal immutable EXPECTED_SPOKE_IMPLEMENTATION; + uint256 internal immutable MAX_TRACE_CALLS; + + ReservePolicy[] internal reservePolicies; + ConfigSlotGuard[] internal configSlotGuards; + + /// @param spoke_ The exact Aave v4 Spoke proxy adopting the assertion. + /// @param oracle_ The immutable AaveOracle selected by the pinned Spoke implementation. + /// @param expectedSpokeImplementation_ The ERC-1967 implementation expected at PreTx. + /// Use zero only for a verified non-proxy Spoke test/deployment. + /// @param maxTraceCalls_ Maximum matching oracle calls; exceeding it fails closed. + /// @param reservePolicies_ Complete contiguous reserve-ID policy set for the Spoke. + /// @param configSlotGuards_ Extra mutable adapter/router slots whose writes can affect prices. + constructor( + address spoke_, + address oracle_, + address expectedSpokeImplementation_, + uint256 maxTraceCalls_, + ReservePolicy[] memory reservePolicies_, + ConfigSlotGuard[] memory configSlotGuards_ + ) { + require(spoke_ != address(0), "AaveV4Oracle: spoke zero"); + require(oracle_ != address(0), "AaveV4Oracle: oracle zero"); + require(maxTraceCalls_ != 0 && maxTraceCalls_ <= MAX_ALLOWED_TRACE_CALLS, "AaveV4Oracle: bad trace limit"); + require( + reservePolicies_.length != 0 && reservePolicies_.length <= MAX_POLICY_COUNT, + "AaveV4Oracle: bad policy count" + ); + require(configSlotGuards_.length <= MAX_CONFIG_GUARD_COUNT, "AaveV4Oracle: too many config guards"); + + SPOKE = spoke_; + ORACLE = oracle_; + EXPECTED_SPOKE_IMPLEMENTATION = expectedSpokeImplementation_; + MAX_TRACE_CALLS = maxTraceCalls_; + + for (uint256 i; i < reservePolicies_.length; ++i) { + ReservePolicy memory policy = reservePolicies_[i]; + require(policy.reserveId == i, "AaveV4Oracle: policies not contiguous"); + require(policy.asset != address(0), "AaveV4Oracle: policy asset zero"); + require(policy.source != address(0), "AaveV4Oracle: policy source zero"); + require(policy.deviationBps <= BPS, "AaveV4Oracle: bad tolerance"); + for (uint256 j; j < i; ++j) { + require(reservePolicies_[j].source != policy.source, "AaveV4Oracle: source must map one reserve"); + } + reservePolicies.push(policy); + } + + for (uint256 i; i < configSlotGuards_.length; ++i) { + ConfigSlotGuard memory guard = configSlotGuards_[i]; + require(guard.target != address(0), "AaveV4Oracle: guard target zero"); + for (uint256 j; j < i; ++j) { + ConfigSlotGuard memory previous = configSlotGuards_[j]; + require( + previous.target != guard.target || previous.slot != guard.slot, + "AaveV4Oracle: duplicate config guard" + ); + } + configSlotGuards.push(guard); + } + } + + /// @notice Registers one transaction-end check for all committed V4 price consumption. + /// @dev A transaction-end trigger preserves intermediate nested call nodes and outputs while + /// avoiding one full reserve-policy scan for every multicall leg. Nested oracle calldata + /// is not assumed to be available; see `_mapPriceCallToSource`. + function triggers() external view override { + registerTxEndTrigger(this.assertConsumedOraclePricesSafe.selector); + } + + /// @notice Checks exact prices returned to successful risk-sensitive Spoke operations. + /// @dev Only committed `withdraw`, `borrow`, `liquidationCall`, collateral-disable, + /// risk-premium refresh, and dynamic-config refresh calls select the check. Price reads in + /// caught reverted calls are excluded by the successful-call trace. A failure means the + /// Spoke consumed an unconfigured, malformed, or out-of-policy intermediate price, or a + /// price-routing/configuration surface was written during the transaction. + function assertConsumedOraclePricesSafe() external view { + _requireAdopter(SPOKE, "AaveV4Oracle: configured spoke is not adopter"); + + PhEvm.TriggerCall[] memory priceCalls = + _successfulStaticCalls(ORACLE, IAaveV4Oracle.getReservePrice.selector, MAX_TRACE_CALLS + 1); + require(priceCalls.length <= MAX_TRACE_CALLS, "AaveV4Oracle: trace limit exceeded"); + + uint256 spokePriceCallCount; + for (uint256 i; i < priceCalls.length; ++i) { + if (priceCalls[i].caller == SPOKE) { + ++spokePriceCallCount; + } + } + + bool mandatoryPriceOperation; + if (spokePriceCallCount == 0) { + mandatoryPriceOperation = _hasMandatoryPriceOperation(SPOKE); + } + if (spokePriceCallCount == 0 && !mandatoryPriceOperation) { + return; + } + + PhEvm.ForkId memory preTx = _preTx(); + PhEvm.ForkId memory postTx = _postTx(); + _assertSpokeAndOracleIdentity(preTx, postTx); + _assertRoutingConfigurationUnchanged(); + require(spokePriceCallCount != 0, "AaveV4Oracle: unrecognized price path"); + + uint256[] memory baselinePrices = _loadAndValidatePreTxPolicy(preTx); + _assertConsumedPrices(priceCalls, baselinePrices); + } + + function reservePolicyCount() external view returns (uint256) { + return reservePolicies.length; + } + + function reservePolicy(uint256 index) external view returns (ReservePolicy memory) { + return reservePolicies[index]; + } + + function configSlotGuardCount() external view returns (uint256) { + return configSlotGuards.length; + } + + function configSlotGuard(uint256 index) external view returns (ConfigSlotGuard memory) { + return configSlotGuards[index]; + } + + function _assertSpokeAndOracleIdentity(PhEvm.ForkId memory preTx, PhEvm.ForkId memory postTx) internal view { + require(_spokeOracleAt(SPOKE, preTx) == ORACLE, "AaveV4Oracle: unexpected PreTx oracle"); + require(_spokeOracleAt(SPOKE, postTx) == ORACLE, "AaveV4Oracle: Spoke oracle changed"); + require(_oracleSpokeAt(ORACLE, preTx) == SPOKE, "AaveV4Oracle: oracle-Spoke mismatch"); + require(_oracleDecimalsAt(ORACLE, preTx) == ORACLE_DECIMALS, "AaveV4Oracle: wrong oracle decimals"); + + bytes32 preImplementation = ph.loadStateAt(SPOKE, ERC1967_IMPLEMENTATION_SLOT, preTx); + bytes32 postImplementation = ph.loadStateAt(SPOKE, ERC1967_IMPLEMENTATION_SLOT, postTx); + address expected = EXPECTED_SPOKE_IMPLEMENTATION; + require(address(uint160(uint256(preImplementation))) == expected, "AaveV4Oracle: unexpected implementation"); + require(address(uint160(uint256(postImplementation))) == expected, "AaveV4Oracle: implementation changed"); + require( + ph.getStateChanges(SPOKE, ERC1967_IMPLEMENTATION_SLOT).length == 0, "AaveV4Oracle: implementation written" + ); + require(ph.getStateChanges(SPOKE, ERC1967_BEACON_SLOT).length == 0, "AaveV4Oracle: beacon written"); + } + + function _loadAndValidatePreTxPolicy(PhEvm.ForkId memory preTx) + internal + view + returns (uint256[] memory baselinePrices) + { + uint256 policyCount = reservePolicies.length; + require(_reserveCountAt(SPOKE, preTx) == policyCount, "AaveV4Oracle: incomplete reserve policy"); + + uint256[] memory reserveIds = new uint256[](policyCount); + for (uint256 i; i < policyCount; ++i) { + ReservePolicy memory policy = reservePolicies[i]; + IAaveV4Spoke.Reserve memory reserve = _spokeReserveAt(SPOKE, policy.reserveId, preTx); + require(reserve.underlying == policy.asset, "AaveV4Oracle: reserve asset mismatch"); + require( + _reserveSourceAt(ORACLE, policy.reserveId, preTx) == policy.source, + "AaveV4Oracle: unexpected PreTx source" + ); + reserveIds[i] = policy.reserveId; + } + + baselinePrices = _reservePricesAt(ORACLE, reserveIds, preTx); + require(baselinePrices.length == policyCount, "AaveV4Oracle: malformed baseline prices"); + for (uint256 i; i < policyCount; ++i) { + require(baselinePrices[i] != 0, "AaveV4Oracle: invalid PreTx price"); + } + } + + function _assertRoutingConfigurationUnchanged() internal view { + for (uint256 i; i < reservePolicies.length; ++i) { + ReservePolicy memory policy = reservePolicies[i]; + require( + ph.getStateChanges(ORACLE, _sourceSlot(policy.reserveId)).length == 0, + "AaveV4Oracle: reserve source written" + ); + } + + for (uint256 i; i < configSlotGuards.length; ++i) { + ConfigSlotGuard memory guard = configSlotGuards[i]; + require(ph.getStateChanges(guard.target, guard.slot).length == 0, "AaveV4Oracle: guarded config written"); + } + } + + function _assertConsumedPrices(PhEvm.TriggerCall[] memory priceCalls, uint256[] memory baselinePrices) + internal + view + { + PhEvm.TriggerCall[][] memory sourceCalls = new PhEvm.TriggerCall[][](reservePolicies.length); + uint256 scannedSourceCalls; + for (uint256 reserveId; reserveId < reservePolicies.length; ++reserveId) { + sourceCalls[reserveId] = _successfulSourceReads(reservePolicies[reserveId].source, MAX_TRACE_CALLS + 1); + require(sourceCalls[reserveId].length <= MAX_TRACE_CALLS, "AaveV4Oracle: source trace limit exceeded"); + scannedSourceCalls += sourceCalls[reserveId].length; + require(scannedSourceCalls <= MAX_TRACE_CALLS, "AaveV4Oracle: source trace limit exceeded"); + } + + for (uint256 i; i < priceCalls.length; ++i) { + PhEvm.TriggerCall memory priceCall = priceCalls[i]; + if (priceCall.caller != SPOKE) { + continue; + } + + (uint256 reserveId, int256 sourceAnswer) = _mapPriceCallToSource(priceCall, sourceCalls); + bytes memory priceOutput = ph.callOutputAt(priceCall.callId); + require(priceOutput.length == 32, "AaveV4Oracle: malformed oracle output"); + uint256 consumedPrice = abi.decode(priceOutput, (uint256)); + require(sourceAnswer > 0, "AaveV4Oracle: invalid source answer"); + require(uint256(sourceAnswer) == consumedPrice, "AaveV4Oracle: source/output mismatch"); + + ReservePolicy memory policy = reservePolicies[reserveId]; + _requireWithinDeviation(baselinePrices[reserveId], consumedPrice, policy.deviationBps); + } + } + + /// @dev PCL v2 currently exposes an empty `TriggerCall.input` for the nested STATICCALL + /// generated by this compiler path. AaveOracle itself always performs exactly one direct + /// external `latestAnswer()` call to the selected source. Unique configured sources, + /// direct parent-call IDs, and equality between child and parent outputs therefore provide + /// an exact, trace-proven reserve mapping without relying on unavailable nested calldata. + function _mapPriceCallToSource(PhEvm.TriggerCall memory priceCall, PhEvm.TriggerCall[][] memory sourceCalls) + internal + view + returns (uint256 mappedReserveId, int256 sourceAnswer) + { + uint256 matches; + for (uint256 reserveId; reserveId < sourceCalls.length; ++reserveId) { + for (uint256 j; j < sourceCalls[reserveId].length; ++j) { + PhEvm.TriggerCall memory sourceCall = sourceCalls[reserveId][j]; + if (sourceCall.caller != ORACLE || sourceCall.parentCallId != priceCall.callId) { + continue; + } + + ++matches; + mappedReserveId = reserveId; + bytes memory sourceOutput = ph.callOutputAt(sourceCall.callId); + require(sourceOutput.length == 32, "AaveV4Oracle: malformed source output"); + sourceAnswer = abi.decode(sourceOutput, (int256)); + } + } + require(matches == 1, "AaveV4Oracle: unconfigured price path"); + } + + function _requireWithinDeviation(uint256 baselinePrice, uint256 consumedPrice, uint256 deviationBps) internal view { + require(consumedPrice != 0, "AaveV4Oracle: invalid consumed price"); + if (deviationBps == 0) { + require(consumedPrice == baselinePrice, "AaveV4Oracle: consumed price deviated"); + return; + } + + uint256 lowerBound = ph.mulDivDown(baselinePrice, BPS - deviationBps, BPS); + uint256 upperBound = ph.mulDivUp(baselinePrice, BPS + deviationBps, BPS); + require(consumedPrice >= lowerBound && consumedPrice <= upperBound, "AaveV4Oracle: consumed price deviated"); + } +} + +/// @title AaveV4EthereumMainSpokeOracleAssertion +/// @author Phylax Systems +/// @notice Ready-to-configure assertion pinned to Aave v4 Ethereum Main Spoke release v0.5.11. +/// @dev Addresses and reserve ordering are verified at Ethereum block 25,646,732. The wrapper +/// pins the deployed Spoke implementation, AaveOracle, all 14 reserve assets, and their +/// current oracle sources. Deploy a new assertion after a legitimate implementation, +/// reserve, or source migration. +contract AaveV4EthereumMainSpokeOracleAssertion is AaveV4OracleConsumptionAssertion { + address public constant MAIN_SPOKE = 0x94e7A5dCbE816e498b89aB752661904E2F56c485; + address public constant MAIN_SPOKE_ORACLE = 0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127; + address public constant MAIN_SPOKE_IMPLEMENTATION = 0xABd0E26FE17BDe4F1f1187Ed8aA80C274E03D8b5; + + /// @param maxTraceCalls_ Fail-closed transaction-wide oracle-call bound. + /// @param deviationBps_ Per-reserve tolerances in Main Spoke reserve-ID order. + /// @param configSlotGuards_ Additional mutable adapter configuration slots to protect. + constructor(uint256 maxTraceCalls_, uint256[14] memory deviationBps_, ConfigSlotGuard[] memory configSlotGuards_) + AaveV4OracleConsumptionAssertion( + MAIN_SPOKE, + MAIN_SPOKE_ORACLE, + MAIN_SPOKE_IMPLEMENTATION, + maxTraceCalls_, + _mainSpokePolicies(deviationBps_), + _mainSpokeConfigGuards(configSlotGuards_) + ) + {} + + /// @dev Verified mutable routing/cap slots for the exact source graph at block 25,646,732. + /// Chainlink EACAggregatorProxy keeps its active phase/aggregator in slot 2. + /// PriceCapAdapterBase keeps its packed cap parameters in slots 1 and 2. + /// PriceCapAdapterStable and EURPriceCapAdapterStable keep the active cap in slot 2. + function _mainSpokeConfigGuards(ConfigSlotGuard[] memory extra) + private + pure + returns (ConfigSlotGuard[] memory guards) + { + guards = new ConfigSlotGuard[](22 + extra.length); + uint256 i; + + // Direct or transitively consumed Chainlink EACAggregatorProxy active-phase slots. + guards[i++] = ConfigSlotGuard(0x5424384B256154046E9667dDFaaa5e550145215e, bytes32(uint256(2))); // WETH/USD + guards[i++] = ConfigSlotGuard(0xb41E773f507F7a7EA890b1afB7d2b660c30C8B0A, bytes32(uint256(2))); // cbBTC/USD + guards[i++] = ConfigSlotGuard(0xF02C1e2A3B77c1cacC72f72B44f7d0a4c62e4a85, bytes32(uint256(2))); // AAVE/USD + guards[i++] = ConfigSlotGuard(0xC7e9b623ed51F033b32AE7f1282b1AD62C28C183, bytes32(uint256(2))); // LINK/USD + guards[i++] = ConfigSlotGuard(0xfdFD9C85aD200c506Cf9e21F1FD8dd01932FBB23, bytes32(uint256(2))); // WBTC/BTC + guards[i++] = ConfigSlotGuard(0xEa674bBC33AE708Bc9EB4ba348b04E4eB55b496b, bytes32(uint256(2))); // USDC/USD + guards[i++] = ConfigSlotGuard(0x3E7d1eAB13ad0104d2750B8863b489D65364e32D, bytes32(uint256(2))); // USDT/USD + guards[i++] = ConfigSlotGuard(0x26C46B7aD0012cA71F2298ada567dC9Af14E7f2A, bytes32(uint256(2))); // RLUSD/USD + guards[i++] = ConfigSlotGuard(0x14f0737d6b705259e521EA6E9E3506AC78dBd311, bytes32(uint256(2))); // USDG/USD + guards[i++] = ConfigSlotGuard(0x9B4a96210bc8D9D55b1908B465D8B0de68B7fF83, bytes32(uint256(2))); // frxUSD/USD + guards[i++] = ConfigSlotGuard(0x04F84020Fdf10d9ee64D1dcC2986EDF2F556DA11, bytes32(uint256(2))); // EURC/USD + guards[i++] = ConfigSlotGuard(0xb49f677943BC038e9857d61E7d053CaA2C1734C1, bytes32(uint256(2))); // EUR/USD + + // Mutable CAPO parameter slots. + guards[i++] = ConfigSlotGuard(0xe1D97bF61901B075E9626c8A2340a7De385861Ef, bytes32(uint256(1))); + guards[i++] = ConfigSlotGuard(0xe1D97bF61901B075E9626c8A2340a7De385861Ef, bytes32(uint256(2))); + guards[i++] = ConfigSlotGuard(0x87625393534d5C102cADB66D37201dF24cc26d4C, bytes32(uint256(1))); + guards[i++] = ConfigSlotGuard(0x87625393534d5C102cADB66D37201dF24cc26d4C, bytes32(uint256(2))); + guards[i++] = ConfigSlotGuard(0x3f73F03aa83B2A48ed27E964eD0fDb590332095B, bytes32(uint256(2))); + guards[i++] = ConfigSlotGuard(0x260326c220E469358846b187eE53328303Efe19C, bytes32(uint256(2))); + guards[i++] = ConfigSlotGuard(0xf0eaC18E908B34770FDEe46d069c846bDa866759, bytes32(uint256(2))); + guards[i++] = ConfigSlotGuard(0x83D20dEEdcd4aC1313496c8CBcAad0fa298c0CE4, bytes32(uint256(2))); + guards[i++] = ConfigSlotGuard(0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD, bytes32(uint256(2))); + guards[i++] = ConfigSlotGuard(0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3, bytes32(uint256(2))); + + for (uint256 j; j < extra.length; ++j) { + guards[i++] = extra[j]; + } + } + + function _mainSpokePolicies(uint256[14] memory d) private pure returns (ReservePolicy[] memory policies) { + policies = new ReservePolicy[](14); + policies[0] = ReservePolicy( + 0, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0x5424384B256154046E9667dDFaaa5e550145215e, d[0] + ); + policies[1] = ReservePolicy( + 1, 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0, 0xe1D97bF61901B075E9626c8A2340a7De385861Ef, d[1] + ); + policies[2] = ReservePolicy( + 2, 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee, 0x87625393534d5C102cADB66D37201dF24cc26d4C, d[2] + ); + policies[3] = ReservePolicy( + 3, 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599, 0xDaa4B74C6bAc4e25188e64ebc68DB5050b690cAc, d[3] + ); + policies[4] = ReservePolicy( + 4, 0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf, 0xb41E773f507F7a7EA890b1afB7d2b660c30C8B0A, d[4] + ); + policies[5] = ReservePolicy( + 5, 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9, 0xF02C1e2A3B77c1cacC72f72B44f7d0a4c62e4a85, d[5] + ); + policies[6] = ReservePolicy( + 6, 0x514910771AF9Ca656af840dff83E8264EcF986CA, 0xC7e9b623ed51F033b32AE7f1282b1AD62C28C183, d[6] + ); + policies[7] = ReservePolicy( + 7, 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, 0x3f73F03aa83B2A48ed27E964eD0fDb590332095B, d[7] + ); + policies[8] = ReservePolicy( + 8, 0xdAC17F958D2ee523a2206206994597C13D831ec7, 0x260326c220E469358846b187eE53328303Efe19C, d[8] + ); + policies[9] = ReservePolicy( + 9, 0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c, 0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3, d[9] + ); + policies[10] = ReservePolicy( + 10, 0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD, 0xf0eaC18E908B34770FDEe46d069c846bDa866759, d[10] + ); + policies[11] = ReservePolicy( + 11, 0xe343167631d89B6Ffc58B88d6b7fB0228795491D, 0x83D20dEEdcd4aC1313496c8CBcAad0fa298c0CE4, d[11] + ); + policies[12] = ReservePolicy( + 12, 0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29, 0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD, d[12] + ); + policies[13] = ReservePolicy( + 13, 0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f, 0xD110cac5d8682A3b045D5524a9903E031d70FCCd, d[13] + ); + } +} diff --git a/examples/aave/src/AaveV4OracleConsumptionHelpers.sol b/examples/aave/src/AaveV4OracleConsumptionHelpers.sol new file mode 100644 index 0000000..284be9c --- /dev/null +++ b/examples/aave/src/AaveV4OracleConsumptionHelpers.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {PhEvm} from "credible-std/PhEvm.sol"; + +import {AaveV4Helpers} from "./AaveV4Helpers.sol"; +import {IAaveV4Oracle, IAaveV4PriceFeed, IAaveV4Spoke} from "./AaveV4Interfaces.sol"; + +/// @title AaveV4OracleConsumptionHelpers +/// @author Phylax Systems +/// @notice Fork-aware readers and trace filters for Aave v4 consumed-price protection. +/// @dev Constants are pinned to Aave v4 release v0.5.11 and the Ethereum Main Spoke deployment: +/// - AaveOracle `_sources` mapping is storage slot 1. +/// - Main Spoke is a TransparentUpgradeableProxy using the ERC-1967 implementation slot. +/// - AaveOracle returns 8-decimal positive prices from `IPriceFeed.latestAnswer()`. +abstract contract AaveV4OracleConsumptionHelpers is AaveV4Helpers { + uint256 internal constant AAVE_ORACLE_SOURCES_MAPPING_SLOT = 1; + + bytes32 internal constant ERC1967_IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + bytes32 internal constant ERC1967_BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + + function _spokeOracleAt(address spoke, PhEvm.ForkId memory fork) internal view returns (address) { + return _readAddressAt(spoke, abi.encodeCall(IAaveV4Spoke.ORACLE, ()), fork); + } + + function _reserveCountAt(address spoke, PhEvm.ForkId memory fork) internal view returns (uint256) { + return _readUintAt(spoke, abi.encodeCall(IAaveV4Spoke.getReserveCount, ()), fork); + } + + function _oracleSpokeAt(address oracle, PhEvm.ForkId memory fork) internal view returns (address) { + return _readAddressAt(oracle, abi.encodeCall(IAaveV4Oracle.spoke, ()), fork); + } + + function _oracleDecimalsAt(address oracle, PhEvm.ForkId memory fork) internal view returns (uint8) { + return _readUint8At(oracle, abi.encodeCall(IAaveV4Oracle.decimals, ()), fork); + } + + function _reserveSourceAt(address oracle, uint256 reserveId, PhEvm.ForkId memory fork) + internal + view + returns (address) + { + return _readAddressAt(oracle, abi.encodeCall(IAaveV4Oracle.getReserveSource, (reserveId)), fork); + } + + function _reservePricesAt(address oracle, uint256[] memory reserveIds, PhEvm.ForkId memory fork) + internal + view + returns (uint256[] memory prices) + { + prices = abi.decode( + _viewAt(oracle, abi.encodeCall(IAaveV4Oracle.getReservesPrices, (reserveIds)), fork), (uint256[]) + ); + } + + function _sourceSlot(uint256 reserveId) internal pure returns (bytes32) { + return keccak256(abi.encode(reserveId, AAVE_ORACLE_SOURCES_MAPPING_SLOT)); + } + + function _successfulStaticCalls(address target, bytes4 selector, uint256 limit) + internal + view + returns (PhEvm.TriggerCall[] memory) + { + PhEvm.CallFilter memory filter = PhEvm.CallFilter({ + callType: 2, minDepth: 0, maxDepth: type(uint32).max, topLevelOnly: false, successOnly: true + }); + return ph.matchingCalls(target, selector, filter, limit); + } + + function _successfulSourceReads(address source, uint256 limit) internal view returns (PhEvm.TriggerCall[] memory) { + return _successfulStaticCalls(source, IAaveV4PriceFeed.latestAnswer.selector, limit); + } + + function _hasMandatoryPriceOperation(address spoke) internal view returns (bool) { + return _matchingCalls(spoke, IAaveV4Spoke.borrow.selector, 1).length != 0 + || _matchingCalls(spoke, IAaveV4Spoke.liquidationCall.selector, 1).length != 0; + } +} diff --git a/examples/aave/src/AaveV4PTUSDGRedemptionAssertion.sol b/examples/aave/src/AaveV4PTUSDGRedemptionAssertion.sol new file mode 100644 index 0000000..b17bed8 --- /dev/null +++ b/examples/aave/src/AaveV4PTUSDGRedemptionAssertion.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {PhEvm} from "credible-std/PhEvm.sol"; + +import { + AaveV4ExternalCollateralHelpers, + IExternalFrozenAccount, + IExternalPausable +} from "./AaveV4ExternalCollateralHelpers.sol"; +import {IAaveV4Spoke} from "./AaveV4Interfaces.sol"; + +/// @title AaveV4PTUSDGRedemptionAssertion +/// @author Phylax Systems +/// @notice Makes PT-USDG-backed Aave positions reduce-only when PT's USDG exit is disabled. +/// @dev Protects the failure where PT remains transferable and Aave's PT oracle remains valid, +/// but Pendle's SY is paused, USDG is globally paused, or Paxos freezes the SY account. +/// Aave borrow does not redeem PT, so native Pendle/USDG reverts otherwise arrive only when +/// collateral value must be realized. Maturity is intentionally not treated as a failure. +contract AaveV4PTUSDGRedemptionAssertion is AaveV4ExternalCollateralHelpers { + address internal immutable SPOKE; + uint256 internal immutable PT_RESERVE_ID; + address internal immutable PT_USDG; + address internal immutable HUB; + address internal immutable SY_USDG; + address internal immutable USDG; + + /// @param spoke_ The Aave v4 USDG Pendle Spoke adopting the assertion. + /// @param ptReserveId_ The Spoke reserve id for PT-USDG-24SEP2026. + /// @param ptUsdg_ The canonical PT token. + /// @param hub_ The Hub holding supplied PT on behalf of Aave users. + /// @param syUsdg_ The Pendle standardized-yield contract through which PT resolves to USDG. + /// @param usdg_ The Paxos USDG token returned by the SY redemption path. + constructor(address spoke_, uint256 ptReserveId_, address ptUsdg_, address hub_, address syUsdg_, address usdg_) { + require(spoke_ != address(0), "AaveV4PTUSDG: Spoke zero"); + require(ptUsdg_ != address(0), "AaveV4PTUSDG: PT zero"); + require(hub_ != address(0), "AaveV4PTUSDG: Hub zero"); + require(syUsdg_ != address(0), "AaveV4PTUSDG: SY zero"); + require(usdg_ != address(0), "AaveV4PTUSDG: USDG zero"); + + SPOKE = spoke_; + PT_RESERVE_ID = ptReserveId_; + PT_USDG = ptUsdg_; + HUB = hub_; + SY_USDG = syUsdg_; + USDG = usdg_; + } + + /// @notice Registers only debt growth and effective-collateral removal paths. + /// @dev Per-call context is necessary to identify the user and allow a complete PT exit while + /// blocking a withdrawal of other good collateral that leaves debt relying on stranded PT. + function triggers() external view override { + registerFnCallTrigger(this.assertPtUsdgRedemptionAvailable.selector, IAaveV4Spoke.borrow.selector); + registerFnCallTrigger(this.assertPtUsdgRedemptionAvailable.selector, IAaveV4Spoke.withdraw.selector); + registerFnCallTrigger(this.assertPtUsdgRedemptionAvailable.selector, IAaveV4Spoke.setUsingAsCollateral.selector); + } + + /// @notice Requires the canonical PT -> SY -> USDG exit to remain administratively available. + /// @dev The check runs only if the affected user still relies on positive PT collateral after + /// a risk-increasing call. A failure means Aave could accept more debt or less good + /// collateral even though SY/USDG state independently prevents PT value realization. + /// PreCall plus PostTx reads reject same-transaction pause/freeze wrapping. + function assertPtUsdgRedemptionAvailable() external view { + _requireAdopter(SPOKE, "AaveV4PTUSDG: configured Spoke is not adopter"); + + PhEvm.TriggerContext memory ctx = ph.context(); + (address user, bool riskIncreasing) = _riskIncreasingUser(SPOKE, ctx); + if (!riskIncreasing) { + return; + } + + PhEvm.ForkId memory postCall = _postCall(ctx.callEnd); + if (!_isActiveCollateralAt(SPOKE, PT_RESERVE_ID, user, postCall)) { + return; + } + + IAaveV4Spoke.Reserve memory reserve = _spokeReserveAt(SPOKE, PT_RESERVE_ID, postCall); + require(reserve.underlying == PT_USDG, "AaveV4PTUSDG: reserve PT changed"); + require(reserve.hub == HUB, "AaveV4PTUSDG: reserve Hub changed"); + + _requireRedemptionAvailableAt(_preCall(ctx.callStart), "AaveV4PTUSDG: redemption disabled before risk increase"); + _requireRedemptionAvailableAt(_postTx(), "AaveV4PTUSDG: redemption disabled at transaction end"); + } + + /// @notice Returns the immutable deployment wiring for operational review. + function configuration() + external + view + returns (address spoke, uint256 ptReserveId, address ptUsdg, address hub, address syUsdg, address usdg) + { + return (SPOKE, PT_RESERVE_ID, PT_USDG, HUB, SY_USDG, USDG); + } + + function _requireRedemptionAvailableAt(PhEvm.ForkId memory fork, string memory message) internal view { + bool syPaused = _readBoolAt(SY_USDG, abi.encodeCall(IExternalPausable.paused, ()), fork); + bool usdgPaused = _readBoolAt(USDG, abi.encodeCall(IExternalPausable.paused, ()), fork); + bool syFrozen = _readBoolAt(USDG, abi.encodeCall(IExternalFrozenAccount.isFrozen, (SY_USDG)), fork); + require(!syPaused && !usdgPaused && !syFrozen, message); + } +} + +/// @title AaveV4EthereumPTUSDGRedemptionAssertion +/// @notice Production-configured PT-USDG redemption gate for Aave v4 Ethereum. +/// @dev Constants were verified at Ethereum block 25,653,183. Revalidate the Spoke reserve, +/// PT/SY/USDG path, Hub, proxy implementations, and status ABIs before adoption after upgrades. +contract AaveV4EthereumPTUSDGRedemptionAssertion is AaveV4PTUSDGRedemptionAssertion { + address public constant USDG_PENDLE_SPOKE = 0x956d8e0A89cfa3744428C4641b5a53B56167a7f9; + address public constant PT_USDG_24SEP2026 = 0xc1906aeCf868749a2DeE203F59b904c0cf212140; + address public constant PAXOS_HUB = 0x62d63197660c080236193CA60b70E49A08E90368; + address public constant PENDLE_SY_USDG = 0xc1799CaB1F201946f7CFaFBaF1BCC089b2F08927; + address public constant PAXOS_USDG = 0xe343167631d89B6Ffc58B88d6b7fB0228795491D; + + constructor() + AaveV4PTUSDGRedemptionAssertion(USDG_PENDLE_SPOKE, 0, PT_USDG_24SEP2026, PAXOS_HUB, PENDLE_SY_USDG, PAXOS_USDG) + {} +} diff --git a/examples/aave/test/AaveV4ExternalCollateralAssertions.t.sol b/examples/aave/test/AaveV4ExternalCollateralAssertions.t.sol new file mode 100644 index 0000000..9165221 --- /dev/null +++ b/examples/aave/test/AaveV4ExternalCollateralAssertions.t.sol @@ -0,0 +1,754 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; + +import {CredibleTest} from "credible-std/CredibleTest.sol"; + +import { + AaveV4EthereumEtherFiSpokeWeETHTransferabilityAssertion, + AaveV4EthereumMainSpokeWeETHTransferabilityAssertion, + AaveV4ExternalCollateralTransferabilityAssertion as Transferability +} from "../src/AaveV4ExternalCollateralTransferabilityAssertion.sol"; +import { + AaveV4EthereumPTUSDGRedemptionAssertion, + AaveV4PTUSDGRedemptionAssertion +} from "../src/AaveV4PTUSDGRedemptionAssertion.sol"; +import {IAaveV4Spoke} from "../src/AaveV4Interfaces.sol"; + +contract MockAaveV4ExternalStatus { + bool internal pausedState; + bool internal revertPausedRead; + uint256 public pausedUntil; + mapping(address account => uint256 until) public blacklistedUntil; + mapping(address account => bool blocked) internal blacklisted; + mapping(address account => bool blocked) internal blackListed; + mapping(address account => bool blocked) internal blocked; + mapping(address account => bool frozen) internal frozen; + mapping(bytes32 role => mapping(address account => bool granted)) internal roles; + + function paused() external view returns (bool) { + require(!revertPausedRead, "status read unavailable"); + return pausedState; + } + + function isBlacklisted(address account) external view returns (bool) { + return blacklisted[account]; + } + + function isBlackListed(address account) external view returns (bool) { + return blackListed[account]; + } + + function isBlocked(address account) external view returns (bool) { + return blocked[account]; + } + + function isFrozen(address account) external view returns (bool) { + return frozen[account]; + } + + function hasRole(bytes32 role, address account) external view returns (bool) { + return roles[role][account]; + } + + function setPaused(bool value) external { + pausedState = value; + } + + function setRevertPausedRead(bool value) external { + revertPausedRead = value; + } + + function setPausedUntil(uint256 value) external { + pausedUntil = value; + } + + function setBlacklistedUntil(address account, uint256 value) external { + blacklistedUntil[account] = value; + } + + function setBlacklisted(address account, bool value) external { + blacklisted[account] = value; + } + + function setBlackListed(address account, bool value) external { + blackListed[account] = value; + } + + function setBlocked(address account, bool value) external { + blocked[account] = value; + } + + function setFrozen(address account, bool value) external { + frozen[account] = value; + } + + function setRole(bytes32 role, address account, bool value) external { + roles[role][account] = value; + } +} + +contract MockAaveV4ExternalSpoke { + IAaveV4Spoke.Reserve[] internal reserves; + mapping(uint256 reserveId => IAaveV4Spoke.DynamicReserveConfig config) internal dynamicConfigs; + mapping(address user => mapping(uint256 reserveId => IAaveV4Spoke.UserPosition position)) internal positions; + mapping(address user => mapping(uint256 reserveId => bool collateral)) internal collateralStatus; + mapping(address user => mapping(uint256 reserveId => bool borrowing)) internal borrowingStatus; + mapping(address user => uint256 debtValueRay) internal debts; + + function addReserve(address token, address hub, uint16 collateralFactor) external returns (uint256 reserveId) { + reserveId = reserves.length; + reserves.push( + IAaveV4Spoke.Reserve({ + underlying: token, + hub: hub, + assetId: uint16(reserveId), + decimals: 18, + collateralRisk: 0, + flags: 0, + dynamicConfigKey: 0 + }) + ); + dynamicConfigs[reserveId] = IAaveV4Spoke.DynamicReserveConfig({ + collateralFactor: collateralFactor, maxLiquidationBonus: 10_500, liquidationFee: 1_000 + }); + } + + function setPosition(address user, uint256 reserveId, uint120 suppliedShares, bool usingAsCollateral) external { + positions[user][reserveId].suppliedShares = suppliedShares; + positions[user][reserveId].dynamicConfigKey = 0; + collateralStatus[user][reserveId] = usingAsCollateral; + } + + function setDebt(address user, uint256 debtValueRay) external { + debts[user] = debtValueRay; + } + + function setCollateralFactor(uint256 reserveId, uint16 collateralFactor) external { + dynamicConfigs[reserveId].collateralFactor = collateralFactor; + } + + function setReserveToken(uint256 reserveId, address token) external { + reserves[reserveId].underlying = token; + } + + function setReserveHub(uint256 reserveId, address hub) external { + reserves[reserveId].hub = hub; + } + + function supply(uint256 reserveId, uint256 amount, address onBehalfOf) external returns (uint256, uint256) { + positions[onBehalfOf][reserveId].suppliedShares += uint120(amount); + return (amount, amount); + } + + function withdraw(uint256 reserveId, uint256 amount, address onBehalfOf) external returns (uint256, uint256) { + uint120 shares = positions[onBehalfOf][reserveId].suppliedShares; + uint120 removed = amount >= shares ? shares : uint120(amount); + positions[onBehalfOf][reserveId].suppliedShares = shares - removed; + return (removed, removed); + } + + function borrow(uint256 reserveId, uint256 amount, address onBehalfOf) external returns (uint256, uint256) { + debts[onBehalfOf] += amount * 1e27; + borrowingStatus[onBehalfOf][reserveId] = true; + return (amount, amount); + } + + function repay(uint256 reserveId, uint256 amount, address onBehalfOf) external returns (uint256, uint256) { + reserveId; + amount; + debts[onBehalfOf] = 0; + return (amount, amount); + } + + function liquidationCall(uint256, uint256, address user, uint256, bool) external { + debts[user] = 0; + } + + function setUsingAsCollateral(uint256 reserveId, bool usingAsCollateral, address onBehalfOf) external { + collateralStatus[onBehalfOf][reserveId] = usingAsCollateral; + } + + function getReserve(uint256 reserveId) external view returns (IAaveV4Spoke.Reserve memory) { + return reserves[reserveId]; + } + + function getDynamicReserveConfig(uint256 reserveId, uint32) + external + view + returns (IAaveV4Spoke.DynamicReserveConfig memory) + { + return dynamicConfigs[reserveId]; + } + + function getUserReserveStatus(uint256 reserveId, address user) external view returns (bool, bool) { + return (collateralStatus[user][reserveId], borrowingStatus[user][reserveId]); + } + + function getUserPosition(uint256 reserveId, address user) external view returns (IAaveV4Spoke.UserPosition memory) { + return positions[user][reserveId]; + } + + function getUserAccountData(address user) external view returns (IAaveV4Spoke.UserAccountData memory data) { + data.totalDebtValueRay = debts[user]; + } +} + +contract AaveV4ExternalScenarioDriver { + function borrowThenPause(MockAaveV4ExternalSpoke spoke, MockAaveV4ExternalStatus status, address user) external { + spoke.borrow(1, 1, user); + status.setPaused(true); + } + + function pauseBorrowUnpause(MockAaveV4ExternalSpoke spoke, MockAaveV4ExternalStatus status, address user) external { + status.setPaused(true); + spoke.borrow(1, 1, user); + status.setPaused(false); + } +} + +abstract contract AaveV4ExternalCollateralTestBase is Test, CredibleTest { + uint256 internal constant RESTRICTED_RESERVE = 0; + uint256 internal constant GOOD_RESERVE = 1; + uint256 internal constant INITIAL_DEBT_RAY = 100e27; + bytes32 internal constant FULL_RESTRICTED_STAKER_ROLE = keccak256("FULL_RESTRICTED_STAKER_ROLE"); + + address internal user = makeAddr("borrower"); + address internal hub = makeAddr("Aave Hub"); + address internal otherHub = makeAddr("other Hub"); + address internal goodToken = makeAddr("good collateral"); + + MockAaveV4ExternalStatus internal token; + MockAaveV4ExternalStatus internal blacklister; + MockAaveV4ExternalSpoke internal spoke; + AaveV4ExternalScenarioDriver internal driver; + + function setUp() public virtual { + vm.warp(1_000_000); + token = new MockAaveV4ExternalStatus(); + blacklister = new MockAaveV4ExternalStatus(); + spoke = new MockAaveV4ExternalSpoke(); + spoke.addReserve(address(token), hub, 8_000); + spoke.addReserve(goodToken, hub, 8_000); + spoke.setPosition(user, RESTRICTED_RESERVE, 100, true); + spoke.setPosition(user, GOOD_RESERVE, 100, true); + spoke.setDebt(user, INITIAL_DEBT_RAY); + driver = new AaveV4ExternalScenarioDriver(); + } +} + +contract AaveV4ExternalCollateralTransferabilityAssertionTest is AaveV4ExternalCollateralTestBase { + function testHonestBorrowPasses() public { + _arm(Transferability.AdapterKind.Paused, address(token)); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testGlobalPauseTrips() public { + token.setPaused(true); + _expectBorrowFailure( + Transferability.AdapterKind.Paused, + address(token), + "AaveV4Transferability: collateral restricted before risk increase" + ); + } + + function testCamelCaseBlacklistTrips() public { + token.setBlacklisted(hub, true); + _expectBorrowFailure( + Transferability.AdapterKind.PausedAndBlacklisted, + address(token), + "AaveV4Transferability: collateral restricted before risk increase" + ); + } + + function testTetherBlacklistTrips() public { + token.setBlackListed(hub, true); + _expectBorrowFailure( + Transferability.AdapterKind.PausedAndBlackListed, + address(token), + "AaveV4Transferability: collateral restricted before risk increase" + ); + } + + function testWeEthIndefinitePauseTrips() public { + token.setPaused(true); + _expectBorrowFailure( + Transferability.AdapterKind.WeEth, + address(blacklister), + "AaveV4Transferability: collateral restricted before risk increase" + ); + } + + function testWeEthTimedPauseTripsAtBoundary() public { + token.setPausedUntil(block.timestamp); + _expectBorrowFailure( + Transferability.AdapterKind.WeEth, + address(blacklister), + "AaveV4Transferability: collateral restricted before risk increase" + ); + } + + function testWeEthHubBlacklistTrips() public { + blacklister.setBlacklistedUntil(hub, block.timestamp + 1 days); + _expectBorrowFailure( + Transferability.AdapterKind.WeEth, + address(blacklister), + "AaveV4Transferability: collateral restricted before risk increase" + ); + } + + function testWeEthExpiredTimedPausePasses() public { + token.setPausedUntil(block.timestamp - 1); + _arm(Transferability.AdapterKind.WeEth, address(blacklister)); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testWeEthBlacklistEndingAtCurrentTimestampPasses() public { + blacklister.setBlacklistedUntil(hub, block.timestamp); + _arm(Transferability.AdapterKind.WeEth, address(blacklister)); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testBlockedHubTrips() public { + token.setBlocked(hub, true); + _expectBorrowFailure( + Transferability.AdapterKind.Blocked, + address(token), + "AaveV4Transferability: collateral restricted before risk increase" + ); + } + + function testFullRestrictedRoleTrips() public { + token.setRole(FULL_RESTRICTED_STAKER_ROLE, hub, true); + _expectBorrowFailure( + Transferability.AdapterKind.FullRestrictedRole, + address(token), + "AaveV4Transferability: collateral restricted before risk increase" + ); + } + + function testPauseAddedAfterBorrowTripsAtPostTx() public { + _arm(Transferability.AdapterKind.Paused, address(token)); + vm.expectRevert(bytes("AaveV4Transferability: collateral restricted at transaction end")); + driver.borrowThenPause(spoke, token, user); + } + + function testTransientPauseWrappedAroundBorrowTripsAtPreCall() public { + _arm(Transferability.AdapterKind.Paused, address(token)); + vm.expectRevert(bytes("AaveV4Transferability: collateral restricted before risk increase")); + driver.pauseBorrowUnpause(spoke, token, user); + } + + function testDebtFreeCollateralWithdrawalPassesWhilePaused() public { + spoke.setDebt(user, 0); + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + spoke.withdraw(RESTRICTED_RESERVE, 10, user); + } + + function testNonCollateralWithdrawalPassesWhileOtherCollateralPaused() public { + spoke.setPosition(user, GOOD_RESERVE, 100, false); + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + spoke.withdraw(GOOD_RESERVE, 10, user); + } + + function testZeroFactorWithdrawalPassesAsNonCollateral() public { + spoke.setCollateralFactor(GOOD_RESERVE, 0); + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + spoke.withdraw(GOOD_RESERVE, 10, user); + } + + function testFullImpairedCollateralExitPasses() public { + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + spoke.withdraw(RESTRICTED_RESERVE, type(uint256).max, user); + } + + function testPartialImpairedCollateralExitTripsWhileDebtStillReliesOnIt() public { + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + vm.expectRevert(bytes("AaveV4Transferability: collateral restricted before risk increase")); + spoke.withdraw(RESTRICTED_RESERVE, 10, user); + } + + function testDisableImpairedCollateralPasses() public { + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + spoke.setUsingAsCollateral(RESTRICTED_RESERVE, false, user); + } + + function testEnableImpairedCollateralWithDebtTrips() public { + spoke.setPosition(user, RESTRICTED_RESERVE, 100, false); + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + vm.expectRevert(bytes("AaveV4Transferability: collateral restricted before risk increase")); + spoke.setUsingAsCollateral(RESTRICTED_RESERVE, true, user); + } + + function testEnableImpairedCollateralWithoutDebtPasses() public { + spoke.setPosition(user, RESTRICTED_RESERVE, 100, false); + spoke.setDebt(user, 0); + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + spoke.setUsingAsCollateral(RESTRICTED_RESERVE, true, user); + } + + function testNoOpCollateralEnablePassesWhilePaused() public { + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + spoke.setUsingAsCollateral(RESTRICTED_RESERVE, true, user); + } + + function testWithdrawGoodCollateralTripsWhenDebtReliesOnImpairedCollateral() public { + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + vm.expectRevert(bytes("AaveV4Transferability: collateral restricted before risk increase")); + spoke.withdraw(GOOD_RESERVE, 10, user); + } + + function testDisableGoodCollateralTripsWhenDebtReliesOnImpairedCollateral() public { + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + vm.expectRevert(bytes("AaveV4Transferability: collateral restricted before risk increase")); + spoke.setUsingAsCollateral(GOOD_RESERVE, false, user); + } + + function testPausedCollateralNotUsedByAffectedUserPasses() public { + spoke.setPosition(user, RESTRICTED_RESERVE, 100, false); + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testZeroSuppliedSharesDoNotCountAsReliance() public { + spoke.setPosition(user, RESTRICTED_RESERVE, 0, true); + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testZeroCollateralFactorDoesNotCountAsReliance() public { + spoke.setCollateralFactor(RESTRICTED_RESERVE, 0); + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testSupplyRemainsOpenWhilePaused() public { + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + // The local Credible harness requires the armed assertion to execute once. Expecting its + // zero-execution diagnostic proves `supply` is deliberately absent from production triggers. + vm.expectRevert(bytes("Expected 1 assertion to be executed, but 0 were executed.")); + spoke.supply(GOOD_RESERVE, 1, user); + } + + function testRepayRemainsOpenWhilePaused() public { + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + vm.expectRevert(bytes("Expected 1 assertion to be executed, but 0 were executed.")); + spoke.repay(GOOD_RESERVE, 1, user); + } + + function testLiquidationRemainsOpenWhilePaused() public { + token.setPaused(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + vm.expectRevert(bytes("Expected 1 assertion to be executed, but 0 were executed.")); + spoke.liquidationCall(RESTRICTED_RESERVE, GOOD_RESERVE, user, 1, true); + } + + function testMultiplePoliciesRejectedToPreserveAssertionGasBound() public { + Transferability.CollateralPolicy[] memory policies = new Transferability.CollateralPolicy[](2); + policies[0] = Transferability.CollateralPolicy({ + reserveId: RESTRICTED_RESERVE, + token: address(token), + hub: hub, + statusSource: address(token), + adapter: Transferability.AdapterKind.Paused + }); + policies[1] = Transferability.CollateralPolicy({ + reserveId: GOOD_RESERVE, + token: address(token), + hub: hub, + statusSource: address(token), + adapter: Transferability.AdapterKind.Paused + }); + vm.expectRevert(bytes("AaveV4Transferability: one policy required")); + new Transferability(address(spoke), policies); + } + + function testReserveTokenDriftFailsClosed() public { + spoke.setReserveToken(RESTRICTED_RESERVE, goodToken); + _arm(Transferability.AdapterKind.Paused, address(token)); + vm.expectRevert(bytes("AaveV4Transferability: reserve token changed")); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testReserveHubDriftFailsClosed() public { + spoke.setReserveHub(RESTRICTED_RESERVE, otherHub); + _arm(Transferability.AdapterKind.Paused, address(token)); + vm.expectRevert(bytes("AaveV4Transferability: reserve Hub changed")); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testExternalStatusGetterFailureFailsClosed() public { + token.setRevertPausedRead(true); + _arm(Transferability.AdapterKind.Paused, address(token)); + vm.expectRevert(bytes("AaveV4: fork view failed")); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testWrongAdopterFailsClosed() public { + MockAaveV4ExternalSpoke other = new MockAaveV4ExternalSpoke(); + other.addReserve(address(token), hub, 8_000); + other.setPosition(user, 0, 100, true); + other.setDebt(user, INITIAL_DEBT_RAY); + + Transferability.CollateralPolicy[] memory policies = + _policies(Transferability.AdapterKind.Paused, address(token)); + bytes memory createData = + abi.encodePacked(type(Transferability).creationCode, abi.encode(address(spoke), policies)); + cl.assertion(address(other), createData, Transferability.assertExternalCollateralTransferable.selector); + + vm.expectRevert(bytes("AaveV4Transferability: configured Spoke is not adopter")); + other.borrow(0, 1, user); + } + + function _expectBorrowFailure(Transferability.AdapterKind adapter, address statusSource, string memory reason) + internal + { + _arm(adapter, statusSource); + vm.expectRevert(bytes(reason)); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function _arm(Transferability.AdapterKind adapter, address statusSource) internal { + Transferability.CollateralPolicy[] memory policies = _policies(adapter, statusSource); + _armPolicies(policies); + } + + function _armPolicies(Transferability.CollateralPolicy[] memory policies) internal { + bytes memory createData = + abi.encodePacked(type(Transferability).creationCode, abi.encode(address(spoke), policies)); + cl.assertion(address(spoke), createData, Transferability.assertExternalCollateralTransferable.selector); + } + + function _policies(Transferability.AdapterKind adapter, address statusSource) + internal + view + returns (Transferability.CollateralPolicy[] memory policies) + { + policies = new Transferability.CollateralPolicy[](1); + policies[0] = Transferability.CollateralPolicy({ + reserveId: RESTRICTED_RESERVE, token: address(token), hub: hub, statusSource: statusSource, adapter: adapter + }); + } +} + +contract AaveV4PTUSDGRedemptionAssertionTest is AaveV4ExternalCollateralTestBase { + MockAaveV4ExternalStatus internal sy; + MockAaveV4ExternalStatus internal usdg; + + function setUp() public override { + super.setUp(); + sy = new MockAaveV4ExternalStatus(); + usdg = new MockAaveV4ExternalStatus(); + } + + function testHonestPtBorrowPasses() public { + _armPt(); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testSyPauseTrips() public { + sy.setPaused(true); + _expectPtBorrowFailure("AaveV4PTUSDG: redemption disabled before risk increase"); + } + + function testUsdgPauseTrips() public { + usdg.setPaused(true); + _expectPtBorrowFailure("AaveV4PTUSDG: redemption disabled before risk increase"); + } + + function testUsdgFreezeOfSyTrips() public { + usdg.setFrozen(address(sy), true); + _expectPtBorrowFailure("AaveV4PTUSDG: redemption disabled before risk increase"); + } + + function testSyPauseAddedAfterBorrowTripsAtPostTx() public { + _armPt(); + vm.expectRevert(bytes("AaveV4PTUSDG: redemption disabled at transaction end")); + driver.borrowThenPause(spoke, sy, user); + } + + function testTransientSyPauseWrappedAroundBorrowTripsAtPreCall() public { + _armPt(); + vm.expectRevert(bytes("AaveV4PTUSDG: redemption disabled before risk increase")); + driver.pauseBorrowUnpause(spoke, sy, user); + } + + function testPostMaturityWithAvailableRedemptionPasses() public { + vm.warp(1_790_208_001); + _armPt(); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testPtNotUsedAsCollateralPassesDuringSyPause() public { + spoke.setPosition(user, RESTRICTED_RESERVE, 100, false); + sy.setPaused(true); + _armPt(); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testDebtFreePtWithdrawalPassesDuringSyPause() public { + spoke.setDebt(user, 0); + sy.setPaused(true); + _armPt(); + spoke.withdraw(RESTRICTED_RESERVE, 10, user); + } + + function testFullPtExitPassesDuringSyPause() public { + sy.setPaused(true); + _armPt(); + spoke.withdraw(RESTRICTED_RESERVE, type(uint256).max, user); + } + + function testPartialPtExitTripsDuringSyPause() public { + sy.setPaused(true); + _armPt(); + vm.expectRevert(bytes("AaveV4PTUSDG: redemption disabled before risk increase")); + spoke.withdraw(RESTRICTED_RESERVE, 10, user); + } + + function testDisablePtCollateralPassesDuringSyPause() public { + sy.setPaused(true); + _armPt(); + spoke.setUsingAsCollateral(RESTRICTED_RESERVE, false, user); + } + + function testEnablePtCollateralWithDebtTripsDuringSyPause() public { + spoke.setPosition(user, RESTRICTED_RESERVE, 100, false); + sy.setPaused(true); + _armPt(); + vm.expectRevert(bytes("AaveV4PTUSDG: redemption disabled before risk increase")); + spoke.setUsingAsCollateral(RESTRICTED_RESERVE, true, user); + } + + function testWithdrawGoodCollateralTripsWhenDebtReliesOnStrandedPt() public { + sy.setPaused(true); + _armPt(); + vm.expectRevert(bytes("AaveV4PTUSDG: redemption disabled before risk increase")); + spoke.withdraw(GOOD_RESERVE, 10, user); + } + + function testDisableGoodCollateralTripsWhenDebtReliesOnStrandedPt() public { + usdg.setFrozen(address(sy), true); + _armPt(); + vm.expectRevert(bytes("AaveV4PTUSDG: redemption disabled before risk increase")); + spoke.setUsingAsCollateral(GOOD_RESERVE, false, user); + } + + function testSupplyRemainsOpenWhenPtRedemptionDisabled() public { + sy.setPaused(true); + _armPt(); + vm.expectRevert(bytes("Expected 1 assertion to be executed, but 0 were executed.")); + spoke.supply(GOOD_RESERVE, 1, user); + } + + function testRepayRemainsOpenWhenPtRedemptionDisabled() public { + usdg.setPaused(true); + _armPt(); + vm.expectRevert(bytes("Expected 1 assertion to be executed, but 0 were executed.")); + spoke.repay(GOOD_RESERVE, 1, user); + } + + function testLiquidationRemainsOpenWhenPtRedemptionDisabled() public { + usdg.setFrozen(address(sy), true); + _armPt(); + vm.expectRevert(bytes("Expected 1 assertion to be executed, but 0 were executed.")); + spoke.liquidationCall(RESTRICTED_RESERVE, GOOD_RESERVE, user, 1, true); + } + + function testPtReserveTokenDriftFailsClosed() public { + spoke.setReserveToken(RESTRICTED_RESERVE, goodToken); + _armPt(); + vm.expectRevert(bytes("AaveV4PTUSDG: reserve PT changed")); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testPtReserveHubDriftFailsClosed() public { + spoke.setReserveHub(RESTRICTED_RESERVE, otherHub); + _armPt(); + vm.expectRevert(bytes("AaveV4PTUSDG: reserve Hub changed")); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testPtExternalGetterFailureFailsClosed() public { + sy.setRevertPausedRead(true); + _armPt(); + vm.expectRevert(bytes("AaveV4: fork view failed")); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function _expectPtBorrowFailure(string memory reason) internal { + _armPt(); + vm.expectRevert(bytes(reason)); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function _armPt() internal { + bytes memory createData = abi.encodePacked( + type(AaveV4PTUSDGRedemptionAssertion).creationCode, + abi.encode(address(spoke), RESTRICTED_RESERVE, address(token), hub, address(sy), address(usdg)) + ); + cl.assertion( + address(spoke), createData, AaveV4PTUSDGRedemptionAssertion.assertPtUsdgRedemptionAvailable.selector + ); + } +} + +contract AaveV4ExternalCollateralProductionConfigTest is Test { + function testEthereumMainSpokeWeEthWrapperPinsReviewedPolicy() public { + AaveV4EthereumMainSpokeWeETHTransferabilityAssertion assertion = + new AaveV4EthereumMainSpokeWeETHTransferabilityAssertion(); + + assertEq(assertion.collateralPolicyCount(), 1); + Transferability.CollateralPolicy memory policy = assertion.collateralPolicy(0); + assertEq(policy.reserveId, 2); + assertEq(policy.token, assertion.WEETH()); + assertEq(policy.hub, assertion.CORE_HUB()); + assertEq(policy.statusSource, assertion.WEETH_BLACKLISTER()); + assertEq(uint256(policy.adapter), uint256(Transferability.AdapterKind.WeEth)); + assertEq(assertion.MAIN_SPOKE(), 0x94e7A5dCbE816e498b89aB752661904E2F56c485); + } + + function testEthereumEtherFiSpokeWeEthWrapperPinsReviewedPolicy() public { + AaveV4EthereumEtherFiSpokeWeETHTransferabilityAssertion assertion = + new AaveV4EthereumEtherFiSpokeWeETHTransferabilityAssertion(); + + assertEq(assertion.collateralPolicyCount(), 1); + Transferability.CollateralPolicy memory policy = assertion.collateralPolicy(0); + assertEq(policy.reserveId, 0); + assertEq(policy.token, assertion.WEETH()); + assertEq(policy.hub, assertion.CORE_HUB()); + assertEq(policy.statusSource, assertion.WEETH_BLACKLISTER()); + assertEq(uint256(policy.adapter), uint256(Transferability.AdapterKind.WeEth)); + assertEq(assertion.ETHERFI_ESPOKE(), 0xbF10BDfE177dE0336aFD7fcCF80A904E15386219); + } + + function testEthereumPtUsdgWrapperPinsReviewedRedemptionPath() public { + AaveV4EthereumPTUSDGRedemptionAssertion assertion = new AaveV4EthereumPTUSDGRedemptionAssertion(); + (address configuredSpoke, uint256 reserveId, address pt, address configuredHub, address sy, address usdg) = + assertion.configuration(); + + assertEq(configuredSpoke, assertion.USDG_PENDLE_SPOKE()); + assertEq(reserveId, 0); + assertEq(pt, assertion.PT_USDG_24SEP2026()); + assertEq(configuredHub, assertion.PAXOS_HUB()); + assertEq(sy, assertion.PENDLE_SY_USDG()); + assertEq(usdg, assertion.PAXOS_USDG()); + } +} diff --git a/examples/aave/test/AaveV4HubFlowRateCircuitBreaker.t.sol b/examples/aave/test/AaveV4HubFlowRateCircuitBreaker.t.sol new file mode 100644 index 0000000..5d3a10f --- /dev/null +++ b/examples/aave/test/AaveV4HubFlowRateCircuitBreaker.t.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {CredibleTest} from "../../../src/CredibleTest.sol"; +import { + AaveV4HubFlowRateCircuitBreaker, + AaveV4EthereumCoreHubFlowRateCircuitBreaker, + AaveV4EthereumPrimeHubFlowRateCircuitBreaker +} from "../src/AaveV4HubFlowRateCircuitBreaker.sol"; + +/// @notice Exposes the production Core Hub breaker's pure OR-policy for local behavior tests. +/// @dev Rolling flow and rate contexts are executor-maintained and are not synthesized by local +/// `pcl test`, so these tests exercise the exact internal decision used by the live assertion. +contract CoreFlowRateBreakerHarness is AaveV4EthereumCoreHubFlowRateCircuitBreaker { + function inflowTrips(address token, uint256 currentBps, uint256 peakRateBps) external pure returns (bool) { + return _inflowTrips(token, currentBps, peakRateBps); + } + + function outflowTrips(address token, uint256 currentBps, uint256 peakRateBps) external pure returns (bool) { + return _outflowTrips(token, currentBps, peakRateBps); + } +} + +/// @notice Exposes the production Prime Hub breaker's pure OR-policy for local behavior tests. +contract PrimeFlowRateBreakerHarness is AaveV4EthereumPrimeHubFlowRateCircuitBreaker { + function inflowTrips(address token, uint256 currentBps, uint256 peakRateBps) external pure returns (bool) { + return _inflowTrips(token, currentBps, peakRateBps); + } + + function outflowTrips(address token, uint256 currentBps, uint256 peakRateBps) external pure returns (bool) { + return _outflowTrips(token, currentBps, peakRateBps); + } +} + +/// @notice Call-triggered fixture for real PCL dispatch of the production OR-policy. +/// @dev Local PCL does not synthesize rolling flow contexts, so constructor values stand in for +/// `currentBps` and `peakRateBps`; the fixture still dispatches through `cl.assertion` and +/// executes the same `_inflowTrips` / `_outflowTrips` helpers used by production. +contract ArmedFlowRateBreaker is AaveV4HubFlowRateCircuitBreaker { + address internal constant TEST_TOKEN = address(0xBEEF); + uint256 internal constant TEST_WINDOW_LIMIT_BPS = 100; + uint256 internal constant TEST_PEAK_LIMIT_BPS = 10; + + bool internal immutable testInflow; + uint256 internal immutable testCurrentBps; + uint256 internal immutable testPeakRateBps; + + constructor(address hub_, bool inflow_, uint256 currentBps_, uint256 peakRateBps_) + AaveV4HubFlowRateCircuitBreaker(hub_) + { + testInflow = inflow_; + testCurrentBps = currentBps_; + testPeakRateBps = peakRateBps_; + } + + function triggers() external view override { + registerCallTrigger(this.assertTestPolicy.selector); + } + + function assertTestPolicy() external view { + require(ph.getAssertionAdopter() == HUB, "AaveV4Flow: configured hub is not adopter"); + if (testInflow) { + require(!_inflowTrips(TEST_TOKEN, testCurrentBps, testPeakRateBps), "AaveV4Flow: test inflow breaker"); + } else { + require(!_outflowTrips(TEST_TOKEN, testCurrentBps, testPeakRateBps), "AaveV4Flow: test outflow breaker"); + } + } + + function _flowLimits(address token) internal pure override returns (FlowLimits memory limits) { + require(token == TEST_TOKEN, "AaveV4Flow: unsupported test asset"); + return FlowLimits({ + inflowWindowBps: TEST_WINDOW_LIMIT_BPS, + outflowWindowBps: TEST_WINDOW_LIMIT_BPS, + inflowPeakRateBps: TEST_PEAK_LIMIT_BPS, + outflowPeakRateBps: TEST_PEAK_LIMIT_BPS + }); + } +} + +contract MockAaveV4HubTarget { + uint256 public pokes; + + function poke() external { + pokes++; + } +} + +contract AaveV4HubFlowRateCircuitBreakerTest is Test, CredibleTest { + CoreFlowRateBreakerHarness internal core; + PrimeFlowRateBreakerHarness internal prime; + MockAaveV4HubTarget internal adopter; + + function setUp() public { + core = new CoreFlowRateBreakerHarness(); + prime = new PrimeFlowRateBreakerHarness(); + adopter = new MockAaveV4HubTarget(); + } + + function _arm(bool inflow, uint256 currentBps, uint256 peakRateBps) internal { + bytes memory createData = abi.encodePacked( + type(ArmedFlowRateBreaker).creationCode, abi.encode(address(adopter), inflow, currentBps, peakRateBps) + ); + cl.assertion(address(adopter), createData, ArmedFlowRateBreaker.assertTestPolicy.selector); + } + + // --- production threshold tables ------------------------------------- + + function testCoreWbtcAllowsExactInflowLimits() public view { + assertFalse(core.inflowTrips(core.WBTC(), 1_184, 48)); + } + + function testCoreWbtcTripsOnWindowInflow() public view { + assertTrue(core.inflowTrips(core.WBTC(), 1_185, 48)); + } + + function testCoreWbtcTripsOnPeakInflowRate() public view { + assertTrue(core.inflowTrips(core.WBTC(), 1_184, 49)); + } + + function testCoreUsdgTripsOnWindowOutflow() public view { + assertTrue(core.outflowTrips(core.USDG(), 6_439, 154)); + } + + function testCoreWstethTripsOnPeakOutflowRate() public view { + assertTrue(core.outflowTrips(core.WSTETH(), 932, 95)); + } + + function testPrimeWbtcAllowsExactOutflowLimits() public view { + assertFalse(prime.outflowTrips(prime.WBTC(), 2_367, 178)); + } + + function testPrimeWstethTripsOnWindowInflow() public view { + assertTrue(prime.inflowTrips(prime.WSTETH(), 3_486, 211)); + } + + function testCoreRejectsUnsupportedAsset() public { + vm.expectRevert(bytes("AaveV4Flow: unsupported Core asset")); + core.inflowTrips(makeAddr("unsupported"), 0, 0); + } + + function testPrimeRejectsUnsupportedAsset() public { + vm.expectRevert(bytes("AaveV4Flow: unsupported Prime asset")); + prime.outflowTrips(makeAddr("unsupported"), 0, 0); + } + + function testDeploymentConstants() public view { + assertEq(core.HUB(), core.CORE_HUB()); + assertEq(prime.HUB(), prime.PRIME_HUB()); + assertEq(core.FLOW_WINDOW(), 24 hours); + assertEq(core.DISPATCH_THRESHOLD_BPS(), 1); + } + + // --- dispatched breaker policy --------------------------------------- + + function testDispatchedPolicyAllowsExactLimits() public { + _arm(true, 100, 10); + adopter.poke(); + assertEq(adopter.pokes(), 1); + } + + function testDispatchedPolicyTripsOnWindowLimit() public { + _arm(false, 101, 10); + vm.expectRevert(bytes("AaveV4Flow: test outflow breaker")); + adopter.poke(); + } + + function testDispatchedPolicyTripsOnPeakRateLimit() public { + _arm(true, 100, 11); + vm.expectRevert(bytes("AaveV4Flow: test inflow breaker")); + adopter.poke(); + } +} diff --git a/examples/aave/test/AaveV4OracleConsumptionAssertion.t.sol b/examples/aave/test/AaveV4OracleConsumptionAssertion.t.sol new file mode 100644 index 0000000..60a8542 --- /dev/null +++ b/examples/aave/test/AaveV4OracleConsumptionAssertion.t.sol @@ -0,0 +1,717 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {CredibleTest} from "credible-std/CredibleTest.sol"; +import { + AaveV4EthereumMainSpokeOracleAssertion, + AaveV4OracleConsumptionAssertion +} from "../src/AaveV4OracleConsumptionAssertion.sol"; +import {IAaveV4Spoke} from "../src/AaveV4Interfaces.sol"; + +contract MockV4PriceFeed { + int256 internal answer; + + constructor(int256 answer_) { + answer = answer_; + } + + function setAnswer(int256 answer_) external { + answer = answer_; + } + + function latestAnswer() external view returns (int256) { + return answer; + } + + function decimals() external pure returns (uint8) { + return 8; + } +} + +contract MockV4AdapterFeed { + MockV4PriceFeed public innerSource; + + constructor(MockV4PriceFeed innerSource_) { + innerSource = innerSource_; + } + + function setInnerSource(MockV4PriceFeed innerSource_) external { + innerSource = innerSource_; + } + + function latestAnswer() external view returns (int256) { + return innerSource.latestAnswer(); + } + + function decimals() external pure returns (uint8) { + return 8; + } +} + +/// @dev Storage deliberately matches Aave v4.0.0 v0.5.11 AaveOracle: +/// `spoke` is slot 0 and `_sources` is the uint256-keyed mapping at slot 1. +contract MockV4Oracle { + address public spoke; + mapping(uint256 reserveId => address source) internal sources; + + constructor(address spoke_) { + spoke = spoke_; + } + + function setSpoke(address spoke_) external { + spoke = spoke_; + } + + function decimals() external pure returns (uint8) { + return 8; + } + + function setReserveSource(uint256 reserveId, address source) external { + require(MockV4PriceFeed(source).decimals() == 8, "bad source decimals"); + require(MockV4PriceFeed(source).latestAnswer() > 0, "bad source answer"); + sources[reserveId] = source; + } + + function getReserveSource(uint256 reserveId) external view returns (address) { + return sources[reserveId]; + } + + function getReservePrice(uint256 reserveId) public view returns (uint256) { + address source = sources[reserveId]; + require(source != address(0), "source not set"); + int256 price = MockV4PriceFeed(source).latestAnswer(); + require(price > 0, "invalid price"); + return uint256(price); + } + + function getReservesPrices(uint256[] calldata reserveIds) external view returns (uint256[] memory prices) { + prices = new uint256[](reserveIds.length); + for (uint256 i; i < reserveIds.length; ++i) { + prices[i] = getReservePrice(reserveIds[i]); + } + } +} + +contract MockV4Spoke { + address public immutable ORACLE; + IAaveV4Spoke.Reserve[] internal reserves; + bool internal readUnknownReserve; + + constructor(address oracle_) { + ORACLE = oracle_; + } + + function addReserve(address asset) external { + reserves.push( + IAaveV4Spoke.Reserve({ + underlying: asset, + hub: address(0xBEEF), + assetId: uint16(reserves.length), + decimals: 18, + collateralRisk: 0, + flags: 0, + dynamicConfigKey: 0 + }) + ); + } + + function setReadUnknownReserve(bool enabled) external { + readUnknownReserve = enabled; + } + + function getReserveCount() external view returns (uint256) { + return reserves.length; + } + + function getReserve(uint256 reserveId) external view returns (IAaveV4Spoke.Reserve memory) { + return reserves[reserveId]; + } + + function supply(uint256, uint256, address) external pure returns (uint256, uint256) { + return (0, 0); + } + + function repay(uint256, uint256, address) external pure returns (uint256, uint256) { + return (0, 0); + } + + function withdraw(uint256, uint256 amount, address) external view returns (uint256, uint256) { + _consumePrices(); + return (amount, 0); + } + + function borrow(uint256, uint256 amount, address) external view returns (uint256, uint256) { + _consumePrices(); + require(amount != type(uint256).max, "forced post-price revert"); + return (amount, 0); + } + + function liquidationCall(uint256, uint256, address, uint256, bool) external view { + _consumePrices(); + } + + function setUsingAsCollateral(uint256, bool usingAsCollateral, address) external view { + if (!usingAsCollateral) { + _consumePrices(); + } + } + + function updateUserRiskPremium(address) external view { + _consumePrices(); + } + + function updateUserDynamicConfig(address) external view { + _consumePrices(); + } + + function multicall(bytes[] calldata calls) external returns (bytes[] memory results) { + results = new bytes[](calls.length); + for (uint256 i; i < calls.length; ++i) { + (bool success, bytes memory result) = address(this).delegatecall(calls[i]); + require(success, "multicall leg failed"); + results[i] = result; + } + } + + function _consumePrices() internal view { + MockV4Oracle oracle = MockV4Oracle(ORACLE); + for (uint256 i; i < reserves.length; ++i) { + oracle.getReservePrice(i); + } + if (readUnknownReserve) { + oracle.getReservePrice(reserves.length); + } + } +} + +interface INestedV4Callback { + function execute() external; +} + +contract NestedV4CallbackReceiver is INestedV4Callback { + MockV4PriceFeed internal immutable source; + MockV4Spoke internal immutable spoke; + int256 internal immutable temporaryPrice; + int256 internal immutable restoredPrice; + + constructor(MockV4PriceFeed source_, MockV4Spoke spoke_, int256 temporaryPrice_, int256 restoredPrice_) { + source = source_; + spoke = spoke_; + temporaryPrice = temporaryPrice_; + restoredPrice = restoredPrice_; + } + + function execute() external { + source.setAnswer(temporaryPrice); + spoke.borrow(0, 1, address(this)); + source.setAnswer(restoredPrice); + } +} + +contract V4OracleScenarioDriver { + function priceBorrowRestore(MockV4PriceFeed source, int256 temporaryPrice, int256 restoredPrice, MockV4Spoke spoke) + external + { + source.setAnswer(temporaryPrice); + spoke.borrow(0, 1, address(this)); + source.setAnswer(restoredPrice); + } + + function sourceBorrowRestore( + MockV4Oracle oracle, + uint256 reserveId, + address temporarySource, + address restoredSource, + MockV4Spoke spoke + ) external { + oracle.setReserveSource(reserveId, temporarySource); + spoke.borrow(0, 1, address(this)); + oracle.setReserveSource(reserveId, restoredSource); + } + + function adapterBorrowRestore( + MockV4AdapterFeed adapter, + MockV4PriceFeed temporaryInnerSource, + MockV4PriceFeed restoredInnerSource, + MockV4Spoke spoke + ) external { + adapter.setInnerSource(temporaryInnerSource); + spoke.borrow(0, 1, address(this)); + adapter.setInnerSource(restoredInnerSource); + } + + function twoBorrows(MockV4Spoke spoke) external { + spoke.borrow(0, 1, address(this)); + spoke.borrow(1, 1, address(this)); + } + + function manipulatedMulticall( + MockV4PriceFeed source, + int256 temporaryPrice, + int256 restoredPrice, + MockV4Spoke spoke + ) external { + source.setAnswer(temporaryPrice); + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(MockV4Spoke.borrow, (0, 1, address(this))); + calls[1] = abi.encodeCall(MockV4Spoke.updateUserRiskPremium, (address(this))); + spoke.multicall(calls); + source.setAnswer(restoredPrice); + } + + function successfulThenCaughtRevertedBorrow( + MockV4Spoke spoke, + MockV4PriceFeed source, + int256 temporaryPrice, + int256 restoredPrice + ) external { + spoke.borrow(0, 1, address(this)); + source.setAnswer(temporaryPrice); + (bool success,) = address(spoke).call(abi.encodeCall(MockV4Spoke.borrow, (0, type(uint256).max, address(this)))); + require(!success, "forced borrow unexpectedly succeeded"); + source.setAnswer(restoredPrice); + } + + function invokeCallback(INestedV4Callback receiver) external { + receiver.execute(); + } +} + +contract MockV4SpokeImplementation { + address public immutable ORACLE; + address internal immutable ASSET; + + constructor(address oracle_, address asset_) { + ORACLE = oracle_; + ASSET = asset_; + } + + function getReserveCount() external pure returns (uint256) { + return 1; + } + + function getReserve(uint256 reserveId) external view returns (IAaveV4Spoke.Reserve memory) { + require(reserveId == 0, "unknown reserve"); + return IAaveV4Spoke.Reserve({ + underlying: ASSET, + hub: address(0xBEEF), + assetId: 0, + decimals: 18, + collateralRisk: 0, + flags: 0, + dynamicConfigKey: 0 + }); + } + + function borrow(uint256, uint256 amount, address) external view returns (uint256, uint256) { + MockV4Oracle(ORACLE).getReservePrice(0); + return (amount, 0); + } +} + +contract MinimalV4SpokeProxy { + bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + constructor(address implementation_) { + _setImplementation(implementation_); + } + + function upgradeTo(address implementation_) external { + _setImplementation(implementation_); + } + + function _setImplementation(address implementation_) internal { + bytes32 slot = IMPLEMENTATION_SLOT; + assembly ("memory-safe") { + sstore(slot, implementation_) + } + } + + fallback() external payable { + bytes32 slot = IMPLEMENTATION_SLOT; + assembly ("memory-safe") { + let implementation := sload(slot) + calldatacopy(0, 0, calldatasize()) + let success := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + if iszero(success) { revert(0, returndatasize()) } + return(0, returndatasize()) + } + } +} + +contract V4UpgradeScenarioDriver { + function upgradeBorrowRestore( + MinimalV4SpokeProxy proxy, + address temporaryImplementation, + address restoredImplementation + ) external { + proxy.upgradeTo(temporaryImplementation); + IAaveV4Spoke(address(proxy)).borrow(0, 1, address(this)); + proxy.upgradeTo(restoredImplementation); + } +} + +contract AaveV4OracleConsumptionAssertionTest is Test, CredibleTest { + uint256 internal constant MAX_TRACE_CALLS = 32; + uint256 internal constant DEVIATION_BPS = 100; + int256 internal constant PRICE = 100_00000000; + + address internal asset0 = makeAddr("v4 asset 0"); + address internal asset1 = makeAddr("v4 asset 1"); + + MockV4Oracle internal oracle; + MockV4Spoke internal spoke; + MockV4PriceFeed internal source0; + MockV4PriceFeed internal source1; + MockV4PriceFeed internal temporarySource; + V4OracleScenarioDriver internal driver; + + function setUp() public { + oracle = new MockV4Oracle(address(0)); + spoke = new MockV4Spoke(address(oracle)); + oracle.setSpoke(address(spoke)); + + source0 = new MockV4PriceFeed(PRICE); + source1 = new MockV4PriceFeed(PRICE); + temporarySource = new MockV4PriceFeed(2 * PRICE); + oracle.setReserveSource(0, address(source0)); + oracle.setReserveSource(1, address(source1)); + spoke.addReserve(asset0); + spoke.addReserve(asset1); + driver = new V4OracleScenarioDriver(); + } + + function testHonestStablePriceOperation() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + spoke.borrow(0, 1, address(this)); + } + + function testHonestWithdrawPricePath() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + spoke.withdraw(0, 1, address(this)); + } + + function testHonestLiquidationPricePath() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + spoke.liquidationCall(0, 1, address(this), 1, false); + } + + function testHonestCollateralDisablePricePath() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + spoke.setUsingAsCollateral(0, false, address(this)); + } + + function testHonestRiskPremiumRefreshPricePath() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + spoke.updateUserRiskPremium(address(this)); + } + + function testHonestDynamicConfigRefreshPricePath() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + spoke.updateUserDynamicConfig(address(this)); + } + + function testNonPriceCollateralEnablePathReturnsCleanly() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + spoke.setUsingAsCollateral(0, true, address(this)); + } + + function testSupplyIsCorrectlyOmittedAsNonPriceOperation() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + spoke.supply(0, 1, address(this)); + } + + function testRepayIsCorrectlyOmittedAsNonPriceOperation() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + spoke.repay(0, 1, address(this)); + } + + function testEthereumWrapperPinsCompleteReserveAndConfigPolicy() public { + uint256[14] memory tolerances; + for (uint256 i; i < tolerances.length; ++i) { + tolerances[i] = i; + } + AaveV4OracleConsumptionAssertion.ConfigSlotGuard[] memory extra = + new AaveV4OracleConsumptionAssertion.ConfigSlotGuard[](0); + AaveV4EthereumMainSpokeOracleAssertion production = + new AaveV4EthereumMainSpokeOracleAssertion(64, tolerances, extra); + + assertEq(production.reservePolicyCount(), 14); + assertEq(production.configSlotGuardCount(), 22); + for (uint256 i; i < tolerances.length; ++i) { + AaveV4OracleConsumptionAssertion.ReservePolicy memory policy = production.reservePolicy(i); + assertEq(policy.reserveId, i); + assertEq(policy.deviationBps, i); + assertTrue(policy.asset != address(0)); + assertTrue(policy.source != address(0)); + } + } + + function testTemporaryFeedValueManipulationAndRestorationTrips() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + vm.expectRevert(bytes("AaveV4Oracle: consumed price deviated")); + driver.priceBorrowRestore(source1, 2 * PRICE, PRICE, spoke); + } + + function testTemporarySourceReplacementAndRestorationTrips() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + vm.expectRevert(bytes("AaveV4Oracle: reserve source written")); + driver.sourceBorrowRestore(oracle, 1, address(temporarySource), address(source1), spoke); + } + + function testTemporaryAdapterConfigurationAndRestorationTrips() public { + MockV4AdapterFeed adapter = new MockV4AdapterFeed(source1); + oracle.setReserveSource(1, address(adapter)); + + AaveV4OracleConsumptionAssertion.ReservePolicy[] memory policies = _twoPolicies(DEVIATION_BPS, address(adapter)); + AaveV4OracleConsumptionAssertion.ConfigSlotGuard[] memory configGuards = + new AaveV4OracleConsumptionAssertion.ConfigSlotGuard[](1); + configGuards[0] = AaveV4OracleConsumptionAssertion.ConfigSlotGuard({target: address(adapter), slot: bytes32(0)}); + + _armCustom(address(spoke), address(oracle), address(0), MAX_TRACE_CALLS, policies, configGuards); + vm.expectRevert(bytes("AaveV4Oracle: guarded config written")); + driver.adapterBorrowRestore(adapter, temporarySource, source1, spoke); + } + + function testTemporarySpokeImplementationAndOracleRestorationTrips() public { + address proxyAsset = makeAddr("proxy asset"); + MockV4Oracle canonicalOracle = new MockV4Oracle(address(0)); + MockV4Oracle temporaryOracle = new MockV4Oracle(address(0)); + MockV4PriceFeed canonicalSource = new MockV4PriceFeed(PRICE); + MockV4PriceFeed manipulatedSource = new MockV4PriceFeed(2 * PRICE); + canonicalOracle.setReserveSource(0, address(canonicalSource)); + temporaryOracle.setReserveSource(0, address(manipulatedSource)); + + MockV4SpokeImplementation canonicalImplementation = + new MockV4SpokeImplementation(address(canonicalOracle), proxyAsset); + MockV4SpokeImplementation temporaryImplementation = + new MockV4SpokeImplementation(address(temporaryOracle), proxyAsset); + MinimalV4SpokeProxy proxy = new MinimalV4SpokeProxy(address(canonicalImplementation)); + canonicalOracle.setSpoke(address(proxy)); + temporaryOracle.setSpoke(address(proxy)); + V4UpgradeScenarioDriver upgradeDriver = new V4UpgradeScenarioDriver(); + + AaveV4OracleConsumptionAssertion.ReservePolicy[] memory policies = + new AaveV4OracleConsumptionAssertion.ReservePolicy[](1); + policies[0] = AaveV4OracleConsumptionAssertion.ReservePolicy({ + reserveId: 0, asset: proxyAsset, source: address(canonicalSource), deviationBps: DEVIATION_BPS + }); + AaveV4OracleConsumptionAssertion.ConfigSlotGuard[] memory configGuards = + new AaveV4OracleConsumptionAssertion.ConfigSlotGuard[](0); + _armCustom( + address(proxy), + address(canonicalOracle), + address(canonicalImplementation), + MAX_TRACE_CALLS, + policies, + configGuards + ); + + vm.expectRevert(bytes("AaveV4Oracle: implementation written")); + upgradeDriver.upgradeBorrowRestore(proxy, address(temporaryImplementation), address(canonicalImplementation)); + } + + function testNestedCallbackManipulationAndRestorationTrips() public { + NestedV4CallbackReceiver receiver = new NestedV4CallbackReceiver(source1, spoke, 2 * PRICE, PRICE); + + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + vm.expectRevert(bytes("AaveV4Oracle: consumed price deviated")); + driver.invokeCallback(receiver); + } + + function testMulticallAndMultipleOperationsTripOnManipulatedPrice() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + vm.expectRevert(bytes("AaveV4Oracle: consumed price deviated")); + driver.manipulatedMulticall(source1, 2 * PRICE, PRICE, spoke); + } + + function testSingleReserveStableMulticallPasses() public { + MockV4Oracle oneOracle = new MockV4Oracle(address(0)); + MockV4Spoke oneSpoke = new MockV4Spoke(address(oneOracle)); + MockV4PriceFeed oneSource = new MockV4PriceFeed(PRICE); + address oneAsset = makeAddr("single reserve multicall asset"); + oneOracle.setSpoke(address(oneSpoke)); + oneOracle.setReserveSource(0, address(oneSource)); + oneSpoke.addReserve(oneAsset); + + AaveV4OracleConsumptionAssertion.ReservePolicy[] memory policies = + new AaveV4OracleConsumptionAssertion.ReservePolicy[](1); + policies[0] = AaveV4OracleConsumptionAssertion.ReservePolicy({ + reserveId: 0, asset: oneAsset, source: address(oneSource), deviationBps: DEVIATION_BPS + }); + AaveV4OracleConsumptionAssertion.ConfigSlotGuard[] memory configGuards = + new AaveV4OracleConsumptionAssertion.ConfigSlotGuard[](0); + _armCustom(address(oneSpoke), address(oneOracle), address(0), MAX_TRACE_CALLS, policies, configGuards); + driver.twoBorrows(oneSpoke); + } + + /// @dev The installed PCL 1.6.0 runner uses a legacy 300k test ceiling. The assertion's + /// measured 330k execution is below the production 3m ceiling but is rejected locally. + function testTwoReserveMultipleOperationsMeasuresAboveLegacyLocalCeiling() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + vm.expectRevert(bytes("Assertion exceeded gas limit")); + driver.twoBorrows(spoke); + } + + function testCaughtRevertedPriceConsumingSubcallIsExcluded() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + driver.successfulThenCaughtRevertedBorrow(spoke, source1, 2 * PRICE, PRICE); + } + + function testIncompletePolicyConfigurationFailsClosed() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 1); + vm.expectRevert(bytes("AaveV4Oracle: incomplete reserve policy")); + spoke.borrow(0, 1, address(this)); + } + + function testUnknownPriceConsumptionPathFailsClosed() public { + MockV4PriceFeed source2 = new MockV4PriceFeed(PRICE); + oracle.setReserveSource(2, address(source2)); + spoke.setReadUnknownReserve(true); + + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + vm.expectRevert(bytes("AaveV4Oracle: unconfigured price path")); + spoke.borrow(0, 1, address(this)); + } + + function testTraceBoundExhaustionFailsClosed() public { + _arm(1, DEVIATION_BPS, 2); + vm.expectRevert(bytes("AaveV4Oracle: trace limit exceeded")); + spoke.borrow(0, 1, address(this)); + } + + function testMovementInsideConfiguredTolerancePasses() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + driver.priceBorrowRestore(source1, 100_50000000, PRICE, spoke); + } + + function testMovementOutsideConfiguredToleranceTrips() public { + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + vm.expectRevert(bytes("AaveV4Oracle: consumed price deviated")); + driver.priceBorrowRestore(source1, 101_00000001, PRICE, spoke); + } + + function testZeroToleranceRequiresExactStability() public { + _arm(MAX_TRACE_CALLS, 0, 2); + vm.expectRevert(bytes("AaveV4Oracle: consumed price deviated")); + driver.priceBorrowRestore(source1, PRICE + 1, PRICE, spoke); + } + + /// @dev Same-transaction protection cannot identify a baseline corrupted before PreTx. + function testPreExistingManipulationIsDocumentedFalseNegative() public { + source1.setAnswer(2 * PRICE); + + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + spoke.borrow(0, 1, address(this)); + } + + /// @dev Expected local-ceiling rejection still executes and reports the complete assertion gas. + function testRealisticFourteenReserveGasScaling() public { + ( + MockV4Oracle largeOracle, + MockV4Spoke largeSpoke, + AaveV4OracleConsumptionAssertion.ReservePolicy[] memory policies + ) = _largeFixture(); + AaveV4OracleConsumptionAssertion.ConfigSlotGuard[] memory configGuards = _realisticConfigGuards(); + + _armCustom(address(largeSpoke), address(largeOracle), address(0), 64, policies, configGuards); + vm.expectRevert(bytes("Assertion exceeded gas limit")); + largeSpoke.borrow(0, 1, address(this)); + } + + /// @dev Two full 14-reserve price sweeps approximate a realistic V4 multicall. + function testRealisticFourteenReserveMulticallGasScaling() public { + ( + MockV4Oracle largeOracle, + MockV4Spoke largeSpoke, + AaveV4OracleConsumptionAssertion.ReservePolicy[] memory policies + ) = _largeFixture(); + AaveV4OracleConsumptionAssertion.ConfigSlotGuard[] memory configGuards = _realisticConfigGuards(); + + _armCustom(address(largeSpoke), address(largeOracle), address(0), 64, policies, configGuards); + vm.expectRevert(bytes("Assertion exceeded gas limit")); + driver.twoBorrows(largeSpoke); + } + + function _largeFixture() + internal + returns ( + MockV4Oracle largeOracle, + MockV4Spoke largeSpoke, + AaveV4OracleConsumptionAssertion.ReservePolicy[] memory policies + ) + { + largeOracle = new MockV4Oracle(address(0)); + largeSpoke = new MockV4Spoke(address(largeOracle)); + largeOracle.setSpoke(address(largeSpoke)); + + policies = new AaveV4OracleConsumptionAssertion.ReservePolicy[](14); + for (uint256 i; i < 14; ++i) { + address asset = address(uint160(0x1000 + i)); + MockV4PriceFeed source = new MockV4PriceFeed(PRICE + int256(i)); + largeOracle.setReserveSource(i, address(source)); + largeSpoke.addReserve(asset); + policies[i] = AaveV4OracleConsumptionAssertion.ReservePolicy({ + reserveId: i, asset: asset, source: address(source), deviationBps: DEVIATION_BPS + }); + } + } + + function _realisticConfigGuards() + internal + pure + returns (AaveV4OracleConsumptionAssertion.ConfigSlotGuard[] memory guards) + { + guards = new AaveV4OracleConsumptionAssertion.ConfigSlotGuard[](22); + for (uint256 i; i < guards.length; ++i) { + guards[i] = AaveV4OracleConsumptionAssertion.ConfigSlotGuard({ + target: address(uint160(0x2000 + i)), slot: bytes32(uint256(2)) + }); + } + } + + function _arm(uint256 maxTraceCalls, uint256 deviationBps, uint256 policyCount) internal { + AaveV4OracleConsumptionAssertion.ReservePolicy[] memory policies = + new AaveV4OracleConsumptionAssertion.ReservePolicy[](policyCount); + AaveV4OracleConsumptionAssertion.ReservePolicy[] memory fullPolicies = + _twoPolicies(deviationBps, address(source1)); + for (uint256 i; i < policyCount; ++i) { + policies[i] = fullPolicies[i]; + } + AaveV4OracleConsumptionAssertion.ConfigSlotGuard[] memory configGuards = + new AaveV4OracleConsumptionAssertion.ConfigSlotGuard[](0); + _armCustom(address(spoke), address(oracle), address(0), maxTraceCalls, policies, configGuards); + } + + function _twoPolicies(uint256 deviationBps, address secondSource) + internal + view + returns (AaveV4OracleConsumptionAssertion.ReservePolicy[] memory policies) + { + policies = new AaveV4OracleConsumptionAssertion.ReservePolicy[](2); + policies[0] = AaveV4OracleConsumptionAssertion.ReservePolicy({ + reserveId: 0, asset: asset0, source: address(source0), deviationBps: deviationBps + }); + policies[1] = AaveV4OracleConsumptionAssertion.ReservePolicy({ + reserveId: 1, asset: asset1, source: secondSource, deviationBps: deviationBps + }); + } + + function _armCustom( + address spoke_, + address oracle_, + address expectedImplementation, + uint256 maxTraceCalls, + AaveV4OracleConsumptionAssertion.ReservePolicy[] memory policies, + AaveV4OracleConsumptionAssertion.ConfigSlotGuard[] memory configGuards + ) internal { + bytes memory createData = abi.encodePacked( + type(AaveV4OracleConsumptionAssertion).creationCode, + abi.encode(spoke_, oracle_, expectedImplementation, maxTraceCalls, policies, configGuards) + ); + cl.assertion(spoke_, createData, AaveV4OracleConsumptionAssertion.assertConsumedOraclePricesSafe.selector); + } +} From 15f9d2c62f87bb3e608a7278f4798c81b350d1d8 Mon Sep 17 00:00:00 2001 From: makemake Date: Mon, 3 Aug 2026 19:43:49 +0200 Subject: [PATCH 3/9] fix(aave): support legacy weETH implementation --- ...rnalCollateralTransferabilityAssertion.sol | 18 +++++++++ .../AaveV4ExternalCollateralAssertions.t.sol | 39 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/examples/aave/src/AaveV4ExternalCollateralTransferabilityAssertion.sol b/examples/aave/src/AaveV4ExternalCollateralTransferabilityAssertion.sol index 26b370e..9264f57 100644 --- a/examples/aave/src/AaveV4ExternalCollateralTransferabilityAssertion.sol +++ b/examples/aave/src/AaveV4ExternalCollateralTransferabilityAssertion.sol @@ -47,6 +47,14 @@ contract AaveV4ExternalCollateralTransferabilityAssertion is AaveV4ExternalColla // local executor limit. Deploy one instance per reserve instead of creating an unsafe bundle. uint256 internal constant MAX_POLICY_COUNT = 1; bytes32 internal constant FULL_RESTRICTED_STAKER_ROLE = keccak256("FULL_RESTRICTED_STAKER_ROLE"); + bytes32 internal constant ERC1967_IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + // Verified Ethereum weETH implementations. The legacy implementation had no transfer pause or + // blacklist hooks. The restricted implementation added paused(), pausedUntil(), and the + // external Blacklister hook. Unknown proxy implementations fail closed below. + address internal constant WEETH_LEGACY_UNRESTRICTED_IMPLEMENTATION = 0x2d10683E941275D502173053927AD6066e6aFd6B; + address internal constant WEETH_RESTRICTED_IMPLEMENTATION = 0xA6Ca0607190d03CF16fe6F2865Cf40c3D160ccf3; address internal immutable SPOKE; CollateralPolicy[] internal collateralPolicies; @@ -153,6 +161,16 @@ contract AaveV4ExternalCollateralTransferabilityAssertion is AaveV4ExternalColla } if (policy.adapter == AdapterKind.WeEth) { + address implementation = + address(uint160(uint256(ph.loadStateAt(policy.token, ERC1967_IMPLEMENTATION_SLOT, fork)))); + if (implementation == WEETH_LEGACY_UNRESTRICTED_IMPLEMENTATION) { + return true; + } + require( + implementation == address(0) || implementation == WEETH_RESTRICTED_IMPLEMENTATION, + "AaveV4Transferability: unsupported weETH implementation" + ); + bool indefinitePause = _pausedAt(policy.token, fork); uint256 timedPause = _readUintAt(policy.token, abi.encodeCall(IExternalTimedPausable.pausedUntil, ()), fork); uint256 hubBlacklist = _readUintAt( diff --git a/examples/aave/test/AaveV4ExternalCollateralAssertions.t.sol b/examples/aave/test/AaveV4ExternalCollateralAssertions.t.sol index 9165221..a6cdcd0 100644 --- a/examples/aave/test/AaveV4ExternalCollateralAssertions.t.sol +++ b/examples/aave/test/AaveV4ExternalCollateralAssertions.t.sol @@ -213,6 +213,10 @@ abstract contract AaveV4ExternalCollateralTestBase is Test, CredibleTest { uint256 internal constant GOOD_RESERVE = 1; uint256 internal constant INITIAL_DEBT_RAY = 100e27; bytes32 internal constant FULL_RESTRICTED_STAKER_ROLE = keccak256("FULL_RESTRICTED_STAKER_ROLE"); + bytes32 internal constant ERC1967_IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + address internal constant WEETH_LEGACY_UNRESTRICTED_IMPLEMENTATION = 0x2d10683E941275D502173053927AD6066e6aFd6B; + address internal constant WEETH_RESTRICTED_IMPLEMENTATION = 0xA6Ca0607190d03CF16fe6F2865Cf40c3D160ccf3; address internal user = makeAddr("borrower"); address internal hub = makeAddr("Aave Hub"); @@ -310,6 +314,41 @@ contract AaveV4ExternalCollateralTransferabilityAssertionTest is AaveV4ExternalC spoke.borrow(GOOD_RESERVE, 1, user); } + function testLegacyUnrestrictedWeEthImplementationDoesNotRequireNewStatusAbi() public { + vm.store( + address(token), + ERC1967_IMPLEMENTATION_SLOT, + bytes32(uint256(uint160(WEETH_LEGACY_UNRESTRICTED_IMPLEMENTATION))) + ); + token.setRevertPausedRead(true); + + _arm(Transferability.AdapterKind.WeEth, address(blacklister)); + spoke.borrow(GOOD_RESERVE, 1, user); + } + + function testRestrictedWeEthImplementationStillChecksPauseState() public { + vm.store( + address(token), ERC1967_IMPLEMENTATION_SLOT, bytes32(uint256(uint160(WEETH_RESTRICTED_IMPLEMENTATION))) + ); + token.setPaused(true); + + _expectBorrowFailure( + Transferability.AdapterKind.WeEth, + address(blacklister), + "AaveV4Transferability: collateral restricted before risk increase" + ); + } + + function testUnknownWeEthImplementationFailsClosed() public { + vm.store(address(token), ERC1967_IMPLEMENTATION_SLOT, bytes32(uint256(uint160(makeAddr("unknown weETH impl"))))); + + _expectBorrowFailure( + Transferability.AdapterKind.WeEth, + address(blacklister), + "AaveV4Transferability: unsupported weETH implementation" + ); + } + function testBlockedHubTrips() public { token.setBlocked(hub, true); _expectBorrowFailure( From 08b93593c0508a6463150d2091c824d00151002c Mon Sep 17 00:00:00 2001 From: makemake Date: Tue, 4 Aug 2026 13:45:52 +0200 Subject: [PATCH 4/9] refactor(aave): remove v4 external-collateral reduce-only assertions Drop AaveV4ExternalCollateralTransferabilityAssertion and AaveV4PTUSDGRedemptionAssertion with their shared helpers and test suite. Both gate new risk after an issuer-side freeze rather than preventing the impairment itself, so the protected loss is largely already realized; coverage focus shifts to the Hub flow-rate circuit breakers. Also brings the README v4 sections up to date. --- examples/aave/README.md | 30 + .../src/AaveV4ExternalCollateralHelpers.sol | 150 ---- ...rnalCollateralTransferabilityAssertion.sol | 235 ------ .../src/AaveV4PTUSDGRedemptionAssertion.sol | 116 --- .../AaveV4ExternalCollateralAssertions.t.sol | 793 ------------------ 5 files changed, 30 insertions(+), 1294 deletions(-) delete mode 100644 examples/aave/src/AaveV4ExternalCollateralHelpers.sol delete mode 100644 examples/aave/src/AaveV4ExternalCollateralTransferabilityAssertion.sol delete mode 100644 examples/aave/src/AaveV4PTUSDGRedemptionAssertion.sol delete mode 100644 examples/aave/test/AaveV4ExternalCollateralAssertions.t.sol diff --git a/examples/aave/README.md b/examples/aave/README.md index 644a50b..8062343 100644 --- a/examples/aave/README.md +++ b/examples/aave/README.md @@ -16,7 +16,10 @@ FOUNDRY_PROFILE=aave forge build - AaveV3HorizonReserveBackingAssertion.sol - AaveV4Helpers.sol - AaveV4HubAccountingAssertion.sol +- AaveV4HubFlowRateCircuitBreaker.sol - AaveV4Interfaces.sol +- AaveV4OracleConsumptionAssertion.sol +- AaveV4OracleConsumptionHelpers.sol - AaveV4SpokeRiskAssertion.sol ## Aave v3 Horizon oracle guard @@ -36,3 +39,30 @@ The source/fallback storage guards are pinned to the Aave v3.3 `AaveOracle` layout (`assetsSources` mapping slot 0 and `_fallbackOracle` slot 1). Re-verify those slots before adopting the assertion against a different oracle implementation or storage layout. + +## Aave v4 consumed-oracle guard + +`AaveV4OracleConsumptionAssertion` checks every committed Spoke-to-AaveOracle +price return against a per-reserve PreTx envelope. It maps the exact parent +oracle output through its direct `latestAnswer()` child, rejects routing and +adapter write-and-restore sequences, validates complete reserve policy, and +uses one bounded transaction-end scan for nested and multicall flows. + +`AaveV4EthereumMainSpokeOracleAssertion` pins the live Ethereum Main Spoke, +implementation, oracle, 14 reserves, direct sources, and 22 verified mutable +source-graph slots at block 25,646,732. + +See the +[deployment guide](AAVE_V4_ORACLE_ASSERTION_DEPLOYMENT.md) and +[pinned research and trace analysis](research/aave-v4-oracle-consumption-protection-2026-07-30.md). + +## Aave v4 flow-rate calibration + +The ready-to-adopt Core and Prime Hub circuit breakers cover WBTC, USDG, and +wstETH, the three highest-TVL Aave v4 assets in the 2026-07-30 DefiLlama +snapshot. Their 24-hour cumulative-flow and 10-second peak-rate limits use a +20% buffer over the maximum observed values in the preceding 30 days. + +See +[aave-v4-flow-rate-calibration-2026-07-30.md](research/aave-v4-flow-rate-calibration-2026-07-30.md) +for the block range, measurements, and operational caveats. diff --git a/examples/aave/src/AaveV4ExternalCollateralHelpers.sol b/examples/aave/src/AaveV4ExternalCollateralHelpers.sol deleted file mode 100644 index 74b709b..0000000 --- a/examples/aave/src/AaveV4ExternalCollateralHelpers.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; - -import {PhEvm} from "credible-std/PhEvm.sol"; - -import {AaveV4Helpers} from "./AaveV4Helpers.sol"; -import {IAaveV4Spoke} from "./AaveV4Interfaces.sol"; - -/// @notice ERC20 pause surface used by independently administered collateral and redemption tokens. -interface IExternalPausable { - function paused() external view returns (bool); -} - -/// @notice ether.fi timed-pause surface used by weETH. -interface IExternalTimedPausable { - function pausedUntil() external view returns (uint256); -} - -/// @notice ether.fi Blacklister surface used by weETH transfer hooks. -interface IExternalTimedBlacklist { - function blacklistedUntil(address account) external view returns (uint256); -} - -/// @notice Circle/Coinbase-style blacklist surface. -interface IExternalBlacklist { - function isBlacklisted(address account) external view returns (bool); -} - -/// @notice Tether-style blacklist surface. The capital `L` is part of the deployed ABI. -interface IExternalTetherBlacklist { - function isBlackListed(address account) external view returns (bool); -} - -/// @notice Tether Gold-style blocked-account surface. -interface IExternalBlockedAccount { - function isBlocked(address account) external view returns (bool); -} - -/// @notice AccessControl surface used by sUSDe transfer restrictions. -interface IExternalAccessControl { - function hasRole(bytes32 role, address account) external view returns (bool); -} - -/// @notice Paxos freeze surface used by USDG. -interface IExternalFrozenAccount { - function isFrozen(address account) external view returns (bool); -} - -/// @title AaveV4ExternalCollateralHelpers -/// @author Phylax Systems -/// @notice Shared call decoding and Aave exposure-scoping for external collateral assertions. -/// @dev Aave state is read only to decide whether the triggering user is increasing risk and -/// still relies on a configured collateral reserve. The protected facts themselves come -/// from independently administered token, issuer, and redemption contracts. -abstract contract AaveV4ExternalCollateralHelpers is AaveV4Helpers { - /// @notice Decodes the affected user and rejects operations that do not increase collateral risk. - /// @dev Borrow always increases debt. Withdraw and collateral-toggle paths are relevant only - /// when the user retains debt; non-collateral withdrawals are skipped. Collateral disable - /// remains checked because disabling good collateral can leave debt relying on an externally - /// frozen asset. A complete disable/exit of the frozen asset is allowed by the later - /// post-call reliance check. - function _riskIncreasingUser(address spoke, PhEvm.TriggerContext memory ctx) - internal - view - returns (address user, bool riskIncreasing) - { - bytes memory input = ph.callinputAt(ctx.callStart); - PhEvm.ForkId memory preCall = _preCall(ctx.callStart); - PhEvm.ForkId memory postCall = _postCall(ctx.callEnd); - - if (ctx.selector == IAaveV4Spoke.borrow.selector) { - (,, user) = abi.decode(_args(input), (uint256, uint256, address)); - return (user, true); - } - - if (ctx.selector == IAaveV4Spoke.withdraw.selector) { - uint256 reserveId; - (reserveId,, user) = abi.decode(_args(input), (uint256, uint256, address)); - if (!_hasDebtAt(spoke, user, postCall)) { - return (user, false); - } - return (user, _isActiveCollateralAt(spoke, reserveId, user, preCall)); - } - - if (ctx.selector == IAaveV4Spoke.setUsingAsCollateral.selector) { - uint256 reserveId; - bool usingAsCollateral; - (reserveId, usingAsCollateral, user) = abi.decode(_args(input), (uint256, bool, address)); - if (!_hasDebtAt(spoke, user, postCall)) { - return (user, false); - } - - (bool wasUsingAsCollateral,) = _spokeUserReserveStatusAt(spoke, reserveId, user, preCall); - if (wasUsingAsCollateral == usingAsCollateral) { - return (user, false); - } - - // Enabling collateral with debt can make a restricted asset part of the solvency - // calculation. Disabling is relevant only if the reserve was active before the call. - return (user, usingAsCollateral || _hasPositiveCollateralFactorAt(spoke, reserveId, user, preCall)); - } - - revert("AaveV4External: unsupported trigger"); - } - - /// @notice Returns whether Aave counts a reserve toward the user's collateral at `fork`. - /// @dev The user flag alone is insufficient: zero shares and a zero dynamic collateral factor - /// do not economically support debt. - function _isActiveCollateralAt(address spoke, uint256 reserveId, address user, PhEvm.ForkId memory fork) - internal - view - returns (bool) - { - (bool usingAsCollateral,) = _spokeUserReserveStatusAt(spoke, reserveId, user, fork); - if (!usingAsCollateral) { - return false; - } - - IAaveV4Spoke.UserPosition memory position = _spokeUserPositionAt(spoke, reserveId, user, fork); - return _hasPositiveCollateralFactorAt(spoke, reserveId, fork, position); - } - - function _hasPositiveCollateralFactorAt(address spoke, uint256 reserveId, address user, PhEvm.ForkId memory fork) - internal - view - returns (bool) - { - IAaveV4Spoke.UserPosition memory position = _spokeUserPositionAt(spoke, reserveId, user, fork); - return _hasPositiveCollateralFactorAt(spoke, reserveId, fork, position); - } - - function _hasPositiveCollateralFactorAt( - address spoke, - uint256 reserveId, - PhEvm.ForkId memory fork, - IAaveV4Spoke.UserPosition memory position - ) internal view returns (bool) { - if (position.suppliedShares == 0) { - return false; - } - - IAaveV4Spoke.DynamicReserveConfig memory config = - _spokeDynamicConfigAt(spoke, reserveId, position.dynamicConfigKey, fork); - return config.collateralFactor != 0; - } - - function _hasDebtAt(address spoke, address user, PhEvm.ForkId memory fork) internal view returns (bool) { - return _spokeAccountDataAt(spoke, user, fork).totalDebtValueRay != 0; - } -} diff --git a/examples/aave/src/AaveV4ExternalCollateralTransferabilityAssertion.sol b/examples/aave/src/AaveV4ExternalCollateralTransferabilityAssertion.sol deleted file mode 100644 index 9264f57..0000000 --- a/examples/aave/src/AaveV4ExternalCollateralTransferabilityAssertion.sol +++ /dev/null @@ -1,235 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; - -import {PhEvm} from "credible-std/PhEvm.sol"; - -import { - AaveV4ExternalCollateralHelpers, - IExternalAccessControl, - IExternalBlacklist, - IExternalBlockedAccount, - IExternalPausable, - IExternalTetherBlacklist, - IExternalTimedBlacklist, - IExternalTimedPausable -} from "./AaveV4ExternalCollateralHelpers.sol"; -import {IAaveV4Spoke} from "./AaveV4Interfaces.sol"; - -/// @title AaveV4ExternalCollateralTransferabilityAssertion -/// @author Phylax Systems -/// @notice Makes an Aave v4 position reduce-only when its external collateral cannot be seized. -/// @dev Protects against an issuer or protocol independently pausing a token, blacklisting the -/// Aave Hub, blocking the Hub, or assigning it a full transfer-restriction role. Aave's -/// borrow path does not transfer the collateral token, so it can otherwise add debt while -/// the token's native transfer revert is deferred until liquidation. Repay, supply, -/// liquidation, debt-free exits, and complete removal of the impaired collateral remain open. -contract AaveV4ExternalCollateralTransferabilityAssertion is AaveV4ExternalCollateralHelpers { - enum AdapterKind { - Unsupported, - Paused, - PausedAndBlacklisted, - PausedAndBlackListed, - WeEth, - Blocked, - FullRestrictedRole - } - - struct CollateralPolicy { - uint256 reserveId; - address token; - address hub; - address statusSource; - AdapterKind adapter; - } - - // Snapshot reads are deliberately bounded to one external collateral per assertion instance. - // A five-policy Main Spoke fixture consumed about 575k assertion gas, above the current 300k - // local executor limit. Deploy one instance per reserve instead of creating an unsafe bundle. - uint256 internal constant MAX_POLICY_COUNT = 1; - bytes32 internal constant FULL_RESTRICTED_STAKER_ROLE = keccak256("FULL_RESTRICTED_STAKER_ROLE"); - bytes32 internal constant ERC1967_IMPLEMENTATION_SLOT = - 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - // Verified Ethereum weETH implementations. The legacy implementation had no transfer pause or - // blacklist hooks. The restricted implementation added paused(), pausedUntil(), and the - // external Blacklister hook. Unknown proxy implementations fail closed below. - address internal constant WEETH_LEGACY_UNRESTRICTED_IMPLEMENTATION = 0x2d10683E941275D502173053927AD6066e6aFd6B; - address internal constant WEETH_RESTRICTED_IMPLEMENTATION = 0xA6Ca0607190d03CF16fe6F2865Cf40c3D160ccf3; - - address internal immutable SPOKE; - CollateralPolicy[] internal collateralPolicies; - - /// @param spoke_ The exact Aave v4 risk Spoke adopting the assertion. - /// @param policies_ Exactly one reserve/token/Hub policy for collateral enabled on that Spoke. - /// `statusSource` is the token itself except for weETH, where it is the Blacklister. - constructor(address spoke_, CollateralPolicy[] memory policies_) { - require(spoke_ != address(0), "AaveV4Transferability: spoke zero"); - require(policies_.length == MAX_POLICY_COUNT, "AaveV4Transferability: one policy required"); - - SPOKE = spoke_; - for (uint256 i; i < policies_.length; ++i) { - CollateralPolicy memory policy = policies_[i]; - require(policy.token != address(0), "AaveV4Transferability: token zero"); - require(policy.hub != address(0), "AaveV4Transferability: Hub zero"); - require(policy.statusSource != address(0), "AaveV4Transferability: status source zero"); - require(policy.adapter != AdapterKind.Unsupported, "AaveV4Transferability: unsupported adapter"); - if (policy.adapter != AdapterKind.WeEth) { - require(policy.statusSource == policy.token, "AaveV4Transferability: status source must be token"); - } - for (uint256 j; j < i; ++j) { - require( - collateralPolicies[j].reserveId != policy.reserveId, - "AaveV4Transferability: duplicate reserve policy" - ); - } - collateralPolicies.push(policy); - } - } - - /// @notice Registers only Aave operations that can add debt or remove effective collateral. - /// @dev Function-call triggers are required to decode `onBehalfOf` and bind the check to that - /// operation's PostCall position. ERC20-change triggers would miss the failure because an - /// Aave borrow against disabled collateral does not move that collateral token. - function triggers() external view override { - registerFnCallTrigger(this.assertExternalCollateralTransferable.selector, IAaveV4Spoke.borrow.selector); - registerFnCallTrigger(this.assertExternalCollateralTransferable.selector, IAaveV4Spoke.withdraw.selector); - registerFnCallTrigger( - this.assertExternalCollateralTransferable.selector, IAaveV4Spoke.setUsingAsCollateral.selector - ); - } - - /// @notice Requires every covered collateral still supporting the affected user's debt to be transferable. - /// @dev Reads Aave only to scope exposure, then reads independent issuer state at PreCall and - /// PostTx. A failure means the triggering operation would leave more debt, or less good - /// collateral, while seizure of a relied-on external token can revert. Checking both - /// snapshots also rejects pause/blacklist changes wrapped around the Aave call in one tx. - function assertExternalCollateralTransferable() external view { - _requireAdopter(SPOKE, "AaveV4Transferability: configured Spoke is not adopter"); - - PhEvm.TriggerContext memory ctx = ph.context(); - (address user, bool riskIncreasing) = _riskIncreasingUser(SPOKE, ctx); - if (!riskIncreasing) { - return; - } - - PhEvm.ForkId memory postCall = _postCall(ctx.callEnd); - PhEvm.ForkId memory preCall = _preCall(ctx.callStart); - PhEvm.ForkId memory postTx = _postTx(); - - for (uint256 i; i < collateralPolicies.length; ++i) { - CollateralPolicy memory policy = collateralPolicies[i]; - if (!_isActiveCollateralAt(SPOKE, policy.reserveId, user, postCall)) { - continue; - } - - IAaveV4Spoke.Reserve memory reserve = _spokeReserveAt(SPOKE, policy.reserveId, postCall); - require(reserve.underlying == policy.token, "AaveV4Transferability: reserve token changed"); - require(reserve.hub == policy.hub, "AaveV4Transferability: reserve Hub changed"); - - require( - _isTransferableAt(policy, preCall), "AaveV4Transferability: collateral restricted before risk increase" - ); - require( - _isTransferableAt(policy, postTx), "AaveV4Transferability: collateral restricted at transaction end" - ); - } - } - - function collateralPolicyCount() external view returns (uint256) { - return collateralPolicies.length; - } - - function collateralPolicy(uint256 index) external view returns (CollateralPolicy memory) { - return collateralPolicies[index]; - } - - function _isTransferableAt(CollateralPolicy memory policy, PhEvm.ForkId memory fork) internal view returns (bool) { - if (policy.adapter == AdapterKind.Paused) { - return !_pausedAt(policy.token, fork); - } - - if (policy.adapter == AdapterKind.PausedAndBlacklisted) { - return !_pausedAt(policy.token, fork) - && !_readBoolAt(policy.token, abi.encodeCall(IExternalBlacklist.isBlacklisted, (policy.hub)), fork); - } - - if (policy.adapter == AdapterKind.PausedAndBlackListed) { - return !_pausedAt(policy.token, fork) - && !_readBoolAt( - policy.token, abi.encodeCall(IExternalTetherBlacklist.isBlackListed, (policy.hub)), fork - ); - } - - if (policy.adapter == AdapterKind.WeEth) { - address implementation = - address(uint160(uint256(ph.loadStateAt(policy.token, ERC1967_IMPLEMENTATION_SLOT, fork)))); - if (implementation == WEETH_LEGACY_UNRESTRICTED_IMPLEMENTATION) { - return true; - } - require( - implementation == address(0) || implementation == WEETH_RESTRICTED_IMPLEMENTATION, - "AaveV4Transferability: unsupported weETH implementation" - ); - - bool indefinitePause = _pausedAt(policy.token, fork); - uint256 timedPause = _readUintAt(policy.token, abi.encodeCall(IExternalTimedPausable.pausedUntil, ()), fork); - uint256 hubBlacklist = _readUintAt( - policy.statusSource, abi.encodeCall(IExternalTimedBlacklist.blacklistedUntil, (policy.hub)), fork - ); - return !indefinitePause && timedPause < block.timestamp && hubBlacklist <= block.timestamp; - } - - if (policy.adapter == AdapterKind.Blocked) { - return !_readBoolAt(policy.token, abi.encodeCall(IExternalBlockedAccount.isBlocked, (policy.hub)), fork); - } - - if (policy.adapter == AdapterKind.FullRestrictedRole) { - return !_readBoolAt( - policy.token, - abi.encodeCall(IExternalAccessControl.hasRole, (FULL_RESTRICTED_STAKER_ROLE, policy.hub)), - fork - ); - } - - revert("AaveV4Transferability: unsupported adapter"); - } - - function _pausedAt(address target, PhEvm.ForkId memory fork) internal view returns (bool) { - return _readBoolAt(target, abi.encodeCall(IExternalPausable.paused, ()), fork); - } -} - -/// @title AaveV4EthereumMainSpokeWeETHTransferabilityAssertion -/// @notice Production-configured weETH reduce-only gate for the Ethereum Main Spoke. -/// @dev Constants were verified at Ethereum block 25,653,183. Revalidate the Spoke reserve, -/// Hub, weETH implementation, and Blacklister before adopting after any protocol upgrade. -contract AaveV4EthereumMainSpokeWeETHTransferabilityAssertion is AaveV4ExternalCollateralTransferabilityAssertion { - address public constant MAIN_SPOKE = 0x94e7A5dCbE816e498b89aB752661904E2F56c485; - address public constant CORE_HUB = 0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9; - address public constant WEETH = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee; - address public constant WEETH_BLACKLISTER = 0x5585996E7cFE95f2D99e61168B8b35C66Ff99B18; - - constructor() AaveV4ExternalCollateralTransferabilityAssertion(MAIN_SPOKE, _policies(2)) {} - - function _policies(uint256 reserveId) private pure returns (CollateralPolicy[] memory policies) { - policies = new CollateralPolicy[](1); - policies[0] = CollateralPolicy(reserveId, WEETH, CORE_HUB, WEETH_BLACKLISTER, AdapterKind.WeEth); - } -} - -/// @title AaveV4EthereumEtherFiSpokeWeETHTransferabilityAssertion -/// @notice Production-configured weETH reduce-only gate for the Ethereum ether.fi eSpoke. -/// @dev This protects the same Core Hub custody path as the Main Spoke wrapper, with reserve id 0. -contract AaveV4EthereumEtherFiSpokeWeETHTransferabilityAssertion is AaveV4ExternalCollateralTransferabilityAssertion { - address public constant ETHERFI_ESPOKE = 0xbF10BDfE177dE0336aFD7fcCF80A904E15386219; - address public constant CORE_HUB = 0xCca852Bc40e560adC3b1Cc58CA5b55638ce826c9; - address public constant WEETH = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee; - address public constant WEETH_BLACKLISTER = 0x5585996E7cFE95f2D99e61168B8b35C66Ff99B18; - - constructor() AaveV4ExternalCollateralTransferabilityAssertion(ETHERFI_ESPOKE, _policies(0)) {} - - function _policies(uint256 reserveId) private pure returns (CollateralPolicy[] memory policies) { - policies = new CollateralPolicy[](1); - policies[0] = CollateralPolicy(reserveId, WEETH, CORE_HUB, WEETH_BLACKLISTER, AdapterKind.WeEth); - } -} diff --git a/examples/aave/src/AaveV4PTUSDGRedemptionAssertion.sol b/examples/aave/src/AaveV4PTUSDGRedemptionAssertion.sol deleted file mode 100644 index b17bed8..0000000 --- a/examples/aave/src/AaveV4PTUSDGRedemptionAssertion.sol +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; - -import {PhEvm} from "credible-std/PhEvm.sol"; - -import { - AaveV4ExternalCollateralHelpers, - IExternalFrozenAccount, - IExternalPausable -} from "./AaveV4ExternalCollateralHelpers.sol"; -import {IAaveV4Spoke} from "./AaveV4Interfaces.sol"; - -/// @title AaveV4PTUSDGRedemptionAssertion -/// @author Phylax Systems -/// @notice Makes PT-USDG-backed Aave positions reduce-only when PT's USDG exit is disabled. -/// @dev Protects the failure where PT remains transferable and Aave's PT oracle remains valid, -/// but Pendle's SY is paused, USDG is globally paused, or Paxos freezes the SY account. -/// Aave borrow does not redeem PT, so native Pendle/USDG reverts otherwise arrive only when -/// collateral value must be realized. Maturity is intentionally not treated as a failure. -contract AaveV4PTUSDGRedemptionAssertion is AaveV4ExternalCollateralHelpers { - address internal immutable SPOKE; - uint256 internal immutable PT_RESERVE_ID; - address internal immutable PT_USDG; - address internal immutable HUB; - address internal immutable SY_USDG; - address internal immutable USDG; - - /// @param spoke_ The Aave v4 USDG Pendle Spoke adopting the assertion. - /// @param ptReserveId_ The Spoke reserve id for PT-USDG-24SEP2026. - /// @param ptUsdg_ The canonical PT token. - /// @param hub_ The Hub holding supplied PT on behalf of Aave users. - /// @param syUsdg_ The Pendle standardized-yield contract through which PT resolves to USDG. - /// @param usdg_ The Paxos USDG token returned by the SY redemption path. - constructor(address spoke_, uint256 ptReserveId_, address ptUsdg_, address hub_, address syUsdg_, address usdg_) { - require(spoke_ != address(0), "AaveV4PTUSDG: Spoke zero"); - require(ptUsdg_ != address(0), "AaveV4PTUSDG: PT zero"); - require(hub_ != address(0), "AaveV4PTUSDG: Hub zero"); - require(syUsdg_ != address(0), "AaveV4PTUSDG: SY zero"); - require(usdg_ != address(0), "AaveV4PTUSDG: USDG zero"); - - SPOKE = spoke_; - PT_RESERVE_ID = ptReserveId_; - PT_USDG = ptUsdg_; - HUB = hub_; - SY_USDG = syUsdg_; - USDG = usdg_; - } - - /// @notice Registers only debt growth and effective-collateral removal paths. - /// @dev Per-call context is necessary to identify the user and allow a complete PT exit while - /// blocking a withdrawal of other good collateral that leaves debt relying on stranded PT. - function triggers() external view override { - registerFnCallTrigger(this.assertPtUsdgRedemptionAvailable.selector, IAaveV4Spoke.borrow.selector); - registerFnCallTrigger(this.assertPtUsdgRedemptionAvailable.selector, IAaveV4Spoke.withdraw.selector); - registerFnCallTrigger(this.assertPtUsdgRedemptionAvailable.selector, IAaveV4Spoke.setUsingAsCollateral.selector); - } - - /// @notice Requires the canonical PT -> SY -> USDG exit to remain administratively available. - /// @dev The check runs only if the affected user still relies on positive PT collateral after - /// a risk-increasing call. A failure means Aave could accept more debt or less good - /// collateral even though SY/USDG state independently prevents PT value realization. - /// PreCall plus PostTx reads reject same-transaction pause/freeze wrapping. - function assertPtUsdgRedemptionAvailable() external view { - _requireAdopter(SPOKE, "AaveV4PTUSDG: configured Spoke is not adopter"); - - PhEvm.TriggerContext memory ctx = ph.context(); - (address user, bool riskIncreasing) = _riskIncreasingUser(SPOKE, ctx); - if (!riskIncreasing) { - return; - } - - PhEvm.ForkId memory postCall = _postCall(ctx.callEnd); - if (!_isActiveCollateralAt(SPOKE, PT_RESERVE_ID, user, postCall)) { - return; - } - - IAaveV4Spoke.Reserve memory reserve = _spokeReserveAt(SPOKE, PT_RESERVE_ID, postCall); - require(reserve.underlying == PT_USDG, "AaveV4PTUSDG: reserve PT changed"); - require(reserve.hub == HUB, "AaveV4PTUSDG: reserve Hub changed"); - - _requireRedemptionAvailableAt(_preCall(ctx.callStart), "AaveV4PTUSDG: redemption disabled before risk increase"); - _requireRedemptionAvailableAt(_postTx(), "AaveV4PTUSDG: redemption disabled at transaction end"); - } - - /// @notice Returns the immutable deployment wiring for operational review. - function configuration() - external - view - returns (address spoke, uint256 ptReserveId, address ptUsdg, address hub, address syUsdg, address usdg) - { - return (SPOKE, PT_RESERVE_ID, PT_USDG, HUB, SY_USDG, USDG); - } - - function _requireRedemptionAvailableAt(PhEvm.ForkId memory fork, string memory message) internal view { - bool syPaused = _readBoolAt(SY_USDG, abi.encodeCall(IExternalPausable.paused, ()), fork); - bool usdgPaused = _readBoolAt(USDG, abi.encodeCall(IExternalPausable.paused, ()), fork); - bool syFrozen = _readBoolAt(USDG, abi.encodeCall(IExternalFrozenAccount.isFrozen, (SY_USDG)), fork); - require(!syPaused && !usdgPaused && !syFrozen, message); - } -} - -/// @title AaveV4EthereumPTUSDGRedemptionAssertion -/// @notice Production-configured PT-USDG redemption gate for Aave v4 Ethereum. -/// @dev Constants were verified at Ethereum block 25,653,183. Revalidate the Spoke reserve, -/// PT/SY/USDG path, Hub, proxy implementations, and status ABIs before adoption after upgrades. -contract AaveV4EthereumPTUSDGRedemptionAssertion is AaveV4PTUSDGRedemptionAssertion { - address public constant USDG_PENDLE_SPOKE = 0x956d8e0A89cfa3744428C4641b5a53B56167a7f9; - address public constant PT_USDG_24SEP2026 = 0xc1906aeCf868749a2DeE203F59b904c0cf212140; - address public constant PAXOS_HUB = 0x62d63197660c080236193CA60b70E49A08E90368; - address public constant PENDLE_SY_USDG = 0xc1799CaB1F201946f7CFaFBaF1BCC089b2F08927; - address public constant PAXOS_USDG = 0xe343167631d89B6Ffc58B88d6b7fB0228795491D; - - constructor() - AaveV4PTUSDGRedemptionAssertion(USDG_PENDLE_SPOKE, 0, PT_USDG_24SEP2026, PAXOS_HUB, PENDLE_SY_USDG, PAXOS_USDG) - {} -} diff --git a/examples/aave/test/AaveV4ExternalCollateralAssertions.t.sol b/examples/aave/test/AaveV4ExternalCollateralAssertions.t.sol deleted file mode 100644 index a6cdcd0..0000000 --- a/examples/aave/test/AaveV4ExternalCollateralAssertions.t.sol +++ /dev/null @@ -1,793 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; - -import {Test} from "forge-std/Test.sol"; - -import {CredibleTest} from "credible-std/CredibleTest.sol"; - -import { - AaveV4EthereumEtherFiSpokeWeETHTransferabilityAssertion, - AaveV4EthereumMainSpokeWeETHTransferabilityAssertion, - AaveV4ExternalCollateralTransferabilityAssertion as Transferability -} from "../src/AaveV4ExternalCollateralTransferabilityAssertion.sol"; -import { - AaveV4EthereumPTUSDGRedemptionAssertion, - AaveV4PTUSDGRedemptionAssertion -} from "../src/AaveV4PTUSDGRedemptionAssertion.sol"; -import {IAaveV4Spoke} from "../src/AaveV4Interfaces.sol"; - -contract MockAaveV4ExternalStatus { - bool internal pausedState; - bool internal revertPausedRead; - uint256 public pausedUntil; - mapping(address account => uint256 until) public blacklistedUntil; - mapping(address account => bool blocked) internal blacklisted; - mapping(address account => bool blocked) internal blackListed; - mapping(address account => bool blocked) internal blocked; - mapping(address account => bool frozen) internal frozen; - mapping(bytes32 role => mapping(address account => bool granted)) internal roles; - - function paused() external view returns (bool) { - require(!revertPausedRead, "status read unavailable"); - return pausedState; - } - - function isBlacklisted(address account) external view returns (bool) { - return blacklisted[account]; - } - - function isBlackListed(address account) external view returns (bool) { - return blackListed[account]; - } - - function isBlocked(address account) external view returns (bool) { - return blocked[account]; - } - - function isFrozen(address account) external view returns (bool) { - return frozen[account]; - } - - function hasRole(bytes32 role, address account) external view returns (bool) { - return roles[role][account]; - } - - function setPaused(bool value) external { - pausedState = value; - } - - function setRevertPausedRead(bool value) external { - revertPausedRead = value; - } - - function setPausedUntil(uint256 value) external { - pausedUntil = value; - } - - function setBlacklistedUntil(address account, uint256 value) external { - blacklistedUntil[account] = value; - } - - function setBlacklisted(address account, bool value) external { - blacklisted[account] = value; - } - - function setBlackListed(address account, bool value) external { - blackListed[account] = value; - } - - function setBlocked(address account, bool value) external { - blocked[account] = value; - } - - function setFrozen(address account, bool value) external { - frozen[account] = value; - } - - function setRole(bytes32 role, address account, bool value) external { - roles[role][account] = value; - } -} - -contract MockAaveV4ExternalSpoke { - IAaveV4Spoke.Reserve[] internal reserves; - mapping(uint256 reserveId => IAaveV4Spoke.DynamicReserveConfig config) internal dynamicConfigs; - mapping(address user => mapping(uint256 reserveId => IAaveV4Spoke.UserPosition position)) internal positions; - mapping(address user => mapping(uint256 reserveId => bool collateral)) internal collateralStatus; - mapping(address user => mapping(uint256 reserveId => bool borrowing)) internal borrowingStatus; - mapping(address user => uint256 debtValueRay) internal debts; - - function addReserve(address token, address hub, uint16 collateralFactor) external returns (uint256 reserveId) { - reserveId = reserves.length; - reserves.push( - IAaveV4Spoke.Reserve({ - underlying: token, - hub: hub, - assetId: uint16(reserveId), - decimals: 18, - collateralRisk: 0, - flags: 0, - dynamicConfigKey: 0 - }) - ); - dynamicConfigs[reserveId] = IAaveV4Spoke.DynamicReserveConfig({ - collateralFactor: collateralFactor, maxLiquidationBonus: 10_500, liquidationFee: 1_000 - }); - } - - function setPosition(address user, uint256 reserveId, uint120 suppliedShares, bool usingAsCollateral) external { - positions[user][reserveId].suppliedShares = suppliedShares; - positions[user][reserveId].dynamicConfigKey = 0; - collateralStatus[user][reserveId] = usingAsCollateral; - } - - function setDebt(address user, uint256 debtValueRay) external { - debts[user] = debtValueRay; - } - - function setCollateralFactor(uint256 reserveId, uint16 collateralFactor) external { - dynamicConfigs[reserveId].collateralFactor = collateralFactor; - } - - function setReserveToken(uint256 reserveId, address token) external { - reserves[reserveId].underlying = token; - } - - function setReserveHub(uint256 reserveId, address hub) external { - reserves[reserveId].hub = hub; - } - - function supply(uint256 reserveId, uint256 amount, address onBehalfOf) external returns (uint256, uint256) { - positions[onBehalfOf][reserveId].suppliedShares += uint120(amount); - return (amount, amount); - } - - function withdraw(uint256 reserveId, uint256 amount, address onBehalfOf) external returns (uint256, uint256) { - uint120 shares = positions[onBehalfOf][reserveId].suppliedShares; - uint120 removed = amount >= shares ? shares : uint120(amount); - positions[onBehalfOf][reserveId].suppliedShares = shares - removed; - return (removed, removed); - } - - function borrow(uint256 reserveId, uint256 amount, address onBehalfOf) external returns (uint256, uint256) { - debts[onBehalfOf] += amount * 1e27; - borrowingStatus[onBehalfOf][reserveId] = true; - return (amount, amount); - } - - function repay(uint256 reserveId, uint256 amount, address onBehalfOf) external returns (uint256, uint256) { - reserveId; - amount; - debts[onBehalfOf] = 0; - return (amount, amount); - } - - function liquidationCall(uint256, uint256, address user, uint256, bool) external { - debts[user] = 0; - } - - function setUsingAsCollateral(uint256 reserveId, bool usingAsCollateral, address onBehalfOf) external { - collateralStatus[onBehalfOf][reserveId] = usingAsCollateral; - } - - function getReserve(uint256 reserveId) external view returns (IAaveV4Spoke.Reserve memory) { - return reserves[reserveId]; - } - - function getDynamicReserveConfig(uint256 reserveId, uint32) - external - view - returns (IAaveV4Spoke.DynamicReserveConfig memory) - { - return dynamicConfigs[reserveId]; - } - - function getUserReserveStatus(uint256 reserveId, address user) external view returns (bool, bool) { - return (collateralStatus[user][reserveId], borrowingStatus[user][reserveId]); - } - - function getUserPosition(uint256 reserveId, address user) external view returns (IAaveV4Spoke.UserPosition memory) { - return positions[user][reserveId]; - } - - function getUserAccountData(address user) external view returns (IAaveV4Spoke.UserAccountData memory data) { - data.totalDebtValueRay = debts[user]; - } -} - -contract AaveV4ExternalScenarioDriver { - function borrowThenPause(MockAaveV4ExternalSpoke spoke, MockAaveV4ExternalStatus status, address user) external { - spoke.borrow(1, 1, user); - status.setPaused(true); - } - - function pauseBorrowUnpause(MockAaveV4ExternalSpoke spoke, MockAaveV4ExternalStatus status, address user) external { - status.setPaused(true); - spoke.borrow(1, 1, user); - status.setPaused(false); - } -} - -abstract contract AaveV4ExternalCollateralTestBase is Test, CredibleTest { - uint256 internal constant RESTRICTED_RESERVE = 0; - uint256 internal constant GOOD_RESERVE = 1; - uint256 internal constant INITIAL_DEBT_RAY = 100e27; - bytes32 internal constant FULL_RESTRICTED_STAKER_ROLE = keccak256("FULL_RESTRICTED_STAKER_ROLE"); - bytes32 internal constant ERC1967_IMPLEMENTATION_SLOT = - 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - address internal constant WEETH_LEGACY_UNRESTRICTED_IMPLEMENTATION = 0x2d10683E941275D502173053927AD6066e6aFd6B; - address internal constant WEETH_RESTRICTED_IMPLEMENTATION = 0xA6Ca0607190d03CF16fe6F2865Cf40c3D160ccf3; - - address internal user = makeAddr("borrower"); - address internal hub = makeAddr("Aave Hub"); - address internal otherHub = makeAddr("other Hub"); - address internal goodToken = makeAddr("good collateral"); - - MockAaveV4ExternalStatus internal token; - MockAaveV4ExternalStatus internal blacklister; - MockAaveV4ExternalSpoke internal spoke; - AaveV4ExternalScenarioDriver internal driver; - - function setUp() public virtual { - vm.warp(1_000_000); - token = new MockAaveV4ExternalStatus(); - blacklister = new MockAaveV4ExternalStatus(); - spoke = new MockAaveV4ExternalSpoke(); - spoke.addReserve(address(token), hub, 8_000); - spoke.addReserve(goodToken, hub, 8_000); - spoke.setPosition(user, RESTRICTED_RESERVE, 100, true); - spoke.setPosition(user, GOOD_RESERVE, 100, true); - spoke.setDebt(user, INITIAL_DEBT_RAY); - driver = new AaveV4ExternalScenarioDriver(); - } -} - -contract AaveV4ExternalCollateralTransferabilityAssertionTest is AaveV4ExternalCollateralTestBase { - function testHonestBorrowPasses() public { - _arm(Transferability.AdapterKind.Paused, address(token)); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testGlobalPauseTrips() public { - token.setPaused(true); - _expectBorrowFailure( - Transferability.AdapterKind.Paused, - address(token), - "AaveV4Transferability: collateral restricted before risk increase" - ); - } - - function testCamelCaseBlacklistTrips() public { - token.setBlacklisted(hub, true); - _expectBorrowFailure( - Transferability.AdapterKind.PausedAndBlacklisted, - address(token), - "AaveV4Transferability: collateral restricted before risk increase" - ); - } - - function testTetherBlacklistTrips() public { - token.setBlackListed(hub, true); - _expectBorrowFailure( - Transferability.AdapterKind.PausedAndBlackListed, - address(token), - "AaveV4Transferability: collateral restricted before risk increase" - ); - } - - function testWeEthIndefinitePauseTrips() public { - token.setPaused(true); - _expectBorrowFailure( - Transferability.AdapterKind.WeEth, - address(blacklister), - "AaveV4Transferability: collateral restricted before risk increase" - ); - } - - function testWeEthTimedPauseTripsAtBoundary() public { - token.setPausedUntil(block.timestamp); - _expectBorrowFailure( - Transferability.AdapterKind.WeEth, - address(blacklister), - "AaveV4Transferability: collateral restricted before risk increase" - ); - } - - function testWeEthHubBlacklistTrips() public { - blacklister.setBlacklistedUntil(hub, block.timestamp + 1 days); - _expectBorrowFailure( - Transferability.AdapterKind.WeEth, - address(blacklister), - "AaveV4Transferability: collateral restricted before risk increase" - ); - } - - function testWeEthExpiredTimedPausePasses() public { - token.setPausedUntil(block.timestamp - 1); - _arm(Transferability.AdapterKind.WeEth, address(blacklister)); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testWeEthBlacklistEndingAtCurrentTimestampPasses() public { - blacklister.setBlacklistedUntil(hub, block.timestamp); - _arm(Transferability.AdapterKind.WeEth, address(blacklister)); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testLegacyUnrestrictedWeEthImplementationDoesNotRequireNewStatusAbi() public { - vm.store( - address(token), - ERC1967_IMPLEMENTATION_SLOT, - bytes32(uint256(uint160(WEETH_LEGACY_UNRESTRICTED_IMPLEMENTATION))) - ); - token.setRevertPausedRead(true); - - _arm(Transferability.AdapterKind.WeEth, address(blacklister)); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testRestrictedWeEthImplementationStillChecksPauseState() public { - vm.store( - address(token), ERC1967_IMPLEMENTATION_SLOT, bytes32(uint256(uint160(WEETH_RESTRICTED_IMPLEMENTATION))) - ); - token.setPaused(true); - - _expectBorrowFailure( - Transferability.AdapterKind.WeEth, - address(blacklister), - "AaveV4Transferability: collateral restricted before risk increase" - ); - } - - function testUnknownWeEthImplementationFailsClosed() public { - vm.store(address(token), ERC1967_IMPLEMENTATION_SLOT, bytes32(uint256(uint160(makeAddr("unknown weETH impl"))))); - - _expectBorrowFailure( - Transferability.AdapterKind.WeEth, - address(blacklister), - "AaveV4Transferability: unsupported weETH implementation" - ); - } - - function testBlockedHubTrips() public { - token.setBlocked(hub, true); - _expectBorrowFailure( - Transferability.AdapterKind.Blocked, - address(token), - "AaveV4Transferability: collateral restricted before risk increase" - ); - } - - function testFullRestrictedRoleTrips() public { - token.setRole(FULL_RESTRICTED_STAKER_ROLE, hub, true); - _expectBorrowFailure( - Transferability.AdapterKind.FullRestrictedRole, - address(token), - "AaveV4Transferability: collateral restricted before risk increase" - ); - } - - function testPauseAddedAfterBorrowTripsAtPostTx() public { - _arm(Transferability.AdapterKind.Paused, address(token)); - vm.expectRevert(bytes("AaveV4Transferability: collateral restricted at transaction end")); - driver.borrowThenPause(spoke, token, user); - } - - function testTransientPauseWrappedAroundBorrowTripsAtPreCall() public { - _arm(Transferability.AdapterKind.Paused, address(token)); - vm.expectRevert(bytes("AaveV4Transferability: collateral restricted before risk increase")); - driver.pauseBorrowUnpause(spoke, token, user); - } - - function testDebtFreeCollateralWithdrawalPassesWhilePaused() public { - spoke.setDebt(user, 0); - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - spoke.withdraw(RESTRICTED_RESERVE, 10, user); - } - - function testNonCollateralWithdrawalPassesWhileOtherCollateralPaused() public { - spoke.setPosition(user, GOOD_RESERVE, 100, false); - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - spoke.withdraw(GOOD_RESERVE, 10, user); - } - - function testZeroFactorWithdrawalPassesAsNonCollateral() public { - spoke.setCollateralFactor(GOOD_RESERVE, 0); - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - spoke.withdraw(GOOD_RESERVE, 10, user); - } - - function testFullImpairedCollateralExitPasses() public { - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - spoke.withdraw(RESTRICTED_RESERVE, type(uint256).max, user); - } - - function testPartialImpairedCollateralExitTripsWhileDebtStillReliesOnIt() public { - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - vm.expectRevert(bytes("AaveV4Transferability: collateral restricted before risk increase")); - spoke.withdraw(RESTRICTED_RESERVE, 10, user); - } - - function testDisableImpairedCollateralPasses() public { - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - spoke.setUsingAsCollateral(RESTRICTED_RESERVE, false, user); - } - - function testEnableImpairedCollateralWithDebtTrips() public { - spoke.setPosition(user, RESTRICTED_RESERVE, 100, false); - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - vm.expectRevert(bytes("AaveV4Transferability: collateral restricted before risk increase")); - spoke.setUsingAsCollateral(RESTRICTED_RESERVE, true, user); - } - - function testEnableImpairedCollateralWithoutDebtPasses() public { - spoke.setPosition(user, RESTRICTED_RESERVE, 100, false); - spoke.setDebt(user, 0); - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - spoke.setUsingAsCollateral(RESTRICTED_RESERVE, true, user); - } - - function testNoOpCollateralEnablePassesWhilePaused() public { - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - spoke.setUsingAsCollateral(RESTRICTED_RESERVE, true, user); - } - - function testWithdrawGoodCollateralTripsWhenDebtReliesOnImpairedCollateral() public { - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - vm.expectRevert(bytes("AaveV4Transferability: collateral restricted before risk increase")); - spoke.withdraw(GOOD_RESERVE, 10, user); - } - - function testDisableGoodCollateralTripsWhenDebtReliesOnImpairedCollateral() public { - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - vm.expectRevert(bytes("AaveV4Transferability: collateral restricted before risk increase")); - spoke.setUsingAsCollateral(GOOD_RESERVE, false, user); - } - - function testPausedCollateralNotUsedByAffectedUserPasses() public { - spoke.setPosition(user, RESTRICTED_RESERVE, 100, false); - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testZeroSuppliedSharesDoNotCountAsReliance() public { - spoke.setPosition(user, RESTRICTED_RESERVE, 0, true); - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testZeroCollateralFactorDoesNotCountAsReliance() public { - spoke.setCollateralFactor(RESTRICTED_RESERVE, 0); - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testSupplyRemainsOpenWhilePaused() public { - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - // The local Credible harness requires the armed assertion to execute once. Expecting its - // zero-execution diagnostic proves `supply` is deliberately absent from production triggers. - vm.expectRevert(bytes("Expected 1 assertion to be executed, but 0 were executed.")); - spoke.supply(GOOD_RESERVE, 1, user); - } - - function testRepayRemainsOpenWhilePaused() public { - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - vm.expectRevert(bytes("Expected 1 assertion to be executed, but 0 were executed.")); - spoke.repay(GOOD_RESERVE, 1, user); - } - - function testLiquidationRemainsOpenWhilePaused() public { - token.setPaused(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - vm.expectRevert(bytes("Expected 1 assertion to be executed, but 0 were executed.")); - spoke.liquidationCall(RESTRICTED_RESERVE, GOOD_RESERVE, user, 1, true); - } - - function testMultiplePoliciesRejectedToPreserveAssertionGasBound() public { - Transferability.CollateralPolicy[] memory policies = new Transferability.CollateralPolicy[](2); - policies[0] = Transferability.CollateralPolicy({ - reserveId: RESTRICTED_RESERVE, - token: address(token), - hub: hub, - statusSource: address(token), - adapter: Transferability.AdapterKind.Paused - }); - policies[1] = Transferability.CollateralPolicy({ - reserveId: GOOD_RESERVE, - token: address(token), - hub: hub, - statusSource: address(token), - adapter: Transferability.AdapterKind.Paused - }); - vm.expectRevert(bytes("AaveV4Transferability: one policy required")); - new Transferability(address(spoke), policies); - } - - function testReserveTokenDriftFailsClosed() public { - spoke.setReserveToken(RESTRICTED_RESERVE, goodToken); - _arm(Transferability.AdapterKind.Paused, address(token)); - vm.expectRevert(bytes("AaveV4Transferability: reserve token changed")); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testReserveHubDriftFailsClosed() public { - spoke.setReserveHub(RESTRICTED_RESERVE, otherHub); - _arm(Transferability.AdapterKind.Paused, address(token)); - vm.expectRevert(bytes("AaveV4Transferability: reserve Hub changed")); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testExternalStatusGetterFailureFailsClosed() public { - token.setRevertPausedRead(true); - _arm(Transferability.AdapterKind.Paused, address(token)); - vm.expectRevert(bytes("AaveV4: fork view failed")); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testWrongAdopterFailsClosed() public { - MockAaveV4ExternalSpoke other = new MockAaveV4ExternalSpoke(); - other.addReserve(address(token), hub, 8_000); - other.setPosition(user, 0, 100, true); - other.setDebt(user, INITIAL_DEBT_RAY); - - Transferability.CollateralPolicy[] memory policies = - _policies(Transferability.AdapterKind.Paused, address(token)); - bytes memory createData = - abi.encodePacked(type(Transferability).creationCode, abi.encode(address(spoke), policies)); - cl.assertion(address(other), createData, Transferability.assertExternalCollateralTransferable.selector); - - vm.expectRevert(bytes("AaveV4Transferability: configured Spoke is not adopter")); - other.borrow(0, 1, user); - } - - function _expectBorrowFailure(Transferability.AdapterKind adapter, address statusSource, string memory reason) - internal - { - _arm(adapter, statusSource); - vm.expectRevert(bytes(reason)); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function _arm(Transferability.AdapterKind adapter, address statusSource) internal { - Transferability.CollateralPolicy[] memory policies = _policies(adapter, statusSource); - _armPolicies(policies); - } - - function _armPolicies(Transferability.CollateralPolicy[] memory policies) internal { - bytes memory createData = - abi.encodePacked(type(Transferability).creationCode, abi.encode(address(spoke), policies)); - cl.assertion(address(spoke), createData, Transferability.assertExternalCollateralTransferable.selector); - } - - function _policies(Transferability.AdapterKind adapter, address statusSource) - internal - view - returns (Transferability.CollateralPolicy[] memory policies) - { - policies = new Transferability.CollateralPolicy[](1); - policies[0] = Transferability.CollateralPolicy({ - reserveId: RESTRICTED_RESERVE, token: address(token), hub: hub, statusSource: statusSource, adapter: adapter - }); - } -} - -contract AaveV4PTUSDGRedemptionAssertionTest is AaveV4ExternalCollateralTestBase { - MockAaveV4ExternalStatus internal sy; - MockAaveV4ExternalStatus internal usdg; - - function setUp() public override { - super.setUp(); - sy = new MockAaveV4ExternalStatus(); - usdg = new MockAaveV4ExternalStatus(); - } - - function testHonestPtBorrowPasses() public { - _armPt(); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testSyPauseTrips() public { - sy.setPaused(true); - _expectPtBorrowFailure("AaveV4PTUSDG: redemption disabled before risk increase"); - } - - function testUsdgPauseTrips() public { - usdg.setPaused(true); - _expectPtBorrowFailure("AaveV4PTUSDG: redemption disabled before risk increase"); - } - - function testUsdgFreezeOfSyTrips() public { - usdg.setFrozen(address(sy), true); - _expectPtBorrowFailure("AaveV4PTUSDG: redemption disabled before risk increase"); - } - - function testSyPauseAddedAfterBorrowTripsAtPostTx() public { - _armPt(); - vm.expectRevert(bytes("AaveV4PTUSDG: redemption disabled at transaction end")); - driver.borrowThenPause(spoke, sy, user); - } - - function testTransientSyPauseWrappedAroundBorrowTripsAtPreCall() public { - _armPt(); - vm.expectRevert(bytes("AaveV4PTUSDG: redemption disabled before risk increase")); - driver.pauseBorrowUnpause(spoke, sy, user); - } - - function testPostMaturityWithAvailableRedemptionPasses() public { - vm.warp(1_790_208_001); - _armPt(); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testPtNotUsedAsCollateralPassesDuringSyPause() public { - spoke.setPosition(user, RESTRICTED_RESERVE, 100, false); - sy.setPaused(true); - _armPt(); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testDebtFreePtWithdrawalPassesDuringSyPause() public { - spoke.setDebt(user, 0); - sy.setPaused(true); - _armPt(); - spoke.withdraw(RESTRICTED_RESERVE, 10, user); - } - - function testFullPtExitPassesDuringSyPause() public { - sy.setPaused(true); - _armPt(); - spoke.withdraw(RESTRICTED_RESERVE, type(uint256).max, user); - } - - function testPartialPtExitTripsDuringSyPause() public { - sy.setPaused(true); - _armPt(); - vm.expectRevert(bytes("AaveV4PTUSDG: redemption disabled before risk increase")); - spoke.withdraw(RESTRICTED_RESERVE, 10, user); - } - - function testDisablePtCollateralPassesDuringSyPause() public { - sy.setPaused(true); - _armPt(); - spoke.setUsingAsCollateral(RESTRICTED_RESERVE, false, user); - } - - function testEnablePtCollateralWithDebtTripsDuringSyPause() public { - spoke.setPosition(user, RESTRICTED_RESERVE, 100, false); - sy.setPaused(true); - _armPt(); - vm.expectRevert(bytes("AaveV4PTUSDG: redemption disabled before risk increase")); - spoke.setUsingAsCollateral(RESTRICTED_RESERVE, true, user); - } - - function testWithdrawGoodCollateralTripsWhenDebtReliesOnStrandedPt() public { - sy.setPaused(true); - _armPt(); - vm.expectRevert(bytes("AaveV4PTUSDG: redemption disabled before risk increase")); - spoke.withdraw(GOOD_RESERVE, 10, user); - } - - function testDisableGoodCollateralTripsWhenDebtReliesOnStrandedPt() public { - usdg.setFrozen(address(sy), true); - _armPt(); - vm.expectRevert(bytes("AaveV4PTUSDG: redemption disabled before risk increase")); - spoke.setUsingAsCollateral(GOOD_RESERVE, false, user); - } - - function testSupplyRemainsOpenWhenPtRedemptionDisabled() public { - sy.setPaused(true); - _armPt(); - vm.expectRevert(bytes("Expected 1 assertion to be executed, but 0 were executed.")); - spoke.supply(GOOD_RESERVE, 1, user); - } - - function testRepayRemainsOpenWhenPtRedemptionDisabled() public { - usdg.setPaused(true); - _armPt(); - vm.expectRevert(bytes("Expected 1 assertion to be executed, but 0 were executed.")); - spoke.repay(GOOD_RESERVE, 1, user); - } - - function testLiquidationRemainsOpenWhenPtRedemptionDisabled() public { - usdg.setFrozen(address(sy), true); - _armPt(); - vm.expectRevert(bytes("Expected 1 assertion to be executed, but 0 were executed.")); - spoke.liquidationCall(RESTRICTED_RESERVE, GOOD_RESERVE, user, 1, true); - } - - function testPtReserveTokenDriftFailsClosed() public { - spoke.setReserveToken(RESTRICTED_RESERVE, goodToken); - _armPt(); - vm.expectRevert(bytes("AaveV4PTUSDG: reserve PT changed")); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testPtReserveHubDriftFailsClosed() public { - spoke.setReserveHub(RESTRICTED_RESERVE, otherHub); - _armPt(); - vm.expectRevert(bytes("AaveV4PTUSDG: reserve Hub changed")); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function testPtExternalGetterFailureFailsClosed() public { - sy.setRevertPausedRead(true); - _armPt(); - vm.expectRevert(bytes("AaveV4: fork view failed")); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function _expectPtBorrowFailure(string memory reason) internal { - _armPt(); - vm.expectRevert(bytes(reason)); - spoke.borrow(GOOD_RESERVE, 1, user); - } - - function _armPt() internal { - bytes memory createData = abi.encodePacked( - type(AaveV4PTUSDGRedemptionAssertion).creationCode, - abi.encode(address(spoke), RESTRICTED_RESERVE, address(token), hub, address(sy), address(usdg)) - ); - cl.assertion( - address(spoke), createData, AaveV4PTUSDGRedemptionAssertion.assertPtUsdgRedemptionAvailable.selector - ); - } -} - -contract AaveV4ExternalCollateralProductionConfigTest is Test { - function testEthereumMainSpokeWeEthWrapperPinsReviewedPolicy() public { - AaveV4EthereumMainSpokeWeETHTransferabilityAssertion assertion = - new AaveV4EthereumMainSpokeWeETHTransferabilityAssertion(); - - assertEq(assertion.collateralPolicyCount(), 1); - Transferability.CollateralPolicy memory policy = assertion.collateralPolicy(0); - assertEq(policy.reserveId, 2); - assertEq(policy.token, assertion.WEETH()); - assertEq(policy.hub, assertion.CORE_HUB()); - assertEq(policy.statusSource, assertion.WEETH_BLACKLISTER()); - assertEq(uint256(policy.adapter), uint256(Transferability.AdapterKind.WeEth)); - assertEq(assertion.MAIN_SPOKE(), 0x94e7A5dCbE816e498b89aB752661904E2F56c485); - } - - function testEthereumEtherFiSpokeWeEthWrapperPinsReviewedPolicy() public { - AaveV4EthereumEtherFiSpokeWeETHTransferabilityAssertion assertion = - new AaveV4EthereumEtherFiSpokeWeETHTransferabilityAssertion(); - - assertEq(assertion.collateralPolicyCount(), 1); - Transferability.CollateralPolicy memory policy = assertion.collateralPolicy(0); - assertEq(policy.reserveId, 0); - assertEq(policy.token, assertion.WEETH()); - assertEq(policy.hub, assertion.CORE_HUB()); - assertEq(policy.statusSource, assertion.WEETH_BLACKLISTER()); - assertEq(uint256(policy.adapter), uint256(Transferability.AdapterKind.WeEth)); - assertEq(assertion.ETHERFI_ESPOKE(), 0xbF10BDfE177dE0336aFD7fcCF80A904E15386219); - } - - function testEthereumPtUsdgWrapperPinsReviewedRedemptionPath() public { - AaveV4EthereumPTUSDGRedemptionAssertion assertion = new AaveV4EthereumPTUSDGRedemptionAssertion(); - (address configuredSpoke, uint256 reserveId, address pt, address configuredHub, address sy, address usdg) = - assertion.configuration(); - - assertEq(configuredSpoke, assertion.USDG_PENDLE_SPOKE()); - assertEq(reserveId, 0); - assertEq(pt, assertion.PT_USDG_24SEP2026()); - assertEq(configuredHub, assertion.PAXOS_HUB()); - assertEq(sy, assertion.PENDLE_SY_USDG()); - assertEq(usdg, assertion.PAXOS_USDG()); - } -} From 15585cc615eeec442c65fca5b8efac98f2fa649c Mon Sep 17 00:00:00 2001 From: makemake Date: Tue, 4 Aug 2026 14:08:25 +0200 Subject: [PATCH 5/9] docs(aave): add realized 24h net-flow chart for v4 circuit-breaker calibration --- ...-v4-flow-rate-realized-flows-2026-07-30.png | Bin 0 -> 186161 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/aave/research/aave-v4-flow-rate-realized-flows-2026-07-30.png diff --git a/examples/aave/research/aave-v4-flow-rate-realized-flows-2026-07-30.png b/examples/aave/research/aave-v4-flow-rate-realized-flows-2026-07-30.png new file mode 100644 index 0000000000000000000000000000000000000000..fc233d9bc42cbd42cc9b3f2e30a9a288e39f90d6 GIT binary patch literal 186161 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 V~;lsoi6{ zV-Sc2&AOFkyoqa*)JI|7Hhr#rwP6rQCHSX*JlAQyf@cjXpDQvHs5{*WFG|hS347@| zmGIA5hUz}Y0RONou#S|4vtpB(j+}ZI+s$L84zK){D4t&h0>rWh<6nrH9g@5=3+cah z_pMVI%K4(n zQ$WW%N)ahf{p4AIk!UUKrA&-e6A5+`(pL@QMN{3&+CPc2iJIi9zLQbxE-r7GtOMH7 zw{|H~+n7vHn3%}jL6Cn=L4M&TffI}WteGn2HDc-i=ap?fnN9bUjRW+!+YA?ZlU0(P z^PJD|S4wz981+zN!G$)Ur&z) z`|0us04+=qXkoGdPq(yQcv+_;$l< z{Y^mhPi$Ta)41~K-%kvu%$4%m0fE>?9)F1iVDij2Z=jcv5bD_XStc+tSoQ@>KD80 za$$Z3zpojEx&+gneEBSh-g&4&VZRNtgP5wz_4Bn1q8)$?RAyc(b)z^yy_z#YNkdz^ z?hv=q6nQ;R3npxAm+fuWz45b&oOOIqV)&U#8Tjb<*19C^oURdq7(C z?&H4h^zutr!L{&4Wl6`uHd#4ltR=4atibx1S!)O>ex%e2Ims++v6`y)62nuv%$V9G zyFcCm3w+mEX_VpyQ!6tpHp)=dsL0?hRZHd8B%7|2AV=#A?4~LpNuaOios~%NWip0) z$ihp;a3(EfV^pvOL+Uv&2zG#8GvV#7Eneof@(~u7qD7x?klBbE9aNsGW_ww1@9}mJ zT}?mc-nwwvfJS;^$N4%=$jFJxJLSD#tK3R;$ILHF1xs0n=V~};rtDFUYQ!^}+BMuX zt}ib~BdkkmuLwOs8Y8=a5x_k8Tou~{)W8#A^wEW!Pu56PV7FhfmyO*cIZmNgEUWA_ z>qK;K6sVJRGW&B<8*S4vH6+ z|4vPPanA?cuu^(kpMv4-;*TrZ)j)!p6Rw}9B&S9n2l>S-_e)^}fewJ)09?Bk32-3Fo1B^4dtpeWgj1LFrGuLAFW-1}1=b#jO!6EF~30uo7M z4L%OiAeFoc=E$nRz^Zu;kZtRLjzl)dlx9PFgz8KlKaRq0Ox*uV0+U!3O{aj7Y|o8- z)ntdK!8O3R@NLah6VMhml8mh?z!2V1{tR%$a8Jz#^T-1`RzxriRRN&yFEFh13=q@G zBRpmbOrY83fA)?7ZjwXtr%z5h-e9_Kf-J_I637==hMY;b7h0+_z&U^xc)Jq!69~^6 zU|ita2Q8`J>tm&uWlTjEmq5Y_7>4oK0Cp2>N-bRq<_e&CB{}Qp)02M}RNL0j84@V1 z*HgQ|sBYt$NYh5;n>m*E9!+okEQPxpMQZNgM%i@p}{^1BPsb$M1t^ z4xDEiC>k;Zm)WYSD!1-LzODI2vVy)vI1H`@AupmwPj`3smE*)D0prFVpnO?;`z_r@ zFYfjrb`z|a0KM)Wa#tDvrHIn`}GXaRh-rU#2LE@V|r1FhX zpDrB!yX+~DXUQ|r!mDJ{(oBOqxy9tcq=TeN8Lb^an`Kj*{h&T z&@>%dBKm+!&=8}EtqeZ&4mvqpDFvwtVRaeUy1}XFh0j_noWCA#Z+0JpEO5XNNIIC- zv`MiFEF=efwkWJvASz@||8FActFz2BFi*j0lO#*{$8~%+G;minRSms<5J7-4RFI03 z<{NL-gUd8OT>>FZNu{O+Lox#(%Voi%;){I#`*K~#nz9Lcm*Na6~IPDS+s5IxR&Msj98kRz5%2{v)hlnfMfZ zUjqe2!VNpI-xgk-Ovwoef5J1hW$}r$qX8fyuqynT!~4yx#xqoB>m#5LRjT`fFU1R|!30}J{u@BoPP z`m|D3lLgXr&+q4%8ORQ)xiC=btd()>+B+dgf4SChcc9WRqpg0%64Uspk9 zJ@F2&*;P6x8KB&i0%1V7L;cJnXr9|#i|kx=QD$Ke;2>_}-)zUo*%4TwZF5-IRfuOI z)0dTz!2>W$C$;dWsVJ#%#m?bd(gvnxC*uuO9LGui9f)?e6D>FC-rD znq6+r{-*SrUZ}d(#^#+YgnbW8S)qc^bB(11G(obEjMo=f7p#f_$oB(N2=xk?Am((Z zjH}G~Pc4W>J!dxD#)SexDqJtr9Z6e4lJZ%8D&Wyf07v&^rV*Ydsp-i_E#yKcZuFw% z0XRgMF!zZ_s!XFPoe%TU<5M2N=f{p_zw%T^NGP#l*YzPQys0kpvkv3`5&g#tUG35SCT4vzK+z`&q0?sBt`B+i zUA7e{%O2R4Hm4qTXb<1rT(e3Bne7)-{Y4<5tEJAn_;jile7ypb_pdU_hwU{Qxp^>E z#m+XiIQ~o7q=;^F#|NSF4%wR$@7Iz_OG(;UWJlbxx} zh8Fwa1yU*Pgih}nyC%MApoq-C3ffwczc=b83cc;H8LZqYh91LzzGXAq0o&jXQ7ik_M51In;&M8R+XkB}g zyPWf9?fv*Z7Tq@NzOBEB%K3*Xx^dc0B}@f!AD@w%Jx~gizOsxW${#c)0>#FxI+=)l z)7};R8h3ny(|9Z8gfnxVO?s-XWx!IBaPDJF;+U+6`5yTC5W($-Np4F*L3B45N=QXE z8UbH6o~PRk?&{sOzyKlz72#JXbCZa7A9>4Ap6u?mdNX|N6WIYD&_Q>{=+iA?%9_f4 zjR+9+7{&Bvtf4tBvaDdxFQKYlR@JUkYZS#%TqZfB97&YdT&M;clY*x z_fJhf^~l@1n>`U}bjhz^rW0Rs61d3aVIcza5xqpghn(AdO4?_>w<#pDRTun;4~NqD z@!t?Kh*#~%q~hZ(qB4ZsY8GT0QvCAOxXqyBYZ5~LD(XDyn-Lg}J)h508lG!qW5Al% z&4D7N&H;EbAr#?>SGi?SbxFKb*T&$TQ&f&v)fFaxBTo~>pN`cYbF-X3f44}cj`tw*Tzamdwk}MW=_13ItjchKu&Lay4XZiP79)A;wRPWli_NCW9^1X4 zR5^l!oS4O3OXwxai1Gxj@~0*ce-Ji%%JL%-Oq@17@p44qL_Z`wWE=mXMoa(`0b`p} z;RM?R-u_18*rIZJ)7ma*SyEC3cPkhwwDnv? z)zGgZdEzqkaTonV74a$&O~d8Zt+RB6hLI_iAsGS7tOA8gTmrL$TBzli$Yfj3QaO=D zL)I@9%ZTJKgEWchl{{jiTi#^p&U{kDxn}<}Tk}&?SOLv$ol%W5kTMj)iq+|;#P@p^ zIS`66G+rv~3j~A64Q%b?vG1o=)~u6OHotP`O(vr{HD6l=jH? z_Xl&>(9N9GgdA5*sB-7^Mb*03G&HYzboS^e@JEfjAAZs*2XeL|Bc;P?@Xb$bj6G4> z2KKR$a7>$im^Q?Pis1zluU@_z6sf9bEw-Iy)v;OA%lQ1gx^pIT^>mV=!Ep2YGec+Z zE1|ORYzIGXoHH53F!@@0McDn}t-pHpCR)X`vim)Ki)D&V9_!;&(K49mAK;|GSVw6L z_;||nE_R<7&Rf&<%dp9nd$Tw*U#XuB`jlAYQ4m`E)zxeN(x0(3^?YL?Q*3id^*2zd?P&Un`pn1Kjjn=Sozz& zL>s@>q>gAOKpc1iIu;B+fNzg!SJZ~?uD-Ao*A@R^((#AsTvR07v-m=D&R|&iY}IVa za_&tqvj3~^G*#S1kToFFvf%Q0i0;LE4~TuVA^*%}@G8ASy4^4^4)mC%LQ|ECDcF9h zNH|+(!2aOD6`r-aK-e@l5|+#@^{bt^EcxKi)o|$jFNM`hK`=_Mg_L*qYdOK8ldr`_b zvCD_9Xxa~^Fk@+ArXKKWF-vDAKk*uY zs-VxReo9u8y4SwW4btL@QyCX7Va9uF7Vp=~vw2OO_Ap~U!Pu*zEu|*13WUqR&ZjAl zv_x2|K=`^DFrK2ZRf$yF_-&Bp+Mw>V@hd&*UxvrC4`f*@X8$0uerkMC)S#0h%k-*A ziRsPyX79CThd^x;P9wwZ`JL_d-UgkO%!k^f`*}_}#ESk__FJb~T7r(n+ji8-xv$g7 zZ|ox<(W%#(DN~Ef-2YmQ;~=PhP3;1eGWs_&Yu?ZG*C-Oxly7AUKM7Q0aIYvcF)AtW z243LsLf2vz-2!q{nmaI5o^`&cs7EA^xzUc;yFWpZ%~x1`b$y|5?wHkxT>K3VP8u_Z z(SjrH*`rzRN;5r3tc{AZSOzOiMJ~-gD{${|&!C29eu!H7Ts-3C0~60ZE#L`04#-p6 z(-5OQk*emZS~(D$*MO}VGr0zK0`t7dmNqsUP;|^vn0mu;`fqI%@Rm2Uw6unP2!{e$ zs7Xu}Y@xQ=^HxtPBl`Ik7$hy2QcQA!+7}V11LNuEe~(T1j)J`tRgH^rNv;J-D52E& z8W-(Cpz`m7EY_pk*sW83Euj%Nk3=U+B63n5B?TE)$r~e(I{U1$f{V+Xw3N-2cD}8R z)m6SxXj@gaW5Lp*#&+6sSzPh<&o8gHmz4^b{tQ-4ZOeioMKW#+iZ7mpMN2EIHK@sJ z3up9zx!{ZFX4mGmyVc_^38bu&d;_1dV&4|P{#-LDN4)LIdy~gK*Fhio*jF7&_!qX~ zf%lp@GamcSK|OeRbZM+?OsM^Gw=rsJeKuTe{(;r*aXK?N4F%`?tC=^vV1dGMZ>9*v z{ka-wnN|Ixg5WOMb8swNZ9M6nn}bVF_G#Xyt&k~bFC3>X*B!Cv*ucASNbWuAReQxv zn6#S@YN?jMqZ{cZ_xw1Ub{d~e$jarGN+bo4)|LhlzdFa|{($*Y`Ar}D`|sg>Ro3F% zXRHBSXcr5PlF#nv&8*4XOUJ2qKF9te=bL*VCE(pC$y^$=cTJ6PZMM}qa@1fxnA6wg^ zt)$lRvZ6PoJeg~6`y_jrb;YUk>X|>X@Hq{Z++~^?e46R@IC51}k#$_~dv$}ce6&e@ zYH5MyM6qB}Y1@p8lX`Y9W0UJaMW!ds3H^(9-aUfd+t-%$dl@ghjU=N+L!lRsi-@(h zC_YLz;h!0k+)rL3sl(?};!P5hi{g4Osf=>3UtZoyUrp|$vvM*%9%0()0eYiHDKY-= zpfRs=lBR5rc2hoCAazmEdxtWxo|;K9Q_6hsOkYv+O3IzZqogF6nB=69A4k$gEDHgu zsnDYQ(02W(5{-#-vh&NIHA?Bh6U)`@s)+`7?j4yIqBj|N(q?*B`a;2Zn{zXzV)J8r zYcEh;g!MZeNI@-1|8m9iYMbJf)u)G@3CGXHEMKh;biX~G@{+d*EEO7Va}K#)+;6y# z%J<~!eD855-2Q}uI}azzD`xyO*_*3!-EZ#EcQ2|1=-6U?2wgw+dMgKAb4eJ@GcOv+ zXT2ZpnNu%)j-L$kYwu(hxc!*+|xBUgzICNtLVHf?Bg}46QFGJU_N6mYkubp;CT)go28vG3-!|dqK667;sxIriVsc zyBUSABG}7L^==?%!9#2Z)2PE9G#@?T`sK7HJ*WJp-7_48`d-C(0JwFKEHu0{rjxHj(BewJCY2W%5Gj8tD?*E>^tOFZH%X}5 zh5^`s!LzdcPAu==ipG!=d_VEdo^>q@{Zh+Q9Qw^;@5VFZ z0mVZ2uWPoOA!cmX`nA}1WbndIUut2Qx&($JkCf&j|HBjVm+Sa$foR%42g!dgYCU6QK{J4^E~y zyszu*g>C=Dg*8r5j~_#?xyCxUZ2S`&+z?KM@=pieOg&tYr2Ft-smnh&%o!58e<)wS`3f=hzu@n zsj0m$WDIAd?>N7t!H$QF^g(Gvsy^%gMmjLRa3_aypOx{nLH3>$eWXMEYK%Tlp6UZ~lYyL*HwsR{otm`|y3J8QTZxGs1po z3EVs@c)AH?BV)gY^|EI|2NQ!d|vhp1N^8uxak&ugDVz z16n@(I==Gn2egX*`2=4LORD1!EBq3R;J^PK{rA&Z4zi6r7rERc6hEx7o~11pdYqSz zBK7)V#)$j5vVVVztU-{20O-hfKmR{=FGV3O;9Ak$9|{PUb^?kT_RrW;BcxSO!vV?* znDe|NI5;82G@ZM zG>|L^59B51D&%vs?;xXDtf&*$`~UM5tO1%r#L9aah)7TdfENx#R0nRE!IzaHFbA+| zYb1J+0*G=2z|krqkjA?XM<4(1)%-rhC{m0%h(VpeR%fWH+coPf*$QNFhDJxOfe=F8 zXQ7E*22PKSzJ`DQ8s<7URbg$gSZo%48#MqBp4tnJitXfGn5F=dR`I#+|9Rd%teXN~ zLvmp~T`_bb4WOqo3<|xu6#(lA2pksVQfb~?Tg-!=$(pkZ_@CCA*i~oHW`Ka}G#?bf z!GE^?dtyze#wb(Se0~M6x^-O1pRuX%Zd0)cT<+9yD#J~ zzKMykhn%@6C@9Eh)wA3)p!L40(IF9g8hL|V=iUdO-d#)7jTO3h&!(*H)_a0=?tmkK zAl@WnmlZMl32$Ubw$DnFip6NmopUqOF-U<^uQ-5`dQhP8goXdsvi_Hb>oh^xkG!iX zQ%5>hfSfaq8^);`s9S#5m~N z%9fVgyrFNkRqixWO+dyesDYLTB)PkZ|6Q}C6Mb=8w-5EykILUeCBSoMo9RMzP;-u#f5gv4(Nt<7WqP5k9 z)3in`Mz}b|uV0;x77x(I$R4%|q5~setA|`lmG^&t_gMw>bB6bo0q_B5Mr-}w(-`vs zs%wnQQR@R~i=kV$;Xv?aN9zfZbe zv-dL-_OvkEc@WBn@mrLDvX1vAtV*##upa)&e6$Iyc#z|0k=V%n`YogAND~DcO&u`W zM2xb`lc`l1RK3zX&;M?2D^opZ>UHH~i`GHuX%l;ug^D1-dGPK)P%|<)U_Lwy6kSo^ zlX?)LH#wHCt)=A%JKczx*#N``5Nkp=U@+ce#16Gi zp^0e-n^DO>+oy#f;_P{h%Ib_dk)qgpo{4PFXXhUSXY_V_Vl*aD$l3<0lq%*_=#C}gp$wY)4(`wI0m0~0-F<_y25%zb$@8*o%7*r8{K99s ziT06GaeHLfqJ9Nvx{tAlyS{_B=eYvfN+O7MhMZuL?KVO|gJ?8Z{13VW+1K~we#*J0 z(YG?Hwzt};9mFW5)MYBRMFi5bnG6>1K2+HoEPU4o@|X62a{9qQitPT~YfrmnAv~nv zwYyA(6u^|!rr*{M`nYAFwd(ib8~|?`GVktBh98TrWJ={cR(~+<0@?;*{Mi%Az$J1? z?gp}O(zS`*+~~p0riFe++HRkw8Rf(#4KIvRcHI5Ylq<; zUfpVo(oxreezNvp@$UAKZK6+8Z(~(fNBcnEtOO3dIWJfvX;R#~zvj~|Yje5jCfNT! z4+fpQ`{`d~U%UZrZgiR?EJbZwAX-=6$Lx66%ZT?y!UDu^vfzA*!`0KZ$m^{T2ERB z1!f5tvOi&lTM0Lw2FbYv9J3+!{~oi=mJPO_J3LkFvMPbe{d}QpkhHBTT$OG$rs^*yIdl}>C2NsJ>{7X3pAFuBs8!Swb$7Hps{UQ1JBMLduAP-i5o?FT4gf93;7^zn>LBl2Mw znSu8?U7H7;T`XwE2H+Sq16c5RQ}yneJ6y1)WnU4bS-|ak9%;s>DN<=fM*wOqvuLD4 zX{U@?=1UG0H=tAZfw){Nx)vBNgK%~B;1y5;ggw!p!T|m~#Y2+20&gTRPd(dwF`JY5 z^3QE(<+|?aMn2iRiwIA~a7aZHURwpF+jRQu$Aoe5XD9ze7VbF(;$Xb5@2{;UNfs9K z*#d?~F>pc#0iv=3BWe4r*3^gb;HK-bs8{Ja?z5^7UbR-?H4uwLfn?_}QeA26+Qbel znrIgeyx{Feo^p$&7iqd-HNIky+65O*1qdnBW7FE5{+==V!JQBcD+W`L;ucKc1x9zx zWAR|4hfK?(@(932(PB~X2j}abxB?IzAWwc|n7<=RyQXg@3~u5mMfQk&_>PH*Ne@mY zpLR_EY(4ULm(h(rSYUUsdE6F-&g!4djLU)X-t`mQy3K1>J33sm;2c3%Ak{I&(38eo zi>u)EIsgW~SLwzz}aHqp}7Hnz6Ab37~a@q=q!5q=;ZJ$n$pK=eA8WEU`5QMbsCuJdfD9-FOw=9@N=$RZu}__bzCHybRt#5p%B} z@;vu`o%LIm=QN{UoeFwAu28>T{CvrB`aI)f%10I8%B%^h(52OpT? zFc`K3I57cq)yCavkncT?v)jlSO}>-J|F-b*pBBaX;YVa{D&WT(0e76|6Zf>bqH@H3T6q8Eu%a%-uZ z489~w>K==2Vo~>5+aSFXaO`Fejk(H6spGEE%V-ZvMMp3VNR$1vxfKBfz5wu&;I;7tJU z#?IcU6C(EEIo(+eiqK>*ecrA=-W)*&E{jL}Wc?Z-V~!y3UIA;B0qD6q17L<_N&;i3 zAa9>SMED7jUbkHxIe+7=8EAH@;NHZ%;t4Y+UeKeN=qw$q8 zb<94Lj!n8+XYxvP&TEZG2jTinYPu6syCLqu0v~rdw{J!Al8Mbat7I{cBylpZj}jA> z1Sb^wG<8JOBqcA|Wr?q9+8o6++civqCU__<6D8{Z9lPbpOJNC>-Pw9y*H|zbArz^o z@i7b$M&54|O3FEAk=^56W99Gaz0Shu1QKT`qH}JB?ykcmN4hlnL;;f3bLR{pDNJ~K z-4yCK|NZ)NFP{l2P(l)lx&0L6b?6h0ZLr-VqR@?&ZN;g-BU5BKn_5GXxTWCS)u?&e z#n1pL82G0;L4qF{FWGZS1CEvdPNt$W4jE$M$bdlniZ;q_!>AOBi;8A9C;A&Ffb&4_r?$>dK|VnkB$-X$5HedpKxrf@S-VFSv-^HAo>@ zk3tOXO3+1r6_XM+En@oO9rqSp4BsBvt{z6+Z;OaR2=eAwVY{$+_dbKUm2Nf3Hn1>s zLfIEmC51?kI3&RrSH&CqHv`scuuY?&NQHBjDb)ZeIPEA>!E*U-seI_A@Be`5f<+DZ zAkRr4Egx#^@XGGakdZYAi#D<-ftRPNRn43V$k2m#`-8yMzuDMZZju`edI}`D0H-KK z=C3e6CHSON)zQhL=(-y_uTzw8`pVzw&?56DU6$RJe{Y zs2SQj2Wz6|yI>i_^-UbueUe~f;s|WSe90{^$Q^g~2O5*HzL$ti6)-9D;9UMcY4LD` zblQWECD3`5&nJOR)j8FMOf;xu?z=d+6d)6a5@e<^mr1YMDS_DgY#Py8!2&0xXMz^} z_=A+@);^7O8aQtMR()LjUO;6X06zWjC`9m5=%rQGUQms(6tZqD1leF08f61Sd40}y zPV@zs*DYUHqT6jEwUoeVLAf>_m2;Pkr%}oRS#!b01~^m|!g3yjYHp4floyMm9`Hy8 zz)M%z(|LDsKI1jg(c%^a2vAw>Dz!rG+5`(isnFq}do+cqnF$({C zd!(1j=1i%&uCYS9c>)K26+mkq!x`#{h25EZ-hh*G@~4U2wCKTGDun}0jJmg91$JlH zXri<}y}=d&^3XESWk0I@5V5p}fWCc`&TdbzC|2vZw2p4Ph7b%sg=RNosIJ z0a)m0T;+E6!^oYKzZl*jqnG_o9#=YJbhUEj@%G!qV=rzM`Ni_w#I;!$4Bf_wr*Cd(jUYTDWW907Y}I7Myuy*$G&<;Krd`DX;K z{Iv51pRJJs>j5E|wo?Bn=b-Cm!rVlL-x}*&l^;5SZXmrI_x>YJIocT^rBIdL4CT8H zPLPEMLXKO4-hub6H)2M^B69{Or4qzcgQYH$V^p3$3?W1QFZgnZ4S_qI8V1qBv6JT- zH*t=s@wPlKw+sy+EItu24a?4|jGr#X8ak=)tT<3&4s-B(0jZ`^#d85PPJ@s!kfBU+ z3s55fUP?yb4BBAG-N?VsPWbP_{Ladl$0*9b-rf}~;F-4@pS%JLnfdp~&VEXdFgY9p z!%L7+cz)kBc#6xZ33)Bq>R-@lOQv82_iiAxnnPUX?D`AB{ZVRfrHd69EHcr}iHN-2 z9p$oM<9%FF4=c=+8nJM2nHm$G8q`@dksj+@1Ur)ez=RXREP>F0W6m-u%V>=eX%Pvg z>4Q)KPa#?~4j$bT+V5p9qtYKe_Mzq8)Y8JZOz}FPht?0UKBu9fQc;`b<1=0F^lk_$ zXn&6rSSP~&a#}mD9h&h1EW#Y{4D&9=Ih}aY+Z9_)B6DBvZKJN@T+O_aGu7M}4#|Il z6s`v}Be)jP)aTFmqumbBn>~R*5CwUphc}GKO3TPFg0p*3N)f7)p=H2sQvTwYhh&Hc z7?tN~GxW`t64_Z^vVyyvnl|^b1Oda(IRY`Uoy=M6Gv$P6zTj1A^VGZT9dlUaL_JLz zI!rSSMXDyG1NsU6Igd7x9Y)|s8Q{YLce&gEz@S?Uo>rk;Q%o0ygu0Eva7=ts?%D0Z zhhW?#l6?b)%t7_^Q*L_rx2|)1=Zy3p;vAz$o0TEgs5aBvbt5j_Ofyyc41-6^x0u1C?aLp!5ILE)T;Dp&X5nYk_Qp%WUF7tXv>;8U09R?3 z7CGV`jkL7(@pM7nei!0cU{lUaHSJ;S_gPQ4uaY~AZwwl?Jcs*bgV ziAEL@o8$L~?%0n)wNI+&Q?{MX(7o74M{Wn^<{Gv6<0uLlSQ&?BJlm3rUT8!? zrW*Eme-PN|#pmZhxEpl_R>+-l&PQ4d?hf?5@A1Njb<+nPH{e8=h-Us+e~q(4L8;}a z?jfNL1l=QGXSdw~G<=8Qs#5%~j|a+t6we}LedBe;?xlUIY_L@xt0$^x8J=cJHNkfo z?VGp88}{?`IdI~11@E;`Lc2YJKPy@y_4q8bWw6F0?(rNj5Y0#usTRPYZv|ZBD;%iY z+#yfu_vzjRs_Os?@dw@j-df6QsSr)=LRH9Z)xUvPsvywjoV$#Qmbr++jzQIjo_f3T zbX+s;SVZ8qilzjC5)watrc`j(bz9L6v^>+zzk@^x#%J8YBB)9w_rELG^U_m`wCru( z|3)!9qajnDQlukWb-K(|JJ8)u3vzj0KiPiRwekQNjop1j&gF-c=IKkSo`%$T)YJh9FUVYu~ z)-;X8SA1EX)-bX83?Nx!bdE)=K=$o4w>0Wkdk=H*bJ?5y6cGTSife!-l@wlWx4y@G zdMTzO#l!yIKBzBCk6^5BSTSP!P&Z#k;Cyftg^+@WhQ(1?@u)iiow5L6WV4+7oA)0> zzh3t6ufXgJgBt7kst$vXE-V#}vy>T?wYS8RNs+Qw zem9q5mkO8eq)26!*hw_uQT|z4J)vU2yFhbFFL=1mvFXT>W7BT$r3Bg{o^93k!7O!5qfT%jJ9!_)sDNheaI3KH0y?<>xlWys(&?%knBQG8W;? zsj^D`JR@fGzLG6eGAwZLu(I@)G?Zo!WVbP`z$wKm>DILFROV<=wIk3xF;pzinxk=r z>6>_Y){~eWm*F|XY;u*%q|1C?UE}yr&CGlp5!aZx$4Kz{pSr2*7vg_DOyL0^Ol07I z$n?%|NRN_%R$2-e{~(Gj+q*fl>|M%K_nb*#3FVbK+w%dMr{2-;+`KK*ZfHE!HQig4 zPdnAq;Z>eDxxGSKJ#!bJakrt0yJ%*ln2re{Ilv$pC1t}ToX5QLkH%1s08KvIzqM9L z`01>w7`y8U0?is;NoFIj{%nNIJF7bAmx||<4GiWn>4}tvEzfk+G50DLlOnhc;_aJ! z!_qFI1@JHhDZ`$e;3^=xjRO4P1Zc{+zeO!a(bACF(}yfgxd3V8H`wc8eP&KmSVcss z4yXIM6Elo`XGXKk?b;8&;7C?~gnD(xVlYi|R(QmBGjS1EN}UvJHlg4ZA~`tn65W771p&xF+*m+95_=o@@3& z6v>`M1uOf&NKb%qQRm`TR36fWLBNjQ?OY&7uD=00DO0e>l`=JAe^B5m@e@rdH zt(C1zul6&7tObw!6<5%-MZ*-5M!`N6098e9c|rL*Jc>NUL~dIN~`l)+`1ef?DYg>1xh?CN$hN)*r-pph(Q;72a`>lx%%bb z*RtB!&|M_?D6I-AWC_@39pkR}78>?iIP8e*Hgjr(_;alZb{{igph=G0|3Qm7>cbst zNKGDJB+a(YMHYW(&l`bm&ehUsHA_e~Kqai{+lIHl91FZ zQ)@jaU<13NJpUUR`wyeC>65xl8H*kVLCRD9sJM;r{qN8Cix1idt$WHnE_S7q#b61q zBZxkn9+FoGpD*mTSui3Bi@yZN1dE}E@G$Gs|Pbm$_o1ZW;-4C3}Gv{0|C?EVe@aA|tJDLH+wQ*ObU6shBK(X>J9 zKGH@tK9%I5=z9vgpznk418Vx~J|YUmQ_A!|Un*vC`;HGe?X@!cNUZ>%n+0w>(r6Zt z!lls_Nb+}GI@91Kd(Qps58gz;0s9ukjo*R_#Sn+~Ha9yd>4~$C$=N@XeZgJ`y=WYW z2X-6!D5EEapY{Sb=Sm_NG&#Ox;44EAf4W+{Vag&(!I2~joP7+ypW&(+vdWE>^l<*N zfrQwA>m2kPg}F7TtH3*D1X6Fo^~!HbV~iX0i;t3y9Foaso4=yC3wZlmIzb z#sR9Dca=)31RW8l5@2yroYduh16Wy5SH#IL+(O(JEW+z-cP|#qFDf0VWTj|=xH<+> z73o*!&xTz32(}L53@3P={~bkDBn8gp7SQ-{W6EVF`9re{R||cGR-aH$u=hIR;F{tDtkuZK$ccvqRjLN5KsR(aF}i!cnQhR4*-k&i1$hkaKJglt+;q#6$Gp!9 z-DmVX;$vb+wLqE&yl7zu2iVe_VRS6}7_>&pAie6Kl1~Ez8s1dx`t=AZL!D5mmA61E z=(9@Y;^0E4&jFzO&*QhP{-O(^#;6;%Tr>t_vj9cy^2zc+5i^Fn32bbK+_BiQU{*;{ zbu%}Zb2n!g&=htDh{8W{s`W!6I)0FNpJq#au9)p+Y`B-V+ zsu`_Yz_V!GmB^!YoY3>SIenN{(#^+2x>*hhy*4XlWL8F zV*%E%LCS#yz6oHVx6wYi@HcVz{pK~xY|?M7knkb%j{qqqJ3zRo@lpB{)QKoA;G#Xf ztrx7xCwmE!oBpgVs6yO6G(h^4b8TW05rAO-_gg*ZYSCUC)6DCD2_5S@zG%dk|0=*# zDDc7@;}T$(G{Rh2G8$#LSj~0p7wqyuD7hwMZUD|RNZYAD+<|=Tu8pvu^>r>Ms5Q=v zIymZLJO5a&BP1gMf=!G(`iCX>v(wSg&;KAtFj(Dq8qFQM0EJ;Wj4S#;#nLW5c><_U z#t>eAfB(#TNfH2MhI6#PeTVTOgx<>IFeia^VE>Y*9OJYuUk_1oc=nHF+t`?lie(1 z!)~(?5?5zyNE6Um7u3nx!$`G|w5GKd)-gujC6xgC{c*ek?ORK<4@F`A8D!@R(E>4% zF&$5v*c%)!%E2~ZXp3r&dOY%@9E^dDb2v3G!LxAhXBP`Mt~6L!(}g3Bh>f)24V^BiMqa(@&Jx!ygj9DJ8s-14{-x`L z`!ZFcOvx}IiJmD{b}D}f@(|QJgRXR=Ax!%D*|=LI9#Tcw(qx_hi7EiISIo4TprBF4 z?q;@>os;jnn>J#UZ*xesMLn$Q(oU!Q@(4oc0jgct9&^3jq2`mu`U%csDAfszjulW| zOj|geETljQ!y5(=i{QgUDj*jkW&jES73nJIjbJ{(H!`J)gG=;d1^63jH})#9dCv;; zKeK}q-qRU$fi(_AZl90ohYwco9$n4a(6#AGh}a8pELihY2OV_bX&x1}JbFwYGy=Z7 z2u@q|fEA6jt8??f+91G@hGxo@K)yggGi#df(O^Dj1l`|S(c!spY}o`aUSCEYuyfB& zGhCF=g`|MS2hNO7lmVT0+M-+q?e^@o337GRJBi8`7}kdc^?Fq;8pMuG22hXK}U#ZVsUR$9F;;7#XT+V3CnsI8@jMiwjWYn z`u}#0BXkt{V9q-l7lcObeT37%1vI+RumQfRdV^zU)L=D*?75mvn@Z!~D?bZAs9Od0 zDXX}Do^y|^F>mA26A}4b6I;`zpl*D{?5J#Ip_5{yuaJ9M%QKG)&s<#!3@gLjZFTCc zzRb9=-udHW9ot>(xK+lwvOa%+oMt+SBfd!STW(&d|FuUCXJ~(Ml&325 zX(_)XzxLINe#0?-6*?GV&O=lA8%xtiu*@Q6T4ru&S6;Hg9Ksju7@O&^&=V-9v`vy@+itSX8~R@Kj=BE~alb5~*jgnJFXH#wsp(6R z`gy)mAd%Ae2B{skV`I86n5AdZ~I1a-?=0%rpnd#L33MF-3bY7&x{9f1`@LZWyUj)j3k@pc| zo*mE5{xgKQ1Un&p=l7*Xt|V^mVjYrR&MdDJi(a|;BZ=eFdpiLl!hWN)m>s`+U^d9t z$mnHN@9mWlOgKH!~vIB+e#Vwq$&AASq zM>xY;I@(-^ZbSB}2P{xyA6PFhZmdakD%dG|m8*$adpAiJs;a_1aGJC73LAtcPb)8U zy^#k1tZe)G6&ocB+pZL4s39`j_gOC8NpkqqgY$6l7)6sTf24(nAKiecH&lYrh|!^5 z+NpUvw>{TW5eT@bWw3+pU4!cpTzZAu@yvq(@e2vxo}cIEI>PX{J!!Qfd3|2}J*SH6 z!$YHLMs|v;whD^gm^6irwCNTx9Obj(6$fp5rsHxewj{uQwCFJ(rj!$FZ` z{BX9Y#b;~pB^*Jo)Kxx5=Y5J(%a}& zi{0w6fx#3UBH0(JoM-4}`f~NM=p-Hbld2rT`Js`(kgF@Cj`?+nv8{z@YO{}>e@~P1 zQwBzMi;`PPZ|iKqo+cM;_&$4G#)PM@Ih0k+499%7xj(hd$~+)>gn?K;+D7A98%U_# zX}oRpULYg6DOl01RDH(~-MgW>loTdSK0t|cdTcQ{@CnxWR<~6U@910SHWL5$e`GW3 zIOL|~&nCESKZ636uc|AG4_@AXjI(Ahvl6FAq`RJe?wTXzzP|m8r+}wOJC1qX zUWOqw@@_Siy|V$dCc^mmz%fe6uFuD0mAWI@70h&ceq9gHB(ni@`A>MlbT6F0VTw$5 zqbq~0F+YLf7u>mU8W2m2Mx>q6M0Kyxt-vd9&bX^QHg4mrtjIrK&HdUvVaSLrNaPlGL* zJ~|Tf@cy%yaTyC8cM+zOI2HTPG{8-Kc>ltI0sU}##1q)p=h9N5I&ILGc3m~+tx;x; zaNsknZMfJ}N7mi1wKn-d(){bh>e9LOI6tS^S3}>f43i@A+_&uKwyZ7ZN|^09%e?Ld z4sZUh6}21llAI=t&X(TWsF-WV90D3dnYG z{SNO4v6J%_47I%F)C(Cs-_ z1o+)|(wUsSip7I=`AZ6RGhwl%%5X*$2DLio=tju>l+B9Z8d$jjG>;Dfnnm(0^egL_ zLzIhpjpa4Anpr=Jn1gTJEd1(T5bCxq96sAF&vHvcBj`xjeKrbIswchJjWZI|Ma~eBGNvOq+9&Np`37qKIHvN z2tjW)O1Q`b3XNtr3WNCcoOc?g!Wzo*f(ijN|02j1}0 zrZ0ukMf-0ksN`s}f8MUy>Ub=1TF^2yeWmAZrSs?($>jiShr4B#TcY0N@+W0*OmCD6 z=52T9JaKo3^th&M*Zie)@>C}?ir7Ec*w&4kj{!F@!Ts*zqjw3 z{o>yUU8@rI#d^vRQ{`E?`FfU=21RaE*JbwG{v0=DcwD^`|NNzKe8=L7OKd^SR1KM= z7@tfc#fXX$%Y8>0xfYjX+BdCN{iV98ROqG#>foQkf9C&@H7L9@EJ`f1$RH0_IbWrc zAFqAt@!o-M+HzxP3*Tl?N{Ig_J5K9;iOqp$+`;<{%H5w5c~O%^&$;=Fm}(}E?u9Rl z>iWqY({5VVE+|6B%$zm>1!yyCs=PeVhE8{=0Bne-|vOGeJ`O7}%Vi6-( zKYj=1_|y4DQoX$YchrbPX$5fo&KJ|+8jLZtP`Z-m14c6nC`|n)&>>oAJs(2>m{p~kWo8*gd ztPHzfpe+%XYKeR*o@uLH8`GO(vK}n|kh6pFEiLm6*6qo*MU#4b}DzUESl#ddX#|rhbm8O7Zzu zN#(#AmvgTvLrm_82d7`EDX~e9v*ETM&3|+~o>9B*#wg$V_4Lkja~TG8quU#K^6(>mq6;k462qB|Ln$_mF45Qxc8NXxz~W=p3EQQ+-yKDHfhEp|XbS zYJs4cXH=~GFO^`qr6qyVw7qckJkNXJy_}|;9hpiC<(-b|7(Fr|zu_!m<78YABa)-t zOU@8kKQkg6cDFf$+Y;hkn>i8r2DiD;qT+qLZv6KcEdNXaW zbSrDC4HbVzlMv0f!-?gLe||phn!=Gfi zXB3m@E^y474YfGS`_P@DmJ)7Y{Svu1&!vs@u2NIrYzWD^G$-x&%-Z=@Qyac*!FDWr z3wiMAzJ1<4rv)ieNlN7?Gya`* z!jH*3_m#yI*_OwV>4HZMk)hWas+C+D7>LWN3$|BXR7oVV`r`om4!9;#W>vKl2?dw(WHM z)&>{V=h4KHfyjJ5pF~r<@{9OFj{{<^KbqJk|ER0&)k?p6$M^QyuXc9qFttCfmVR#V z%T}b0c;PoYd~dejGFO@6%RvEycDEw+OjQ%V@Y>W3LDKUJ35B0uG7zCV_5&J?Kmiy8 zTAJhTtiL-SsPxj5Bf1jW9+%%C>)|%@n#%5Pi4rTjCu?3UQx@tOX6KEBnF~d_xH8?9 z`FhjJlzzr^maWCAO~-#lZQ6=B-I57)gY%`W++#+E1s7dLVtD3W))*BVrX82%r6n`> z-HE!E#MTz^h;ARBcp*MGwz$tPy13yy-{7I{^^8u9=qHeOvx;t<4j+!~FTxw;TyD-M z7Gbk3R?mC`jf1a2L{dEQ8%LUYX~braZSu zf6+Ll^I-EUtp`$X1CQ50L9*d%qIM{?0#Jx2WatIAxbaO>a?%oe_qd&*S8;+8NU~6~ z!S>8v>*lk3xD4pJyF2RC$MmQb59*dm&1UnE4_vCW^t4Z;jw>{qh2^6P8@j#)qoS5#Vz>E*ygUca$`C6m9mt*YHaN2!Ny05ze&sECx2`g% z5$8OvJyqz<^Q~vT{(ins`bJxK^_wKFyGahL#Yb1Rqg;lZ`GNpF%&_eVdDtcK!G2GG z6Q7W28edF*7OD9hXZKF3OjOlq=t&WE-Cx>!lp=59;(+;m>&^5DFYD7rjb_cd9?m5T2w&uc$c z($m@J$qgzWHP#_Zwte^qnDH>>6Wf=j@-U()%_ZU8a9LV5uqL1jSh~7ZWt6FIi6e0 zBE8v|AX6O0V^BLhx8W5i`Cw_PaT=2D*`h~-#BoZ=K%WFana2EjPWBg9O8ImpQ1M>a zy4iO<+%uyK{*wSI@fn_}->To3WXW9Op}7m${KbIT$h=R&)D_W!d%|KZ5sH<<19Y}C6JzP2J5A+m(%5!u-ik~!xl}`% zf%J*2$}CDOSsM_Efk4{9V~-DRep%dP{WVoxK8aT)X7yFp9xw3N=r*fTW&QC;UDR`; zfIuqB2_bd%NCb-h`Be(pQZq$;u4)$hnplj3IDKD+%4f=Ry^xXFtiR|TZqB%I@F+7+j z>8geq&-bRG*vIxLcQD*Js%9;tmf*lorHI#*PL5pfW0jPfb2u|bRq3FU^^>%hEe6N? z%#S;}HNrzsxg_`LT<_36ce}Vn$x4g_XVzhB_w0sZDLX|H59q2S?Eg~-z(>zi0hku z^(8XW@1K!vLhZ`LA9+nHPkZhDPO4UACvntj zTCW^SYyVj4zD8Nsx5kDlHg83l-#INkzXS~_rpFMqXvJytl%;jj*Oixkna34ma#tM< z8jZi_`8Njg9?{#QYOEYXab)y{(X=4p^15a|W$8CTW~xrFyNiuG6}9SvIIDzYU5So_ z3Nt**!}O#~X>BWwlbw~%b5u^eP{to56RHO;*i?z;+0Ea$^|R)}Jflc&Uj2w@(y}d3 zm*#L<0UG$oK>YM6`K8J!NzD&4wzaF_JQ52pr|0IoS2(w-0-k6p6!bk|k#1f)nEHES z{(w^=c9}9TY{Ig7V`qiB?4I+gTI+o)&g!}#$+vA=Mx!z&QgaJ+ENV>m3z^lM_x6sL z_!<&gYIzG!hwglD=CT{suQS)^SIDnSoZI+WU^S<8$Vu#M$lOGNP^5%dmfg;DZ;Dc= z@7!c1b~Q?9ww7&^tae|#^1`3+04ATJ$#PGJ!_JI&MFX?uOSt(Qrj_q}i(3J-1vAAS zn){<@iMl4|Rs$*}g{Y?74m+*|x`}CBJgzJmaO$-k+wqovJt%R)MX{l?q#DHtsu<>9 zTTC}bb=RGnTWL9XP0HK0jmw%?`#CEZIj!s+rb-1{BM5K5x7Q6tJkOe+p5Wy!%@)az zo>(C&A6`m4^_BJ?@0koWCDP+uHQO0%@a;{DPA`U%7FFj}8=N=O+}QVUuC4Ue^|~V~ z5?4}x(ptG@M;4iAw_GWhRNFgg9bo^-GBZQ>+s#__5Hg{4>v-q9k#8ifXvxEhWMet! z)}nABXlZb3Q0&fykgJ_a#RUbVd4+v@zxogV6We*R6Q5;_)p$D8+MlvD(P`s8^J;h~ z?UbRlOzX|@KqB=4cb`JP89Hy*))U^pVhzSjW1&;9FQ9X8A(UR9l$hmjOGCiSNotb9d5e`MQbG|V1q7tK z8&N`}OH#T)x?4~YrCS>5?rsEWL`i8xkPhh(ICHu8f1iEM80Q`DIPbT2eQ*qQvDU-$ z+%fMtulc*M6L!X%;-=qC6sa&Qd5v!kCD+7@{F!BTI~q9ucDY~lY@6zA&swFhKz5}x zIpg}%G?vM-+1a^97ZF1Ku}4`gVu#tW`>5vQjW)WZ6;Bqi(4V=sX|aV3IiWSb?+?q@ zE7<4&&Dn>R_s>*iHvFhilU7RRmVOo6Tof!jCNXS4o!!=@Fz35|@WkZ!!NB9UtBgAK zCtj-0Yk~sw<*lC$>Z5blDp)K;DD0pY`kFA>XNiTcd?z1#A$?4%Tujl(Q@qCdaoNNu zP=%*DL&V$Yme6Va{MLLn>GQU&_phcVC(o;^W$MlIpWz)JdX2s4z zT2oqvQ!gx2i~BFUUBqxtbZ-|s9a&q~;;dPftIQ0^^dG~QHj+Fq{Ui7=qq{0kb*xx* zf5NtUWA3Wo_Ua%l`DLox%U=(b_K`gV9)VHNx`SZZxAy5N7GhVUQi2-A>4qECU*0(O z`fnBKRjc)5B@VSUB_+SeOwOU6+OfFsT%LHtYj~;c_Krs6m{5cK_A`6YNEY4JL7!6I z!W8w4jZcyn(^Kbogp=<>r;DE!b2q*)@DlRaQNP4fezHB+X?yNmfLX`TD39KMHj6L4 zapr+uRo%r?_+Zr2d%UH1tNC59&0fag_8sZz<=E>&Qy*tZ8i8?n-ZB5hIj#M23vWY6 z%(J@SXUp_T(VS86xY=Yz^(iTvREFoW%y>>}V*ApCd_G)&ot^^9bApjYZZKt$Oe@32q=#F z+$!b}DqS$K=7cI3Cn#wymrpv$GDHX6~Q&eDePR^bweXSd?PS$cD+r>F??v|mS>9GsDBdXO_TE? z87rDAX-reUKip2$&_IO8Js0~s%1fpF>Q6m(PrztVlu_=Xjc$qcp(WY3iZOpTds?OM ztxoY;sjb(^3$gBwwU6%lnhwXH&&%tq)#Mu=zp%BDapw+{p=Veq{#f(Q`3^6E^Ctz} zPuFXo@Sr9)xxZ>Pc1*u6$jkg&yW`uicj;X64+Uv7Gu@5Mk(LK6lJl*NImf`mJzh3# zLwci3kXzB?@;FP%_T`KaWr;N#riYVf98+U@B>7xECtKybPdHkv$<(vBM_M5aH6F`U zg{qA;LO9`p$E4%uzV;gp%gLLli>9DxE18f7xyl_^N+|VCszdKIU!veRFc`0$-Ib1`P)-SaTeIGVvds)f8w%_S}Zxy`R z6@FRGJ54$*{-nG0mu8lzrr>0bN%mx@&$;#v$~{N4O4NMJ%?{;}7l%cRRTZ5B#>90$Y z$ZIlPE)%-uAWRKA<#30#P1)4ppke4nMF0|tUAMmW-h(cAUND9vRF#H)$w%6UJU7g{ z!^n7n)~*As7-OKO5OLGiUTRGP<$(>teSJqDv706s!BqiBvL$r?hxMsY(z@V)-lkEWDv-imW*vor2uoJh%T zH^^H%FvVArVBe1=faYRjbnFAEUt2l1crop8H68hls6d+P5{<#Jdts&`MH1uL3|e34 z$zmB3o;?j4WG*SleO4uVYu&)jS9FloW4|W zt@bpzh-qn*JHs_pBB&2z27KOge@y)dOdA9U@2j#!A#@u6$g&2pkzJ z%Uwb_)vx=|&|QV#+(;7Y#<11pJSarSm1tg6gQ6#nS<46^>;kH3Wt)Lj`n>P?Agw~O z1t@Ub2j-kS_MjhR8X9C5M8h@x=AcES&<9%9S?@Q4-Vy`>s-q%SjCE*fy&ivLA*4q% zwrpd4Lg2d4$*P#H7C<0a13lYQqURFh@&wt1P$WP4q~$f8IX~#iBR6KB&3!#n&nZ*l z-klONUMs=$U+C1W1D3@4(ge_^8Md|$4QJCJ zkF$pM;4*}Okx}EzX)~f5aS8$11=+v7zh$;Yiaf6<(6Qy~AKq*z`Qh%QZd0C9J@OpA zTY~$~*DobuQ#E8lv-b0H)m~WXFLN8(JhAQnq>woYi4>012;L~J(zAtTU5jZ!xKMR| zM6iY$f}p{y3j84&Lj-qThuG?ahY04KE$H1l^_{qa--fkN!8y4&u$yGAXd&u}_*X(O zG`kanY^TxoG$Hb)%vO!giNfl`KfAB`zhDjE9Q@MVFIds9d6D3?4fOT%SMfhA9^?na zAmqvqrD+kA-hTYijlW&%HNd85OwS33@gJH7dR#n1fhmD>LYC)4|3}LCT9rAC5|4-S zgYUHTb*!wQ(($k{N-mh%FlO@ZUZ(k;_4;&y^16>N9CEIatt2=P%?5}F%f^-5SgtF7 zt%OzS^JNp}Iv*ZO+R#cx=wbaRKw?;~Vq#*-(hLnp)=#;s^hfea!;l-^yU>upLiCs^ zLVXu>(nEhaMq~-R?4}Wnvm~a$@gGljhKGezxqLp;^4)9JsqBQIF`5@;M0ebKPID&| zN*dKRLKQDYj*=Hw8Kbr*W1s%K5Tc`2Lmi`In=-&#DRn}cNLI*dOnke#9^!*%XO4YM zjKA11iIMerlBT~h&+j`fdT1{-mAaBSDegw7q)T=q6yxP&?hF2|;FK{9Sf|wh{|1DQ z1}l5^Ql=O%&fV!rj7p+h`#vEE6c^MycSqIs7ZFWf2oJGi?VB&wPVasNw;58*1c;)xH%zI?V#4G4w zmaNs^q4_Q=?RS%6nK?f0ops zuN7}q$7$K_d7M0pXHVqAY-y3oDsS~9m7@-dg06FyHa`VHMH%C4P*ue#HDNPrh&f^F z(kiQ_F3ppA{d(pgc%ub<1WFg8k8ERzQ%zoOOi%5aS=&n_)^xgp&{Kr1?|lV>1(_=x zPJNm5DPl)D3vZjn1q{sv|x4|RW-`+>PmJwu0n*sm|fuC=hD9mO|LLVFxD1L;@H zf_D{r`1q>!U6=(P*kN}ylSI5+TS7#!$JC{=1DTkjmHcT54!#CIby61GUn8$}xoE*GpedOisM2`SRw?LG`1jF3Bd@S|P)P(cH)NK)>BMR8L*Z9oUK<6&lnD>e*-#8~O$Y z+`E;E_$X5T{FBXEEQCbQQocbWU{T6sji+oNi(c(499v5;-{VYX=If5OHv7@nC0?(W z!Yr8#n4p(FRQrO+O~Y-x|)!NeX9><(=YCyj&0XSU|x#xB>VDu}zB)Zo56QTYmWV zm{}j2Np6yVaLG-NYA9uLQhmffR<3qUhB)@l%nNBAvb!HhghEvQQYe1&o+}?N z(yH{XWe`$7@bvfpCY|$4>pF`Y&!6E6jRaI(bly;zzc=-Fz{gtez0%pkQ*I#t;gyZD zSB}Qqj7v?hv92+x`}mujOkzf)s&Pz)sqVxzeVe5R1NGYd$?7lIAy)i8Pf?^0)!GI~ zt52q_G<@7wOq=V+Uh}j5MCIj&kh3VaIt;5Y#hq7(UXxL`7n5%1LLEm=8bz7Q#d1`%J>Sgp(q}M?_cNK> zq$>&WF8231ImJU;zS_)P%zm;Mf9ui7rl;A+7an}aK)x2-x`2e@K-ehS3 zUE%#52q4vpw&=%U9dj5i^||hLzYMz4HI7v$KdvRT%mhB{+blAWx6ao3Ib}IsL1RBKF!(C6rL+}T?5|0q=*bT*Y)ZLc-pYA7fxwUjS}@(tH4`P7LWtpYER~yw zE-%i{uZlN^fQwTXC`FEX^+0W50CUr$UCPvw(U_|)E*m^Lc;{~1y{3kK0_Jhr9;bh} z<(}g!Ju~3kJykPm>wodAOtW8;xY|Faybx<)VW1}?rvBa1epu{XN>0P(Y5X9TzN&)K zDR3Ef!;R(oYR--zi|O+z?=qnVMNf^X*o@vif7_%u7+Iw=YVQVW`o&=-Pc8i1F9%Qn zo=^SbP;xaNYNcLI;8Lg^TBSznYjNpm$TOlci+b(Oy9!!U=r6eZ__M$!|Knu)D2Lu; ze4nkV{8Lkkf{&5pxwKL=@`qeJ&nGd<7?hx6VW-|q+#mZLw;#!oi0>cw3I93F2*$7Z zw6gHrj?CSxFq*8Ae4$rt`?UGT&jLIFXj|Yrs84^Pj4BzsMY5BaM00uyv#o!OW{gw0 z7s>9_Qu72(37gPA=5{r{WpyuG1^Ee?ON?IwwA1jKnAt<%w1<5d{&|Ir31AUU)n?Ds zA90s@4)v)Wat3E6xc>O`hBfHk2jX?qU~$vQI9lXPF^1iSYEaHU@H9UI35G6<^M$X+ zdR23g?yL3jkIJEUZ1zyDm8Yl2lYAEjvv}5E`N`OV={)TiEAA!bYs#S}I3fp1eqR*_ zP4Tf*>}3XD29P3uPC~X{KPxuexcSNQq>l69*uAIi6>ZENstN+1!7svRg5mjOY+s^U zTxx~*5dY5*;UO>JB{$xm0l%}Q^~a2w1(rN9dM>P(84AZYT_uO>TZ+7WZyGQbsuAZD z?Bp+$t$Q&l3EMA-Z2&mMZwe@PQ>pHYH)=Nz>gk=H^$V%R6(`C;_@szHAJ!RE1~hp- z{Cv^Wd_OB-G{fWBg2Tb;x9$0Y4iZh*`^hjiyIW2cF$uq`<>bzl>hZbzymQtztr3Mr z9#;1P4<#(ra;G87;3`4CRE6n)!j$Bw1O z9_*&S8PL*0WX4Di1>e0*(sQ{#rmMG~At%XFX+QElzz)E)nZU$sbMazROiQx}YFhWQ zI%VikujXh*6ZhwC(nB&DjL-S@2ih8sOFUwbgZ%qthjJaS^;L{T?89ElN1IlQ+;TH3 zFe6ZTPU+~AXgtK5jrUisD%~g+K&b=V)y!!QVu4Ei*z$nHzRWQMZghS(SBWKCywmd?hG6e+% z<$3q_^hK&)@oALmQhuG+xb&$g9nFv8lYJ8u)Pxt{0C=?)VHg9h*8|l=MIuOvB%Mn4 z?o2Eyc~ZeEgA>QUf9Q!zg#42)7GzFXPTkUO`pp?+jnq#@S(JGhakoDWJH))~sV`>(C{uE z&J5<>dV~4PLFee?B*@MdX_ZPbDK4OnBKjaiP~Jq1!CdeO^E$uwRj*_DnTRY*vl40~0R zlSZH1RxGsr{8GZ<3!w=!KV z!6}NxsG|imp_jVwLH?vt(twzC%{?qskxVRlZg_h2k)yWHLx^l61Y4{!l$81Ep|ERF z7ONgr825bj$e!CwX82P_TL)vWL|aX}NqaJ{Us?h}J`Ej~q@|jym(L2<=jrr3B%^#! z$9`0C?61fgknNYfIdPkd?X+uk;kW5X;ChhfV0>{tN|z?{$vjOUx#lvaSB0`Rtbfg} zB{F)aWyOAKmBKe7qD1##r!&nj?nFc1iQguLO*9@bYX>`Aesa-2heY6AQxswGGU9=& z^DDmwA~VB9w(X}9?V2cKOB>a%6!fa37q=Vde^e>6g+v!*O0CW37f$s20RL-#nzX^aYK2ZpdGdK2&QI;%EIu#_L`-db%p!f91w&y=|{; zOFd8*S{F2p!DUNb7h2Y3gHnrIQy03x8!(M=UPsYxgA&fux9!`x_aRV)ZY^X~R?gX4 zcyayuISn|!<7Cqz=F#8R@dj~Y>x|Fl0N{$Wv&K!LiZ{k5(_VI%^gmy3FxAG%=JUh3 zCu7(3@t__PrQgP$)UdL63BDyr7!$s7MhgCc@E9&(p*--yn05P!J!$(zkV^QIs_oII z$3MQuroMlpyY>#|JmQtdY_;~O!1ITFr_KEOGdU8wPMzlM~HFbM&!$vFJ(Q$zJJ$jzU_g>db|``Hxpg^ zgHv%Xs9(f`8veniZ-kTL&K)Wa4h~iiTZ-sj6USaI)%s6gj{d%vZ_!b4F0fzt?|c9K z!#nrPLhk(MIw$@gu9Jv}=AZjXRoc-;|IZKMdXTyP*S#+72vPpe`}PE}{=a`Zn$$nH zHmGNVon7WXH%RZT|NkGqs=Kwy{H4rL9E*<0mt=tozboITii+`IKkyXns7!#cc!!vH z?YI6QbV-^*kGvxgyZZB#GdC6Xp~1Wljt@+5{Evp*%5uHk@%HQuUTaN0^P-p8*~bfv-Pk zF#7y{7);c(-V@DkLVx66xJ@S0B51^08#VS?UGzFZuo0}lLw+E@I^&O$V!+w%IazX%Z-5Rj5 zd4Fuwy8S*r=4(D#45n4&gT!viJS5?Ve{cisFkXSnZmvnVHXM2lmo_%6k1wE5GF=4^ zwZ955sP-pXKLS>Wd0%{3140ANiZO7Ua}f9ao zi>2l}^}45?VX1!Lyp{uLXytf9x0|jeOTDbT+Y<+bzxh#=495pn9VRXt(V zWv;3D!Au@PQ5kR>EKQ3ph7@x@q%!mTsI6~lDMA7uGJ(r{-8`^0^7DHvCF>rO$GrFz zanO@qQ2K6@+Qb&T+td*2DFXOuMpQoxzyf_sAZTGxh|V~YR+u16F$TR|&^209n^&Iv znt-znEzlaGq1XFIRO%I^+T+vH@Ao?@eE>RsFC_L8ca9+8EqIYg%4Z9q9mXJ|sN+(Q znV3+8?W~{&(4-tb$s&OGx;0>rump9JuPcvo)}b*zVTA)Cyl}JU#;2Xu&t1lzB`Vhf zN+$ulKSuVSp2m(=XpB#gYOq=((v)V?)X{jvc{LH$K8u|<2B$jh$ZCd8u0TTn0Om$K z^x3WE-FOimdh7$<8ZyW(@yaX6=#*C%JnIO@voI3&#@U+hWB>Cmd zhf)XQIS>ck#t{lp@9(4K#!pgyL6>u%69wnNr%#^_L`{DDT3Y(-c$5JQuSYjIrTqNN3tK_Pc8Jh3#{H);j9@>8TpkpI8k@H%s2bz2$zE zX@hA%NRO(GfW`X0$Pu`^O~Ov373NBn_HgBnKR%%r z=f7j7GPytHxmn(Qa0Z377|5RX0W`Wghjv|uWGFcCox7;XiHV7cG@u`<2$F1SvYE27 zv)2y6qi%C9T)R-xUyjD)<6`qioCg)Qk^ZQV_NjhAM`vAgXRejHN6*D{{1;?cvnSw@ zIDM=}Ew=ti$#ruw@MJEM!P3Ozu7+XhTdZ!FunIs8fAFk8?b`s$Pp5Wc2MSSrW9;f#)eMEp%^?nbQT2^rpWR;tkU-f~?TXX_P_Zf~TAO%g5@hg<+Dhbbj@KffmwO4U zdQF-e5sOznw3W**L?o>>=U_AOy1r3wx5@uVod z@rWA;`-0VMK$PR;c5s^NIN?X+BODuwDXp+u!wpIU+18_syev%O<6>QqUDV2FrO6pk}kxYuoZN@QhanGRfT_7m|ucN*jooCVhmmRf+OD%mA*7zgHa zf9$$fk(u19z{Qyo@x#i*P)k2~(8Q@mJ>$1$T`^ecc<3?F)Av*XmTx2g6?2b;3!of* z8eAhsr(iT7|JFrR*=uNwIP+vcdB}C(14jW39M)~+f$mG}R*b!9+0>L=Odvh(*Rn%1L$B%J%foyCdFFui z!`c;@X^3Ej7qnnVNWAtXhw@xc{PKMIqw6V(;*O3L`B0{582C|cYLjvT8Q6nM$b66F z54}Z1u{}EM#-qXaNel9bEqWe5GNb2wucW#KGit(iGw;URP1R9t3A0d}3Ej~z5;mW~ zmP)?TBjRH~k&?;h`_{NTx4T1#Gj$gtKUPBpY0&C%4c2;^V4oSwjh@xVA|lR3X`H6- z57x(IxjGx1*D?|`dLA(1m<(n38b5!Yyv*w?hGs%L=?QtQ{jpAjRcgw*y*~a4>+bBd zSunvbxDgXOX8^j66m(g)IJ(2RIOn;+QV${l**`)g4}F_}kR;wn2ZcUe*jaLk6+prr z(b?U-as28m4=w z)?wpRXpDqdLaXvcB#TzE?b^2XQC}ZoMTFWLa9g??nk9WK%oi%fa@c%Fl#HE$cA?!! znDD8+y*+Ntp0tAf_xWq>uO}Cu%-^1k5V821;vlSz#jI0mFQu`aUb3)1>*kF~C zsQr#P=w1UDVH+O)G1`T+1SU|KNS{GqpmS^K#q~_0l1sp@SahIUV?0iGz{RmBzV!tW zfWfm$zj?dO2g*NH%D92Ggl{;W9kg zfL$VzFoPPT4oTdi&yRc`ePze&zl$ua8EEtN1K|=B(Z& z;97tcM!?U;&&Od>Mz9+dqov(eD#xDV4>%6lxs4>)0qaZ2#@W^7ajW;x&&>mj;UA3> zWfLR{E$KJ39ef}dCc*#w^d()YJJnEcGz8(+>{!!iC~5E4RpOc}n&$XbRQk06BQ*n6 zGQp)*dQ%bhWHKb<>k2)FKILv1w_9!K1$Kd$WYs}`UC*s2f>H%PwL*c2#wY|Y>+ha! z(gpdZM_U5ZA?2U$yI=1St@u*FfzyV&%AHdlxOo>v-iAfL%i8`6k{SZY#Dnjgtd&D2fnFOaA132`%j(3<*$cyGN4}e6~(;rcJyQ+Nf@^K9* zvT#j5l4w70%Pg+-JS+3oM#nAILe~so=9+G!AIF^btj4ILK6%xK9?;)&jp=FL2*20S z3+(lb)StiuEUu;A!1jM=a{1=%Tb&#}g(w4`wm&4thO=GXizEp-U(lp*nQL)Y+rY-I zcs{my>viRbv93h=%?+mQCuqE)PAK`o+IZhd^r-LO7bEvQ`5|2U(?gkw80nFmD(LPT z-C^QZK1H;pY50jN16L8dc zXp*xJ<(Nea;TiS7_V;V*|aTU+GJFO0E=-rN1n8qm072Uq++nGM(gciJ1xn> z?~h$GnknNUm>RpTiuKH;v!_BLeH8BXE6bl^(->$EXlV&Z@R{=_QIM(W(G6ly#GG!M zv=i{KzZWCqwwW4rBn_0`nW(&WgYyRTQq@ z=SHLhyquJ{2A_}LnSMU=k{qvb`NgDwzwmk<81y*_GhM4_CMAnm$eh~WBKlsQisz%E z$SRX}xa*smrm!pQeV`Ao z)!iMTbT7mRqr|0H)P&q9ZyV0i@5$Qg@ulpu(yuvssEC)K$P0-jTD!`*@CWt zMqg%YNF>54_4Q^)tRaNqacd#x${HgCWTko#D*blM9>bgPTU#kPY}b$tAWaYD7*r{KTa zb5iUJf1y1KjLvkkd+EKnobsEd0)`kl4le-#=hrM{a7$jO=$2BX&8rQh>uCHUw|aUc zE|f=EBt?`_K7?Qfo-T@w_t1WtA3y#X&`qQij7OZnpC_rSw81Q8GIoC&9qE?IvX(jh z51-7MTpQl|)Tu@)QM<)Pj%JA&N}mk^V0b58LaFO+#6_UXz)P_^75!zY>AfFZAU3r_`NAPg zq&0|!q80L@K4X9`kY)(TTa#yxuC?BBn~;hTHLB)09EU$4RDNAlmhJs!@+*o21c zNEG$Lq~Q9vYNkNs;ZZT506jy~qg@hEN13H;G!!+DWMq_An#;Cao-$iY?T*c%^0*+; z&F=b9(+5-T9|aa=cHW3%VkfswS-Sv&>(Xi1@5Yz&k2Wm(h6VlG7#H}W|7P=8jk@yU zA1&Y@DWI4uy}d2I3k>db^md5SqCq~k-cGQdY}jJ}YQ_T+^u)ZzKq*d(WdD`61AZk* zrD3hB(;>gAAGm>cG9ys}OEbf01o5fegNg{ag&d9+5DjME(D;aGLtfqvB%s>BK>xHl z5d2CK>s?jgb*FpYi!PCr@9)zGJn6~eNmNf4uV68`L7WiKv(R4U9>Ott^q#4tXT<+r zgwJ~{S`>}jSYi?Q;_IY5R#{p;?l_}Kowt=^1la?V;vSgrFACKV=Ix`Mgq6z~oVM-R zjCxA0tVP-SnzXrvwvixPc5N7+@^X%VgJqa74dJy%9?Zs?d_~uJFzSexAxDg-xeX{s zx(m{jn&Rs+MBCwppEPUjIZ?1*-&Ski3u6|v#vuPl&2J@c^ntx%C?Yn{^bQh7(foMF zJIq(d@1rR+=WYu>J|Y!$DtdcVgZD)t+_MP-$8h#hu_Ve!nJ^eO3+l5Eep|7)W%b~v z#0})}4+Q?kCTxrK;(oOzXo}u%edp3qg00X^^#2o zj~clHN{{X*k=_oeE(r~BK`#wnM%%ej`YL@DTpYMpEwCQ)&G?>g!DelTLLMxqk*xX2 zn`CzG+v|K-<_%)3%0)RI|J`q?dg3VA4hH$=KR~n)RPw}wRa8_o2R8O3z&IKv9DRC& znFBG>^wP>oy!jNIMU&##^iy~0jg5>B7JJ~7Dvw~d^-fIi4eg_%p(O$d&k{zAQsi7e z93bTpwb)}lc0YiK4)I+;0>P4m+ajWOe*WRSF~XV;JmtgpI-|vfL{HT zfQT8n-@Jakl#m0it2uDo7fhZ9>Jj^V#XA>wu!&OeVeN7|`69IQ0j{b=IL!s_dg|6w zhaA5jZwTc8pgM3o2aZd2Q>}ijs%e?jV9v=^4gPKOuJ(xI6^Qh_M@luzKj9M-Czwxx z8>4=0{>c^iXtG*PGKFu*JH%IKc*kq_okPCl+g8E>h z=`kPb{5hN$HGhrY0DI84`H)P_DJo*l{gw8w!_UbAg;Y>8Va0bPMZ8I6i?Y`PcwX2- z*Cst{ z{WoU~?%@cN9w8RaR`v@ZBknh8n0S7SE2UoL4>z5t=}joN{lVpu)5V%2>B=yUDNNB~ zY|eGvrd51k2a+&6LbvwK&r_8#9YB${hX`cH%?w9K#S?yO8F{@~+f98j@AfFo_)(d# zfmVkTbw9yQhjNEN8}DtJC%XxhBc#*vP3Yblz;f-=c%Mn%ujldH?tNp?-Q~v4hIa$2 zFr?;eZIq4GkU1GS){%5)9CW|h-HdzcUFtr6qVPLOLvvO|@j2bs`?#6-U%9s-R(G|! z{F|9~jMT^7|4LdhchsC$&fx?)9SO``)D$YEuZ`j=H4T)Z{8?OQN zDX6%H^7la25F`$jrJmxO1>sMjh@)^D5edcO{4X_;vabIw#{4h;^?y|^3G=fYx>m0T zDYyqbJkK&RGfy4x{sv&UxXK9*X9t$xni34tEweK%32XI5Z73|nf}MMN=l`^ooi#NUb>9tL!i%t-OKQuzPWp;k~CCL+2U2~~g| zNOUa2$c%wGxpMjv*t9V#S0|ice##1qbS$j=vM|Y(K-wDvLW+J!Wo1ELjy0`@|0r#i zP1moNbb;rfWLD>rZ`UvG>Nsg;$WSa>RgLb{w#O~*>ei`G(1UEc$&?yX3E2d0=cwhY zj@(=E#`)cw3Z!`C)FW>dYyVvRYPF5~URmy%TYOhV_SYQy#NO5P$_(qYqFt?!O>8=2 z#ZnGU+|~TUD_*zN9U^Is{AIqv`f}nH7?h!<~GuoIcXR zd3*jX^XsC~qAq8v^OZ+KQG{`=3-(jTXDxKEHg1i#65i9hugFs4w}-5^by|RB$Nl_D z&*8-z;kt!KZ%0HY4h8D#CkM}t1EWcozSO%3CLg^EQ|uut-u~L^M!Ox8WS&4_K6GKT z6OTS6lJ2+juu{Jy#i6*v($3DKQFwCg>BYJGx`S@`z54RCP|lKR{tMC#?fLmL!DLs> z0d|k(iH$S9alRsZ>jPY>9^`x`%aDXS^(unPY0b{m&e9o!K2tO2y=1MSu$n3 zfz{%vg0*gU)q~uzvtVDEhb&fNBrf-59kq*75_C37Msp6EHCrnmo_9M_te?BYICYuR?qLk#pX4_xo&MVKiXHbr$N|bFAFj& zT=VE!(8Jao79Q@lR`td-a0-u%J6-^djHP$S@AXQYD4LG^PeamC$2MUt5b zEZId=A$iRLl9TUY8=ajlD`Ioo_74m=uT}l?a}v&LL`0IF_6C=u3q3v3gH#u=Q!fFH z7U8ystrGb9wX~z7W6N6TG^0jI(L_mN&HXSd7XH2m{}BZ2skdZ{Tu)^d7Jgnj1OaD# zG1O=Txo5{)e9oDV8$GrE*&3)CxUoV6Uw+(z%`zGgzLvAf9d|XJQ+ge5$Aa(lQi3m# zqY@7%0oNL@DEpraj1Tlz#`S;@&;+WzakrKs%--w7Q?#HvSxNO$Kt^VD?e|=Ye3Au4 z^1ZL(x7M7?fPH=ng}i0x(;2^?Q<(doOU;n&Ajzaclz(k1sv!4W%;I^nYxwT{`zYY; z%8^6sgtzYExBN?pK}Q=hKU_i;Z4Zodd%>`EhVy?)I&?`W@VY{xWu89$mQHUK3`WU~ zUlo@jSe&|-56*WgU~8|EFitGBPxE9j;cw7@_(M37>8JJpQS}`X5=KPl&{>BQt^4}= z|C~;FtD43L@z}p!|C>_e|DeF|Xc{|Ll+B&^)t@m-C+npil$ZQ31sdW*gu8m3EFB;G z_D-Faqm6Fj@QQzsmDPWX2`6r<30L_Pgew2O$xAAY3dzxaHQlmiu3;u7E7E3hPb(Z9 zJ14rI1R#6U^vo-UrPSZIzD3*jab)H5{KS)-nF-ygCmP?n*(Q_jCxs_}YmMKMji@B# zJgldBjQqK~yL;RE*V2cj&{ns;vyP{G?LDq4EZj;tCE?Kw8;e=u&3$`QyW@Yx-!!>Fe_qbz%^%48<(CDELggNDs)4OWr@$?kfoX*JKx z?1U&astPFCZdo(9U6d(IjTJLr@fBs8#nWb)^q6BF_a4_zW>5IFJa&|$6>t(&y=wh= zB0xM%C$+9#y!y#aULrN@KN=Fp{G}@9X5Wh`^*aRj?)&?SF79j;w)Y(r-D`fWfe{gQ zSRfty9*J{(W2}Pes0xduwR7*DE{R&~`5AAFP^&(jpN&9C?GMk5ZB_;bs9`Y+ zv4jx&4#=j4%AL6G4^*#JgjMbBk za7*dA<09oOA*pr#N9@tSa82NWPVmk&^1Ekwej+7EuM-kTrU|xXlTJm9}5X zduuFPH2iHtdmrXP3w17~?C~daiKbf5#4?=2-_e&JKb0`W{IBPlFiG2tv+E=(geN|~ zRiC44(?5SpZ(N#f_m@lYa$uNCmTEKO;*N&>HCLs5dh8HeAe9a=kHULKTxwIvuK%(+W*61nf*6 z$08TjM3m|45s`D;x##hAaXgJ|T%C2?zZZ(0&mlk7FY7xdM3A_o-eJtdb^HDd<+D;c zh|{r+gkt_meJNz1@8IE4Bi3E&CtnDpOmQrRao@qlP zAME{7uhPH@C55C$Lv2XxCXn<;%=hn~YD`sWrCGI%? z_pg)pg0q4%)F6w_G=$`zi+UdtGP-ec;{JAIbGA{V^jkvezu(-4t_a#alTx#aF8W9f zCs~6k#WYdR3#dIyySloz6nzE3TmmFIPEJp?a{hho85Q6=d>YkChn+oyJ#1!H4nNB0 z)UTdzIg*3WQ^`V|5j@XC9N|>hEW*Mf4-UC3us@ByWDp$Z`e%jC`_nTpNQAw6m>i-^ zEp3fr^R7qev~lB23)qknL*B+4O7U^ib0A`97K4P~^3U*xt`nR8J_kjTz8yw)H|nCd z4!PsOQ)?3o`5jioPyz9QeuLW%EA;b@naN=UA#BJioYz!jooN5Pi!$mD#tRp8S=dtN z4a|5@Q|&C(ged2Mq(K;5V?F=#oUkMys#m6QIp_I2 z0Qrx}%(KC4r;CDYk-sA_{sB|%eq(DXoolxF-`k;MZ~%z~PY?>dfBp9DO8pMhccNlq zvcR<>8E$^lc3x+y>*ZLjnotUzeoIKedPU1;doSuPk{MV1?PukZR>I zF8*|Km<#F=s;6OZ0DD60_f)%PlnxY#Oq6cRLPjAtpeh0gIWZCCdM)gdMlp;)Yisov65P77$qW#VghL34dC7{q45(2Sb~eGfgH%cIOwJ)~&m6JvHbz{M{T3i@|X znh=dcatWN0r3g3XW*f-341;6``t7_-Ksxq05f+OX>K#7GcpSa|uzkLb;{hZMa=p~_UHzv4!Cz!!JidfISp96Yx2CVv0^y=V)? zaO>1``q5$?jkjggud3_VnaO!n-`=9Itsk72(6DXX;+?FrjMjV41tC*?_fqu}?%;)D zfyO1V#|8cRYyvE@rA!)FU#VG{SNGeG`QMRpYTXA4B6 z4z=m&>E5*;l}AA|M)u{_dWaslBy+y_&Q9j)2h1q%k^6a54KCK$_ETov$bFdj<`8f? zHgDdG9+<|pyP+g{-c3-9r2Q4=sw&jMkyBtR>Ut(FrT?!9Q}`8O4MYzSOqJ@OR_r6< zsnCK+TyI)93WfRRR4`n9G2bFGe^G~h{0OvmpYaSZF5X}gE90jUgOsXKDu?cACLp{F55D2z(lh&qy+^c7V0A=4`Cm>J-#wqs(4sGAsDAw9| zSpjzuIaT2i9U`hCQ@FZp64^VkOER6QviW?O7uqF@`|%f^>)%vK#@>A{x4MQE?Cj%e zlgc}hZjQ1B_|<|O62d}4I*8`oQ_u!3&(u3P9&Kt}cp;8$^E?yJslh!ra`*_k@U5J( z!G>AoZxa9b2QRnu z$GVr-3b=?f#~wRzNeNUTCS@NIl(2<=y=P)H08rwIf=D1YZYIG_0W>Rt}lGD z@XGa7*fuvXFzUgKEDOXpoKWiq-KoP_IRgBT!Wo)aV6h6a&`a+Ta(??Z$JURCRYSQ~ zrk@jCRKGbVyAz^s(pzb`$pjlCSq&2EH{(96fAsX*>>c%f^i8Klo95{X*Ycbw_Tli) zV!!41vA2gt*e>D|p`;&nUMJ0xWd3UmIik?L6X7u)6||R?nCrIjR~AeEjW{9&=fYvh zk6UaS&PBy;Bm@NNS>4pD!N+fHj{+&h1AD+Mg$0lXy-q@UO#J+sXNMaaMV9Yo7+BYu zBmz-o(_76`W@#9ot|k2Xq1#NV5q~zemxf=7BBhvZp}VTuyXMIMyJ<$qp&3nbXMId8 zwK*dJO$7E6F<(6wX|$T4C;}d)cWI^J-i?1=cGBoZs4}cIh~6b9`uF^NtGgyravJ5~ zt7#t7}S6Ne1?}pQ5`!7nP@n8#9w9nEU~ifMe+Id-mKzqxt;T}%hgdaEfBGg4pUAEzb3Di z4IYjaP2oW(*7NSM3$~tnX&Q`vpon9m+S9WgBpU4N8@@&b>Gz)?9ev@lKKhhxn;Y;0 zDHPAYKJO(7Jf_yP3JMDwM^kzF^r@f;`3?=p7b-{U*GmEvOJ!&^i^ z)u`~GANFmFt#1l&p1CqpcGRB_M8yezD;3$L^apuk^PE=%My>?YXm_V9~jk3B)lx;MBUX(R_L)bJaSIkqZ za;FBtVf;LqBkbyW(EA|+bAnG)!t1N5Ys6EH8pQd^X7$(5TsDL0Ur{oFx|q{9Bi8ey$`m}C0C(NojR*x1uj-7^oK z4#R^On5L}eN-2T!(>XwEtJ+&A)UBNYBgwhn90@`4HcK{c=6BRsY`XW2;q@ zowPJ7w=bC^XTnf^&j1NEbY607dyCP@@U za`71zoXdniK;)%r8@wY2Jg2}w^jI1g(caL#c%VQ-Osn3U6bcIJzL1dDF<&pN_(~H& zJVDl;<~h91f^93W0Q|MN(S_d1WrVpk@A{qx5dfYftFXCm4jIv$K ze{|JXOo6g!cSPV1i)Y?>{Ph#de~ZZQ$P0J?Sd_%~HD@vgt==YF&;aT^;3_e`t~c3b z=^a@B9fAC&;bHINGLtfjFuuva)A_BV7xiSmNg72Ds#{B_4UD&{6*wCIs-(VaY*MX>G)TgAi) z)^3H&b?-Hepvr1VHm)pCYFVmR#!|E3;Tfxd62;rzCgt>5oa>2-{e9XA_-zqhv! zQY6{JkUzEZq9Y$=FJ`)^L1A{~6=G_CrYuIJi0EIeHbGvhC#k;qc`27Ze%wowkvBy|HFuPSK*w=)QQT)dDkNa7pbaHTS3s; zK+c;&_Fd1zy1!WXZFl7d-do|tN3QvHN{kY&1JrWgN?TrXHby9GE+%f=32kE@F%Vo} z%|i0%;6_Rkokyzw#$_BVWk7Ib?b?On%5X>W<8t;b^;ef(-q+CuN0SFD8My<^drm`Zv&x@fc5 zQ}T(3;W)3%Jc@`z`QjYo9D}$=I^_~~vDWg{g4c5=5H%sD{4<4o?(eNsSM0-xC45=^ zZ{oM%>8O#dhBO^y|LfwTQz7?qB?R)7OEi#UrX z{v1B|*v!cSP!nn?EQW!=T}e@3e}?YoH#D=y#c-V_C3 zDz{^cth{Q$P0gH7%EZl+SR1wpn0m7%ezAfm=nsvOt-LTXF;_U{B)-2R7q8co_(J&J z4j{Yd!JD2wx}i(o7@MffMU~7rCWpihRe_M6EG-M()=szih}Oo%Ed=XLOibzA|F!K} z?gY8)1zz_|w8MQsGn8qlCc%Xif`6zZ14n*=aKK=satyxg#M_5_7o67(Om9PgD$Ggq zun|i9cK^4aN%(S<{VfZ!KT{MlTdt`BSc{Zz_o(OnXeBbc5r0VjfR{;%6&b_%a+yCv zyI)JjB?^tZSC z#XzidY)%BOE+{E~D-N)B#aaP)OLJy?-AM&>O?RW={#|8US~G9KA?NhL7l;TSy(4)4!;b zcaIS=lt=o_v{B|?JkwzF{&M}}Q&NRhSXP2&_7_!74yE{+ItoGhwDnIdHS`^6?2=Zg zFnQ)WA+jIcu8Nr}EJL-2zi9xse&KCL@DsN*T3+h~SUnQ*O&oQLYM}}YZ~Cc6`g>(+ zJzp&}8@WYaR-C^JX@8{u1oOOAIPP-IeX-iowmn3fCgAcx(-h58T)%qeuL@j#2iFo5lwygc1I0>e-%s4=Xa2s)Q}VuiG>i7@%jX&>Cr ztcx!>7~z8iHkDQt8x@DHx-&0v&50@fNa1#6{kL`kt;%EX%0otWED>*GXM9DXGCmVO z@x$;j^eIzZQXuCgA1S47D`(i`t9G4nBzUHKFWkw(Gs@+86V7Y9OFT8{h5_T#`;_d{ ziDl4jr&*OX6R34Ap~eTa6%hj-{I|N(+9M%hY-{@?|9#@ZMIW#_I3oVL7Q)Hc8!^?oLY(Q8x1crDt!f}XEk zm$3ww$a?={Og2req5T=QUXRQ5Ol-mXB9)l}N%u7ajJPs3$yLF`H5`R5;|bc9R+|d% z+oao^t8?Ja;hp8auLlsQD#6);reRTrTO&zGn90(F&gV(Vo@NWxbjGb+j-zJIe zNL(FWH7j&(?-C>)SQ{?({HdnP95|CFlBfKUaPz*u7}8wVuw!rSwzX*vvokEB-RAF%zlJ~4A)!C zI4=Rtdm9KGh8Qqq>}0?}cJ&Vl$+&R=UO2SAv;^BtG)kEqlsMefoPT*S5R&(ByA%{Y zU19jcI-H;U&Dh|Bl#z9m0G}PFBsB;YY; zrsQIIiJVXCUypCp`-~Vq)zQ2>rL?wk_$O`xg=y2UsJ!2@|M^NB`gBn~6~FCi1Tk7n zMgYi)R$a#-7=j0c6Dns*RpkJ@pQy!tBk<~ERU4qwnBTBxe~T{P=er8@7GBUt7gWJ; z)|*bZ0A%0>53iic~{Q#sVZz{7)36=W1Eswwk;Iu6>*112qT+hPn3*A7?nIJf)6zi~xoE+kVnqC+S<@mBJ zXwf!kar5Ai=z{U`?$Xf-BFaDJ6dp+sg6PAi-%*+TRAeOd%N#X9J8HQ zGEJBG&N*H?DC%Mt&Fe>=&;U#N%mqPnwwJ)L>)rjsa59K5;x3GUKcP_co4d0JsN(h! za-{oBm`tLH`c7Ar7wze@ydE}n4^-!p&Jb<`z5XbU%Qh0cy0L-0o|i;sTH_pB`&ID% zjw6iwE4NEDnBR*lmK~eTc;(7wnuhH=%t&Q9zk-Ab@eMPZlOqL-TiZXGNDv|fhZ5L} zKSUbs2{48D`@i!5dVH?MBnmd`<5=-_G#h-Pa-I=0arjlbDtXDOCYDG`aCcDuK??2o z=;U+7Oy$weCT>q$&5HmxKxo;HjGTlM zPeizA6zU_>tEU|wRV%D4(b2Q!D^?F>KJinlzsHw7r9-@8T5^(o@n~1fymh375X;8z z76GGz{@RjE=(JxajoP@G@kk-Lne%EtT#u7 z`MeH$D}%SqWtG?UCXlF_LdeA4vQ_SA=(`-c^?9bsbd^PZB7(|%THP1B zJ*9bDxBG|yxYL_Xmipkn?`TOA*rzkrrpP^64cE;DeItS$_)jUU;jhUujp#CUwse%5JWHkou0x2xKW32uf!(zB zd=Qs0ob4fPj?KgxEj;>fxD44a8g)T=P%IH}Vp-)y78@hZ2?wWu4XvkU;`7N2Jd=YN zler`gn|QH360qWz1$(F&)N4ku>@VlHmWRnt2Dyd-Gs`RfoN}FKx$bHs*c8Ty`|#Pr z%lPC53po-DWyWkg+?N!}K@XJNyKpWe1t=hI_M=1Eyf|Z-KmQp%fC+!|uHpX_0p|Vh zK$xwgVB~#`s#0$bznFBMWsCFvq3rd*a5i)yQ7u6s9Jv#>=9w*hxmSo~f-6)hdY0 zxW6zjRWqF9yNsT(MTm-AWV|{k&Vb?$K^@ zfmOX838$c3-89C>toIy+J;0dg@BLo?+RZ0zOPhUmh!yXT|a9=GKX<4I-P6~ zr95l>#}gWnE05~1_Zi>9zgKI$aE=lX+Fn+DzMZ5s>xEJ;k82xi#KPx(K8i@*IJUHc z=L^iWIs7qV2Wqo|hv)ML6<64F&mevW(Fo3#X(oK|D>wf?m5OJ*m zOKYWGLh`6sa3GVF3#uPK`3ReW9giZ`zEWqxsHa}2yiUT`lq~QV7OQzeuK6AuipHF}r zK}kfp0Knx88r9hgF{g87>fz&PNq&f#xZ)Ptv!xVSPi=~sskL7Ug%|4T%__bcN0QC- zCdsQY!9nMYS?v&(YRC34TA3R-k2hkmRR_!1zPeUWl5iD&N%VHHqQ)L)TolvQ;5cI; zS#FZ~{tLbve8h<$FmU4_@O0UV>T+f9T5+glCCL*~pL??%oNi5Mil&HcncKrTJNUy1 z`@*ee0P1zfY2Oq2?|oV72v!332z!@ZTo>^%qv(q-7&sp1Tp(3QvNF>aAi!50-fMs& zgk7$PlNy zveP(bQ@DkMp8dt(<1)2%n=6qb1PgUAw*Llx{tvIASt6-uHD18U@qp`~&3fh3F4y0_ zKK~tMN4aEk-7a(yK?g2*05o!W)~OR#etCTSJoeOHA?0Jk7R+#Q14uUIW#b~*A17)y z&Zv$}Cjh&0ku9Xe%Sip<=U6hpObu$keG=dNU;uZ(emqw#16Q3CZ+Q+9u>%KPFH`$S%ki@9v7W&e* zm`Ald#g6T4PHsuC;UtzJ;Y?Tcja4c1Y<;y&w8cW=9}^BF9GT$L7eKBY310E1@G1y0Q*}SPa@7OZC*OWZH z2Yv~|DplF&YZuxG(8=9Wnd^3@wMuwdt9yccBk_tU=hETrF-xkExsZ#1S2v}=+|ra8 zA0;3qjcem>6Di%kNp(4S-}<>mI$rX zg_oGN(qLC?slro1A3WJG`D|3E1}SQrc>Q?-$m#OK<28jjGORms=I_0)ely8sTcaM2 zi$^qAK`t0j-_nthk*;P=cQ#(4DBF!*6izkVl-ydh%ww;tv~<5s*Vd;aD{$eUd_Z|y z3$cHKuA5N0e11K#dW?xDkdS~U2ckFHw-o&XCT|4B zJl%OW4Rji^Z9G4&u#vB?kk)*u#rIzf%gNa(%8gN??LC!PyIT(v>gGkK7Pq(cUIuW3 zOYCgz!ymkFi<3f6iN$?b`(Dliq8h?yKwUQR%U#+k=CMec-%Sc6D==mmQOa(!AgP&% z)#Q7TFekWZ*NGu>;@29P0w-~!?Mp&mhi zD^aGE1VC$a;EhE|_5$Fuh(OG6&Cnafxfk`920Bl&Y0nvL42N^4Q0j8)9n`ybG7_TS zizq^;PbW|!xr$9+GT#TfAO)Bq&oi2bF&&`lE%NyVQHk8(lCVU)qYJd6iSZw85fI;Z z8et#-)wlny-mXd}sz#=i&_smoKH~*tumo;2nxN>+xzkcjw< znUt)SsPH5lH?O}lUl{=awa`ACDBtfp{_+x;=b`%!0~*A12Hw+9M2h1yI?W?1<%qqv zlZa5Fod8xQlg8(P_Bm=4(>Nm$`%cfdwXMuoN`rx=9WB=7Zf^RSj{!emg5MJ|Z~e{q zb515HbmYhl)!?DuY2v9m0RvXR6x!}-9PlTp%I=r~Mq8rg`8v)p#G1m21{5$VcJws9 z=&C~p_c*c{l&aC07XMy@#NEkb1kb~^?0e6Xlk2#GA3H$iq6{n>bSy@H9 zC$N|^GAl2bMPHydj+M$z3xQ~SPGpcb^F>8tWga$5#V-c>^E^HFY9%jb{}%p};Tp)8 zny4!7?bhAjshsl6juLTv+-r}5sbD{usjc1imTUyW|CUC5(j$Er5oseLXp&ebqV!2~ zj^%gh4Q*q1=ThJqSz<}|&23FU)HMaV@8R*>eh4))bIK_ul=J9gpK7SF^?HaNzh!6` zfmx)dsk$eZKG}sUa^$xb+8vwp0PFN+=OHe0P$t*FN{7&XyXkf|m6~#`UL!F-J-1(r zY-VS!W=&{X%hH7pl4sNr@$6CW_Ac;4@7bR(VV$uY*(J$`mHUgKPG>y3J|INBZJe(s ze6-|zb3^JgC{XMK&c1-v%dqAody^$wV=UyE+p?@^uJp07FM~D-*p-I{hoArUH}ZXT z+WMh-v0Iq8G>C9W=IXx;L2~lANrS?5D+>v<--=}6_SXeUCD=(ErV(8z;siD5 z-F`K(S4TaqGZJ#h5omUm-DG`M3|vYXY_(Rdi|_rbZy`eUDE|CmJiYG}%QvCiFIfm@ zQ2{@X^w6m{hhprv1cZ9FLQ6EHv4j#3Skhkvoz=zJjT^`waqdr$e zPwo&e?~MoSSi!>q>buI`;3F>U1roFI^f&^37Xc#AJ<<;JPYwOWajZ)e!vvFJW{#Uu zCsm9CE_*zwO*k%t?+_XhqxlZ=?!7q4ZC;|?V~`__+zIdsNL1`jKt9H6x zs%1r2{ccnb$>ax@rAqumDf+?DrX{?q6;^3woIhw_b zKD)FA)^)(WwhDZ=Mt9R&%^VX_@G0ntAzW)Y-a z#cn3u0W;mEaTjmM&_pzr0$^+QQCq1%ZMFzbwFb#}Rxfo~dy7=4!j`G^W!@Zg z7qctDVDy~6EkHBksUF{V;x-ceZe<-k!SFP(P(KZl+YEWAWi?3N+2PDoWVUm=HGykb z*q~Mk{d=EC66PAuQ>XT*hq*u;ZZv|0p(JQ!DH^kp%CLW9QDPi1Zianu2=@>9StC0s zR+lbogTtjSlg?`$BPv#SJA(OK^E%a&Fhd$n2PuY5T?XnIeZ5X)a zgZ`@?Pw4}&WKkjC2KAU@>?iT`rq}}~gGJTc)A%-0k)BsLE|H+5LVQ}k`ccP^n;pp5 z>Yz(MJAMW5g$7ZHKb^l-_%okMd&;V7QV@a z`Rd(?r~wXI#7L0glY-D}R~*&2HNU5V-5~~6&KPvj7TAu}AZuqL)Ke1=2{XwIuqOd{ zOrbxTuuOF7jbHrt@84F(?OMVcxChjX!FvFwaE$??jLl|=!(zR&!@W$~u0u5_x2pXj z0ysFK_{NX|#9tHx(8=`HinLU>TT>gw6xw3;$RhDk_+xQF=#lUf`rlU^aERfidP8N2 zD9miUPKU5^o!NUn{E!gT;*4f)ZkSK@u|ExyPVlIX7+?#si{?b_CVpA&bnkV-qUhmb zIb@onp14Zpf89eAgAB^Y%W2jkH_E~kIElm@_2fY2PZkQ z)hJMn9SU*9g)8t6nt!qin?HQ~c<^RkV&HHB2TrnB57g_sX%wJIi`ScETo`dNF!!o7 z&vD|TtowtC0K<>N^pazN3bY^yy_~5rfO`q&Cp^us*!^H{RQgnZQ2mlAaBvlB9_w<#e@Lh7~)g3qlbR{d2FCv|1)6Z z#R!b35YN5ye zRq{df+>>z(PP@ zEm;)ppOi61-e^(oolR(6|D6i6L)@6jis8W)dlnQEN!`v38_x25X76P*a;n{SAG0E0 zN?(nbj)UdkBqtvyHEjF&#g}!70Z#OI(`@?6%Vk!6=@WpaH&TDpf|TYTjy+{i%Tcmp zK!M0vEIV|xL#B5D{TtWcVqypW4Gqa`oH65ov8Q5*V8g3xN~wcJumAl=%XuyWjKzC0 zzUA^6I#Av$jrqEy$Q3Zakj= zZkZJhz~}i0fH(%2-Q1F#6aAOHb>5`y&E3BgXKA5f1}mydVE#9B_8~h>qLs6p4y7!3Fr?Saxj5puHfREu zQ`!#zQc=?t<{tR7R;SnEH@4{z{}~Rzt)JSo^u{Z{Qj>0%Q{)@*$e+Uyelg+TsU`du z1Pqy6IGC^}m4s%aF*Yz%u*>KMY^_r;FGBHQgzW@!D2m2x9+*#RMx7ZYtgV1yAEXD9 zCS?gGW7}~ML*TFESEM+sHVbNlfsC`^^<*f>-xdQuGD@TH!+V-MNTsqJHVcLO%eXtQ z?@pkpE2^E}`POoQ$Ub&C2q#y-*7L=1_$FSr8X0$8Siv+~Z@Yk{R)U*=%Pa(Y0AQ3a zvS_uhnm!|VBd$efHA(5=Eaff)C?9D#HtR12VtGT9ptpxc(gxr3pFt%0YYA~%eKx_N zWg%`F#Z0v2(d3sErbn!AS3d^MTpJ1$6An^aUM>&BYY*LPE9*MV!TUg+$8x4AXYW?Y zNZJk(mGWXj$|KBn_PZ*E z@hvSSSBPISD!Yb>O(<^9(c>qL$9*c;^LL$gFQUg$lf2`=t=0CDYeU0lo@f-*427g* zvy;i))*`oFfyX3nmvG2WP6|X#Y9!o8)MCK*-x{mEG03IJr?8-+#Va>W`}&QwX$5Ck zb!;_St{q+>%D~D{z`+^)KS&lpM1&$at7E=eSoeDE5$}f^>;IDV&L|xfv>B6^nX#kG12-?TO2{lhfuW*#~vivL&2^1e(GZPu5Cd^F}ukG|?e#@K)bM@_e z{+uFNFzG(+WR(R-sOrO?e_;bZXf!!sV)uYJq{Qpwg^7uQ1?}4dSEcU|x$IddYdriV zd$}&7oMviZJC9FVK{$bdw5@}oA01GEpNY3pzc3+Mz|8KZ-;aZ`DO2qicJFA?Ps3(P z&9GQC07i8GXLP(Kgbnjc(y`}FA0YZkde%JVxsFGzdyap1ldjv^safS@?-lc)wcg&0 zZv1?`X3O^vmfXyqZG@{j>bN&|wP-7RC~d1^vF(Zn>k$srIzQuMO~6h%@(~HC*e+rU z2y3%S!WbBfTu+j$4wP#0YDk^ncDz%_l$+V@=Q=WWOwG?X*=LO}N~+Df=wXyo4T9fk z#STR!oIY%I8Cx~KJ>{P7@CT6QJr{#)Hwb^eG$4iwjtcUYx%%d?GXhX!~>a_ zD)#*1(;-M5FL=Pd>hn}SuCBs=>LF3IwDfK(^b^9>>nhNGKPD?m#QB=JwRF?4)zPvr z6qfLcYFnz=)Z|yEg*Kb5u>Jm`OPf!6Ce={ua5ppw!Q`mVWtxNT>%_~yNcLgY?w%hX z7u{)OCW@&tCMFG@K8UA{L}lc09^M@<@^e}p+}NpW>%DfVaE~MdYGP=YRd4AuePQZT z`$ep87q@G<&Hb8X2*RO_RKm9cd7FVh^Kwgzm$(nDhN8Y6;G7*!v1?azbmgAo5p|8~ ztWKdFW*{h$dGAy9brrn`>KC4&NAm6Prze*sfXIrEOJdx?T3-lpSCg6+5hj)y8>Bm{ zn?33yq=rSR^1)ZHxRszCp=Kq-#HzXua{hE{=Ty99==J<(57mPW&8xjd1W*R(CiE9) z9lMWgHKOfOtB1eMKOR!n@EK9OSf^5!5ksLlfy+EJxnCfUj<2lxX{TLxIIuj-onUtA z-t_aC(x4qo60k`BAOA#$%t<~8mvgQxfmf+ssac+orro32gjiBhU(abmYI6HP2`Qet zdrNzyOJTD|q!NtJ48rR(s=|5t5CX`kZ90dT3NEZwENMh`CA`OV)4Z`;{V1VudBQ8> z5^j(8szJ+*MHOoQ0v!LK4KCr}=ubMczN{NC`L4j{=;}B-@?}&oe8i@M||Fk_VER|1Q^>+Rg^l|HO9BVFz6W_r#dC z`Gbu3d}YB5EwQJa#{=UQ{dksXMn#Ld4aD3;UlSW&kT6|HRlp2G!hPpA@Vks-DW0L* ziObtL`8vRHvCNF;mzXY>0sh@P(S1U^>|Qk^vM-pocHpGPFAu>#XHU2C2>(rFaA7M9 zq^?v1W&wsh6@CnbjL~n_lG9D{ir%M&m+h^t=)oG@Ip?C{f>St@Y9d(y<|KMpb4lco z(R4XB;rRnBvNH8Bk_lgfZ4#NKE=@223$u!$jLbCTHSuWe7cFV{$w)r0_f};6wJ@#k zIc}|ScuQt+j@urs?n~NM{$M<%M`(SUR`<{h@Gt5K7Ql>I)~rKotJDQnaKg_z{NFjZ zHFvCZd6;aT6HHc1w=olZzB)c#uAS0q-Sx>tD1x~*oSe@>ty}o^rSiQbzAV<-CMBwV zQb_CS>PV}l@ zQFbe(vdlxFkwB1MUonMJ7f+`==L8w{s+z&*G90MXP>W9Q8DdWp38-;Fxw@U1LYJHz zCunA|fEZl^QLc}g%5}v`vSiP9tl`6i(6)sA*{4o||xg7gjZAMOy7iv(es1U1TwYW*n8bKi8ONcAa0oweW#=$As`# z!wUGZSW8F@ys9-r-nenD3eHpP3JuqDArAE~jSQW2liBaa?s_LTuSTs=P&Z!MqsX4+7@JQ3 zF-((NMtqW=eI%dK6Ud0>tDl^m$PvQ(+)2~|zjmh1b8O?G_;!~`K~bx&yw*ydHs&w} z*+_~q3_Z~$2-3u@2MLY`1)vve7~!UOk?Soz6b<_iQ&}+JPBTml{$j6nF1bAXA;{^_ zljHBegYKsQ?mNYF!&~Q98aI2e5SNHF?;XO7gzl++oLwnbvG%H&m;?wA!J7DVa7jT{ z^{M}{dgEz@!p~hA3&zXT(wRIw(wFfmPENmOPHqZQEE{ zCuTg~D@_?`0W~Bh+$<601N4>6VrdLx8T{%Y$hc&;D^4+QdMXgAwn;&SngMzO!<$4! zK5oOEeOZ=OP-vVrWQ@$~SCyo)03sy$0ukCy(p+|{OfZMsaY8+RU}I=k4B0 z4N{q3XR_3YC7nMX^Mit3)?O+>sG6GbD= z8hgL29zV6@)ifcMI%)YndNvqS{}|a;jsyO(IMKnh^a;K>5yS|^3Q|mvVEW_VS02=O zI{5qjarD3+_^4DekDjW|I4dj10}c3Fg&FR~*&N}KVD*#3k^%7@K5ZdKvuGK7KSRV} zVW2AD+!>j?twe5BKsS5Bj-M&(XocpuY&KcQCqg}~il_0ZO`Xf`HW_0~ONKa&?@JfK z`?v!BO%nsB+lrWwASeWr%S^&)hT=+8Wi}a`6h|pH3tehI#=mef%O;mo;ulJruC*o| zd*dn8w&eO+3FM(F6LK+n9fXi=Ppcb+#q=b`d1o6YAJ30n%kCEZn9kB%r;AK5mRw-q zRs31R)4{61RmCn71CLUmm}OcZ$WH{CyFXsq7IOF?I|Zu&&5VG>1~7oO$CiTF*p%F^ zrDdEIP`%qqF>^QFTXWY`i|NjyLi3H%9+w+RxA+R8o z#X@Ti7Ygm@8)gJZ;V$MSHt6+dy~JLc+H?8(2?rM^zoPN%#i{5e2}q<5!a1>YQA{ks z161bS03d(a8kJ2EGWrj1Y$syIew^rut!cqYc0AwYpmXY?1BtJk__6L%p(_5 zY|fi*@#PkGD~qy@__z|ch+@g@9j8x4ngBe)o#jwc9rkF})L;%F$KU98bkT%L&HHPXRTI6IG5da$eNDETf$Q3T-49hbbIL;-mjq2IIFi3UQL@FMtDr3QAwVgS zh%Yg0ds#=aR6ZG=t<|by+PCcc*UI-1fy-p%GewH?<@@JFfN^=bXkHL6) zwdo2ZU*^T&t1_uvlfW?RhBdEc?_JBWAXgbK69N|VVD#ots;Id}Ew$+G_K>Lj*2LI= z@kS#N+dCGQWbM{J zwM8xc(A2E4z8e8)*dAyL!i}Wk2@3a}+B1 z=TEn;tbJC`A_DI9n(XSN@6C2)_Lr(tmz#Kh5s%K7S5Q+3F6UEIE=iJ83v>;W?pRC| znpnXr7reh;|4A)!gSmq>z#t0M={oV_wQe(C!O5dci|C#xT~&}lV_oXsJwuEqad?R) zpZ=T}z5A?pQlr;HpSjQ00<7~7Znt3WmC2e)E@<(f-ZHH;zlrpFUBUQ7Dlf$XExvVr zawb~&t}M?vvHRyl6%8(-mV!Kis}cD7iBoM#NUnM8IZi)?K-{fj%a6dj33qNKRoh3grg%uW{4=Hcu!tEO}NfZ$Sr(!VZV|Zz; zJ)6Zm*?pJlK+&``V=~^`jNIt&Kl6pvn{;ky7yZnsK(u-J;5g*#)0J^3Wgd&~m&Xc8 z`Jg#ntyK&udj@T?+Wc+W5y?GK_u=u2@fEs#t`}WvuWSH^IQB<_c6{~+S%41>%9#{f z(DSB=x2d{syUvKH#^Lv>QldP@82LnEH6qA2ElFw*bM2-GiC{PxZ^zbX3W7OKzI7pC z-5)jw%(t#HnYhw)wL$?ARRwl4(z>qdCyKKooPiHFwW~H8UI&ZCsowPlRgf~GnF`-H zwoHt~jOL5%Xo^0X-*Fj-e5zI{!=IZfQWgd^?95pto`5U9b05OrIy`_1VL@KkqhGV7 zE;V(>z_kiW;CKK%yG2HD-HgHNWd9uBTvhHh5QLPjhm~|_bTK)ySY4Ey*H7Pm*jn$< z!iVG63suOP-+(Tr4ktv@F*8eZHtzwD&-ysHRC2>+ekLG*K<@lW)31%lE zMNZhGnz)kWh*mVIz8*jE3LIGsfjpleN41cJ;m9vHXORKa&LY$@)}0T_*jH3yRH%)_ zk}!njw+M<^$pC7QbZ=@20V*nn_E+x15wF)s!UPB5wV$6vo^{y`G^)7RM{T}y=6`~} zB>ZN%J|eKp0j zh2}C2H5qYQEZCb#d4jxOCyy2#nIbIh+_@E8PaGrx(1%zd+_&D3`uC=bz9TiCg}9q5 z#cgZ`udjd)3@N2J(H^#nK{nC^ug?tY7x8eADWwzfaaqXN;b zVt7X{u87zw+hdB?BbIqpNtV`f_SQ}>9RrT0Gd~I2zhI`{J#sP`_Eu^$I&9|i;Xe_M z##sUluW@h~Z{eHDNxN27RZm_$f(m+a8o&*7`vL4B$~S;bJm!>4OC}P#1w?y>H@j@W zZ@AWS?5MwwZKCM!mqt^h$5h3OY@x%o1lo&Xeg67#<;_7B!ICJF4k##$KH;;r$dFXT zNSx|Z+jI&?>`}DetddIbE@G#z7UG%rfk0WpFz2zQ+$A83EU)3oelh5DpvSYueMSNZ zX30)XSCLz8AWN%u%Xk1K-G5<5C0#e z3`;zpCE2w(9Qqus(H^`P@MbzXtut#i{lv>j)3B*E9a1{RR?t^IW18k#@E3Y$-1w$+Krdm`xF+&GfdjRMyZ)mF4Ge{D#KfB4i z-d?(KkriUGqz2V<rH+!^^A~vEUSh}sIoTIq+gE%gqYh%ya!q!2)!fQDy&!uk!6_{xs z(iJ+2T<{wiP_=@x1|20*f_o|~KQ^ye3%jb-C8{4Q&VIP5F%@3Y#`_XtabE^>IuwX1 zAK8)e{>IDno9zGUpZBHL1--P)Bu%DJBa!uR2$m(c|kqlN!ZC{g;OEUxJHbI5XuF@c8} z#b`QO0MwHrz-5u(-Ssb9&-&TSUL~}*C@v)$(-N^z$X+uNe+R-(YVi7R^0SuM(TN^P z1c9+s3EwQ`A74Vr)toKGE~+?8Jywy>9T0Cu=Xr6abhl>|g^+PD8HtyRwv)}3C}Xa( zCk1ED_`^;!J6{A=tVfjlyJ1AbCG3z!drQII!>9U(>ZjU=>b}gcac}rn0-hnCSh@1G zQblrvIRf6uAIgmR(Ji3&g6}ocGdG99QQN?ID)-BOjZLz$5ije5AHVot0ZkBbz^xAX zmxn`Uo9?ZO$*}i5n?TjH^puSnC+hQQw!cS-m`;;tfzDjo z)#7o#vho2YCigBzVJY7Pw;C(Llm-}W4r1eq3azK!(YkKQInoHaJ_v?i_sFO5!I>?B z2w{#F4tsF(U{a+lSKG5!GHQU-V5Q4s<*Rq_Hi@xwlljNyMgO{8e=CQi2r^m)kCcacjR5(wR^8u1uODWbMv|Lc6Q!tZW{=U>P#pf% zdi5ffXf6`ucl#~^8n*E$cbQAEU(}fa;dXHOTzrWCxQfcC=oB81i=2Xfevka>=JL~< z(<37|vRMo5dv#=Y%{X*0#O1fi{|^@N{)#PqRNHIvpRnA1nenW7i5l>YEE2P~TEp_$ zIxIlA`hkCdvbm+*dywWL=y|^E!DI7 zDQh=c$Gu%udz1Kp2KBrnpwvZ7yo37?#YF=sq6DA}Wh!<_jB;mcWN}f0Sw1T)T;^h4 zmGsD|JhSO~Fd^G&I&XI`dcZk%BYl6mibtS7k!!Qz5{#fT^(xL%1bb{2b955GO@V=; z18|jHT^G9&P&2cfb67CO7xan$5UvKfU;=P_*w_WfI(&?-j1C9dT8Gtv%o1SaeAdvf zD9`vIB0`Licw^}e6>^y4${xa>gZdGb<7e#(-Wca(?B>u=+A-?#0O~>7yzY~qk!?0) zRh+B?1(i!_{sRJTlTVu@TJEm!R8#jf!KGWOoQ(TL4;My~4Wg+L2hPC*`DB0raVscm zVh}u{En&g!`2R8YmT^_>QP(H}Dh8#}0xBs;cdLZb(p}OZUD6^pEhR`dNJw`mN_T7; zlx~!6-nsPXInVv{KKH}@-TmbthrRb&>tAz@Ip!E66O4y1jwbw9*6UkDheOc_E_ z@f>T>B+M#f)qCFW(O|>#qMNMC|J)iz>ggYGTRh|euNIX%h}5?@LkvExkA}DVZK)?W zlewPFj?WM|{2V{Rg+bo9)=(jM2Kv`KOK>$)(R|LOV@MDmDaBXLbX~^9v58< z=PK40?mCzU$=jIcEnT{ld&vPK7OKozCFh!S#SQs54WUXeS*R2u{^q>cW9B7lHh(i3 zT;RsNdhFp*g{WJZbi_o_{TRyC5vWGQ_bw3;rPnWm63#;4P?Ei>VP7msGRjHf`w(Mv zGmzQk&T{3D-29Po`P@uC25q?9C;P*bjyUXTphj{%d2)RR)oNvz*vt4r(p58p=0z`Q zan1QG7K>Omi?F;OMLUYbk1UUE)TM9Q1Y7x#%W!rTv{&2?r8JsPxQXcj4PNrM4ASw4 zVvHN3ye`^B7p%U*;ktX~hQ-Q_WXj$_(w>4BshOA1mx%8^Uu_^x^IPq2=<+hUS6LWX zTzYp-9Rh4uLwYic15bO0gS!~X8FZ4+4ZDl|*xN{poIfz_rP<=sN38A#AazySZC2x( zxD0WgI7i)Z)nU*X38ieQJNmm!PwkWjC_|t%uICz^zBf*NH^!j{rtg$(mG2WO9QUp= zf@Dd5Ot$5dD&D>-c|39j1&aBBj6QisgI(dJLq*pY$Cqt7##8Ae_qlVdhY0n_4_CK4 z)(>7JuOAlW3_lrALE#|tdm6L5c9pc%~*2kDxcCB^kfiD>{E6TZ?`!&X|wF&4*$qgs3rO-P%lI0lQ z_#_w3rvlsLv>h{3E7eN^!qoeOm8GSL%KhMYWvtBsoT8@fac>3{+^QvIgL`f_?1hZfz~SZw(=WgTosB_6B= zlx-HA%{Y6GH~C^nIJB$1b5mll#MjixRX8yRPoj-%kacCqMrTx*9Q? zE(KMM&%sAzM`DG04t0a9fLrG%Oy5iWNgxeT);5J=J9-wS} z>~IX}J!E=XDWPAKjh)Jp7k9N!EV5aIhdh*puyMAM^3(%N@$Ye<^!4zzr)G!iDI3FG=u4Qy^()PuZroY4jPYdGlm@;wml&GPfKHG*(S+X~zgA(E^}Kqd-Ua+{ z$tLIc4Kqrih^ri5PIx`s=f5cmN9mFu3k(|+{ z>gNm0v+sRH%+1Z$!6c*=BC2VK-rw$T4XM0;(?HL>yh@r(bI#P0<%~oWn-mT1*B|)A zM~^@K602wzyG<^F8HIO6Ty3M8E_7yvZ`S0Y(8C+kl>+>87XnEGBD2ZJ!&Qh24q6xM zqn~%DpYxk_fBLHH+TMKHHfhxasf(&yjCA2e4Y65z^ut``(kxe_3om*mHdD(-GT8K{ zy?9SKnC4BoB+%SgcNAm;We~9122CE6pOAur!f=S!#&MY5sn1E*e{#s`<(|L&8wo0PIhp7EqD8u~fY~7NJB#C~BMS_H2ae*Fn zU{zddgBc&O6ea$p6l{rCf=u`KJFu;c#nLM-xwDJk^*u7y-^l2RW4aN^rGHnS6GWpk zNvy3K5Opn3>${ARtUPOM5_tq#91ZjWJ)v|<+O=7_uUZQE%dy|RWBMfstbEMI70^<( zeAOYK`w4rD`A~w}E0ZcnnwSvV*mVnu+elc0C?g=RrmyeVq>Oc4=cR&Xr8nZ|TN~i% zrY^&jFq*pHn{s|h=$DB`xH~jiL`8<{Y+L%@h3?l>p$lSSy13Zyh_Cz-r(MS}c3EZ% zvD!#w7YzHOar#koe9R{y;01B054 z*zbi)-8V5w1NpZ*mTw3f!4#7ksH`VyZwsflz*syp{u6LmTW3tWPmaV3PL#8j-JD)w zp4|=K{v(WwuR!t&+G-ojZr^@#OQ%A)A&8R2kBB7#gjf0OCZEL!dm?J`h}8RO;hK*J zudjk$?JpV6Uzfp?5=)`azdN z0@RID2gGQY80qf$CJ75?v;C2R7E67{X0ZfT=Dim}KD_90AQt+^0tMeyTfcAk_~^hG zl#lFxe7kex4KmI%;Ibu^#pSQINNxp`mlaA|fKL z-+aza`{D}Yd3%>OT4Y`|@-L|QYvV%jd?YDYUhwBL4-iDy-}&~*7x7fs-vK)XBVf9L z$9Mdzyu3V*-S00~qiGmmU5lcVJoqzuH|EM|q7nVQ_AAfhz4j%^*fb7+GHkp#-@OiE zj}~ZL{QSe@h2k?Aj6(lToTjOY9v1bFw+jjjjq9GA+IyPxWMqG5EwA?wY?MItnSl-j zN_XQ-Zl=V5Dxaj_JFEGPtRSSKOY%l+E2XFmFTMr7H`rVcR`gjYR+qP8zhnE zqEKV1+3JZKL|}UW`i|kuIu&MPO5P_`l-=M2Jp-0pMF&^GIj5oswD%@fdedY>K`&7X zd~A!%y6*xJpAoEi3l6S|M@6)eFZleDX#Blyr`RRjL1Pr0PTzt|cMc7|VZ+6CJn=I( zj~lE&Nsv_eOVO(HR|2*Bpko4N7I$*1w>8b7sO3+_BR5=RP6g7=twdkM(Y6X&nL|MX zjioacOs@Ch;K#KHgw-F-Wl763YU`T~mgbtJ)=XfcT$^Ivd~r+M2Ql~g{c3{C@bvVY zDV^}@ot3V1UU_SoTTVN{+4Lp`T19qu!#aY^Ft6tns^o#xYx^KFF=oJNyopB$y$pJS z7=Q5L-#!A1`f1Sd1l8|yaT}n6FBQct>bQ)h=4r&3FC^Xi&jNpFtJO$~NmBY~=msKG zb>`r6eSIpZaqsVdiYyBlgP~-adsvNI!aPKG!84AUjg5`}OHEnL(YDsP3l~B|sP1W6 zU%#C~-zGLY4^ZHyjU|@ZL*5#z*d9=)TI4FY9WckzQWWM*@A-rhRM-A_g}gi? zp<@M*&pMP+__$UOJzM?c^hjMdNkt{4;@R{ZxmbS5APOOBetu19j0B3C=&9fcG_aq7 z+PdHUrCC*3IZRh;fQ6~0D3JGjOj6>N0=?Gc>_^c2_UFZd>)-J8t9g!`DI#cg8YhPm z9D_GkM%3OjXpr&RCLq=tt`>94#~rUsdGrIm|YJX+-h%0>KIy+f9k23r$ zJ{Z)$7^XLeXax}h*z9?2j|2Xgd(9#82R z31GnHL{4g!*(RRh?Z&i#t^&nWDS~uLG7^%^0caTOuA|piKLXv~4OvU19(%df3YG*j zD0E1zt+oDpLQD$=igXlv?Ae!t#auDj5qJVogIXjOWpWb;9i`rVeqIr1->p`@XRv!n zQ|RPknmCv&Le88yBL&Ok4HuKnm%r8$V#-%9`oC!c(YR^Ag8Q!j1S{q|SH0lnncV{Mi3IUDt&N`;!$v8^{n5WRQlt0W-`!?jZQ|8xxr`dU|>*_bIPjNgQ0rpcDfqqbY>y*C9~g@DkCN`}@$f^&J5UbAv6&vegP%Yi$aS2DOV>A2#2{ z#xjB3fJ5Ugg>uW9FaJKoX+by+hyyTJ9LBekIN+w=d=kTcA zZU_Q0Eil)jtABwV3UdS*h4fkJ|GYgeg6%wLN%iVy2o31E=NK6qms_f4TtyLY&Ha*L z{Ix@^gJirGxE%36iiJd%X&A=H(x6QEs=PoUzA+-j|& z-_Jjrg#yWRiCGVrjHf^(xeg? zK9C!`yT8A9+Iy-5g#G6^=O4%`Z~nFo_~*Mi9HcoH>U=QVgp8W-&S7J>yLhKcg&Tp@ z?mmxYfTEp+KPX84nY)6_teUXNRp){SLcKYt)W0*;OiH@=-?(tStZqKxj<4h3VNa^` zS0nbb1=%yx)AOG18k(-sFtj|_2gQ3Y{XaK4t>M!Rv}xCs;(?;>9whOFd^h6Y#|~IH zFu&)vdJAZ<=;A=$TA)Uf{f&{H@c-Ve>+$3nBvgl#3A z)tpf_Kjj7vi6g#$^8-O9RyS~)u)McFnE{)|>rUM8+Fw1xf9{Bah*;b<^pu?a^IIU4 z{GYFy3)kelNkxUImq{VBl}2qO3V*)^bOdFjd(YMA`{Jl6G|VqbIF z7MJRAgZ>Ty3u}d0_YL!rk{pkXb{_k~T{~?5$)~k;WUE?LkMk={Ubejcf8HW39bKy| z&lvU1n>TyF@M#E)Nn=F3by;>R5eJWn0G{!T0oV;X*apC4!|>l2-#$(&N%Efwm8n&l z+ZMy81kaPMrl!X26HNE^DAsn!rB{%_d8+eogk^~F7eBmjW@c7%z^IZ%4KBT?S`Evv zH#ZoesTIsP{<(Jrgo&=n`do75Ntjd;3jc8AGGMOMHg+Kad_K{CQYS94~tszkF%3%&Sfdh=}u8 zmz0#2E*yPXOX+=RhxjH`!6~L#>*WcX%%7pacg?@lNgONaTA;g(xK+VoZH4GN)oBUk zHUeFLG!U7tX;nJs2s=+Vg-psp_X}#Tz;v|Ct_QLPkJ8TfJo4`)X#Vj(1a~V~Jy?~U z-?RQTpK20i|C>6FU8v~5Ku^2?ZlV{C&y2n_|MA#Gz<&DG;r56PDH^%3$KyC@*sL~W zPr#D4<)cA;3IvbHz8J-3_(IqjI$N#Z|8b{z_3BM$2l=NWxaiw zO2%Ja;TPD?NG-ti`}E#O-T?8uNN8$Q1&;!pY9=8e;EP!HWn7nzxMjCjB;F5J-__$a zp6>bazJDL(m^KOMh9qvL;tx8eLu@Q{S5y?qbRtHcK7H!gbWqqS%!owO;OkoM_^%Hc z14esf%NxYwwZWf+3Rm#Qlf&JqAlGOy5}4_!8Zn^Q>}tIR9&EZ)OMU$y#sAMMg2An$zRG7W;~tK0ksfKXYQ{h)A8oiGf;fL%ip7-B-BCCw0LLq}35xjl&_Topl$! z=)$w=vQ_jvm2l)uKtTArckk{}{ynv(nOaozHzC&>7HcMf*rk$&br@L|1;&1ecg<=e zo--zc!rBH*?f#L1f&%ceyoZi~u^|+1_ddjU@jnAO-xL0!&J4z22Hb|7EQTQ+(Kyb9 z8>=~h$etNFT@K+<+k=%Sq#uxhdgmNOMMp>5xJ{LO{K#7E++TFsPWc&1f%fL#`z~cr z^HI3yT1v6)m1%*>&Ky8eBjV?fKQ|+`u(Dt{alQ5EjGF9kObOX{B44yep8sgxOWQsT zBHSf2C2#^a!GoBFp?6IKlq=E1>!Eth|1@W-%Xo$O=75RCkRW9nBs0tx2yX==&V#V8 zo0T7d#83C_8E|8}Rk|`%crShmPP-DLhzk3E$F_oV#)96{&LG3%V0$IiZENuo0Ras- z&!8mapcVUL6@;IzY2q$BnM&y`;7&{nTWU|@LDS+@Ky^KSd^&3#_3zAO4nRN+ok@qV z3kwSi1&~|jERax8y<~rkvZ2mR4axH>E z*FlQkku?%hQd0R(#b`q%k;r3cXz00|pZO2|j=CaH<_!1uzj!mStxd*Lq@yj8=d-E9 z%}?8t%q-7^C?<%kT^u1dX`^A$Lg_yKej0Iy_@cEJxqb1EjVnP4*!Mw{Jz0Qz6K)=v z@6{Bn`cIJj=((`TTt_Up-`pJn0dh=COtWAnNXT-l)};_Fa$|l>ZX_r}+>Q(z10_KO z9)cZ77%XG3LsTrcpAqbI|Ni+jqG4_f9nt$$aQgNXnJbk;G@AnBDOa10tU&O`zPVPl z5eTb;;OFI=CkW=VFjiR#(fennR=>iad(N9sz6FyUbrZZNHeCrmhjSt)H&F%Rdu*(K zf*J}d*dm!dkM+Fv#2J(`5_}j;t%eH3eKiqc_!-ua93&xbMdo3U@O}oTd5>7S07&s8 zMmq?79I>>l!Fj5VxXmKYO>y4GuIF%YN(9%4^gI%G42L0%h)o9@Fh#(z^5R9ooI5tI z7VKCpKl)2~yV`C4#PXstv#|X1^fW2!+F*Y2Oi2>6Rv6{qq|Wu`QeqX97)Ucdb=g{$ zk&9N4V>dT+=^DPXQB-`3^S$Kx+}ESW%p-FRD^1TBEdmrqTpI;%Xys-ETa|(iJPDt@ zQ3!<#;s!=b_cRHQlr$^Kd-ssrdekh6nV$YWh1ae@IYjH6wxZsl3RzxVSapsq$b0OB;%S zYbk8x`kIF0v9T{=1gf`U`JGxet|G}`4E)4iMC20; z+0s%Gq!Vrj5Km3Rrr$eox2u5_Tvqk~zA|1O%?>`I`2??x^V$RBAhrOf+strkx4+012_>CcpbU@+}RnZqkj=bRi+L(KH}Es#+I@a|0E|IXeV8uxBbFhYAAf>?z{2TM97gv@Ss#sZS6=S)`91*_nAyT5 z;(mnofOnZ)yLgHT^HVU_%LL*Astf1e5D>E7eQ{$^{DwZa%c?f^;I$^>i^eXPr(z*& zQFzIWdzH7#K4fhSy&8{#Xo9-kM>}o&7N#Tna1T~>c{#dTz`>eIPVI% zdu%S7TV`u(svw0%S{DQy>1m~NtOz!t}4EYMNzX)(Jp?3g1{a1Pp z0b+XT_XE+7eZA<(fAi((uXskJ!X9O32=R$U(Jo*=={ijzTF>S|RGJ)ko&5`*W8vAI z>+~0qQ^F|ir3Y$ z21evOrKX`Ve=@k6Tj{ivRxG11&{y)vhokp~C~>P2ApcB=7h0EoWxBziq&A`!uG;R zsaHVKUH$g`pIE#G$5wOQgtG)usZFCCvw6ILATkBmcX^zvukPMFBU(a^ZP;iDr@=tP zj!o>*Yu7so=FM+u6f+fRJOd#=TKr*o(eB=ZE2q+*UYmcl5Yzq{*tV3P#6DuLCr95&K22Vry%-jW@UN2KO4Njz`<<~d7zYa^^+8oXI%-yDxBC8VL zI+CXvO`BprFnzwhjd8h60DuKEHQ(*@`zT0U)C;jB!Y)bcXW`gx5mnEw2`pC@p z10Hsx97{`0@7WBd^QTXrvYJcIV0LmX4{J0esdIVSNcbhnxHaT~01c6U=LomxP?R}7 zeKiCuTy-MmJ=8iT!9U;lTg!tR!Cym5f(r$w@g47ql2g&2X|gXm%XOQrtrF}Shwq=& zc!HHxPFCSc)wBb+|Llc0gBs`6(VL~C_I|yhA=r=QjM#@Q-9n)dO5*r97BNZkjoiNW zu^;h1Q|}}zP*9jCWX19)Bot$mo)BR-J@rX>M4#oUU&E2a9(vfRk1rCB9%m#gnMcs52IiN0IdJfjJsULmrC)uDTaK z-E~~!jw6a4u_GfLAN0yC!Fzo7Nfu&3tXtzPc*f_B$Jt%&A5jTQe9<}vkGU6}Ndb>c zZ;$T6!rJ1-SN~G9U4b&B=+3~JIrl?ouyvg&b0(D+?^+}KbJu$Ta_(;`9`ON4bOD?V zZ8kS<>yu2-SyVoZDik{Pt*xlZIu5MuOjxKM2XqoUnc-JCE4!B#u^RSiy_0$Ca9nkC zaCrEIhrVfD?`5w~Uf%ARvqUzUDTVK4w$rzM;_H1VI=CV0hi!Ih>#j#m$HOb;ntig( zOvIjJzHKT{s*(Dp_@HLLqiW461MSlDK`|R%^mE&DA(b-E2I(h?XabBMSU^i zxRo;4OK4m@$Nl-~$g5G~cO$P3m)J}3JFMi(C>63-&h1dyi=17(H$Aa!D>6Ch_Qf#Z zA*H2qj-oij?!nxM7nQzoA9j&2ZQUjT2KudDwH$Q?3eRc$C=QogXR^F2gtXO_E|D2c z_lGKs{V=B~{N&#AtK4N!4z64+tG^_8Zhb4fQX(iK&ujP2aU7fI3taL=+xTRunHEJh zR@P1r&Cbd#?j%LC3=?$zeOdd**&%V(Uf!yChF3+diQ*Is)%*Hx)tuq7Y$;aLp=nji zdHehVVbk6PB1bGw2)t8mCTjL|1Lj_h)an(mbI@>7i+R20Y_^bSn-Jfz9-Dr1e8j|~ z$6@s9mU_{BucJNF=BBnVz~RhjuT}UHm5kdARcRE8{}d7^45-`jzn#1C(|2V~|EeW% zua@^okUu%217u{BH7DbTM>~r&TuXi32HRMD8r~-^fuuCT3ky$^?H@I-R;+OEEmp2^ z?)vd(1=bY4e}Nmh@3h!dA^V<_j36MR!f}}%<6tZF+K0!D#$%Pz&TbXiy2VnyNf}Oj z5NqyDq{mBT^t+%_0bEK_XxbVia(GQ+XlR%dD=N0bT0Zz()S5#}xZ7$BCX9zUZx{=O zL1ER6%SV*_WHlJHK*lun+gS|3N*;5)MCjU-< z?<9Ad{OTEH2ItjVTeQb!dna+LLev7K5zlhIsph17>bn_nvbH30n>~mega-7JiC^z6 zv9z8TzQhUl_nhr;C_#tVT;hcJ)i*U}W^3^)$)W<$E zA>&a$_I|vqE~!%`cGbMbGJR&P>>g$4bh!h$+=yfSz8_hxX5dNzztmc+HkrxCvIs|h^g7yHo_$H$g+Fz`mk(A<(N~lk9KOMo+8kMjm`(!1yHym_(ThH$mmif-t zuZE`b0w`XhtG7fp=b4d;Rl}*r*$QqyUr|=Lgi16w+wb&c&M;8?lssuosZSK`;c%|i zn!;D39Avo@Kx%GS__pES0tZfl3EiTYE^5?{-G`2skb=1%9Ubv|O%-C)Wujqy;Nf7O zvhjMiCyu}#okOA;t>en|mB%0gvQ77mRpSCqDAyG0&}gSmy-|6<$4^$6rTD0_fN#i^ zACytTzk|4zr@Y{|qe1$DN!gLx?(>=HMqS2{&0K0aGpHV~908}E#Le6$Vq^V2aL>EJ ztnuUzyq0lD#$z3WR6m$&57+H44?O&rLve$l%QBBIcSB1R#C!kXMEI`c|Il=hk7!!@ z58@%Fi;j{{b5C32x>RKoF2y=dZwm z2W35g<{~$*B2GZp*J-s^o5}3T>1^taBB^zh+B?e$S%+R*0d zx29?KUz@nc06aABJRdS*3vc-{6IX>D>H6y}|9r+c-!GfYf37jF{%??l|G&7e{}iI& zmchB|&uiL=_+NcK^+7=C9!eEF@ zgLtk0{`Aw*u4iv!ZU#YuY@;E|lme*NXtpg%&N~-^#t1;YwBl-^7KR|xmAwrO4J`mQ zBbYgOp-&a8Sp&KRh5K7a2Rj2N@j~vZgv{FXvhTUW0icPTw;Xb}R;!5L?j+xB@ zwU9KZNJN2gxen+7vcdhzLgkoaWwe}Q!V_rJO*i*N*3}d_G+j?M~nbyl#zFExX~H8yY&0fB+s$TSGuGEyw$^L`|Y+8 zP_~St-tjq@gv6T;)%UDM3BYRl2nw+x7atm$ye7iR+QnWSA0NL^bFj)`Y-BX( z*#@AE%z(B-fVDuj-c)`gg@XmfT>Ja?3MX&5&$w3KY$=F|j<#@H%uJWz zvgm8#sIIN8T?iCd?OMq%hEa`@@FzAnkAu^}AyncDS5=TpWNfa1)Q!EkZK}6aGds>A z8J{$mx^g zwUcEpYIrja2fu*RBK7g{v3)z&z;vq|J2V1TyU&&9Nk=kMY;{k0P6-Ka4;n^{KtjAF zRBYD$nQ7zO;y~ffVTZ>~hbC0R0I=lKmdmaN5Z-=sUI7R-y^mxMb(EnpXg>t|GE#_@ z&V?NA?c29E0C|ce(tUh~Ljbs33xYwekWp_$4La5kZU4f5Qmgogfg_++J?w zY7hR|U%Q(TB}THY$>(S1NUwOGr)R1W^~P_ab7_RGu$_tIflS zT^lLQI|NE&ID*AC<-Odx>O&nS{Epiy+Sp9s#o(kX_(-cDj^~`LxKN*1hor><%>!|m z*RMmXCjUf@xr%?)6B71~Fw4h4z~iKUGw=@RRmyp~IyQ}6-j6IZHp92J(s*0#@85rg zaIEZGnM&`^w1hKtLcx)g1PDNfuuw=h!TB(8X1ag6Ido?6Lte>6A)Y81(ot_vb)5!e z0ToJsstu1@1#ZZ|<;&_ejR9n!$|;IGs_Fbek7hq3Bcu6G3hJ& zu=n(FnI|B~dlbH|>cti+H6uY+`I6QyoH97H!rqz%PEC^c+HkRS{7Biix*;-sjXMdJ zZSUCjBF=i?pjy${^KHora{NyWHPnI8u3cvP{q1ah)PU#@OpP)?kXm<+z8x@{4HbJH z_Z+V3dHfV7fkKJNBf+>ZSU zBsKMc8K|{rS~r>oHd6WG4mnFaxB}I=X{hDgM0t=+TxVCwZ!egc7jWUM=2+TG9WFEt zV%BSettVWV#mn+0kVsP{?Ct?GpMMWr}zNTYTm^}NbwgvdYum4Qma zzey>mx3Te_JSKwE$5Yla!RogjE&F*==tB8JHhc-AF@Citf>4t_c{#DbXiEP)R@vE( zrEz!d>VfJtr=VJQ-D>TvirmWt;^HK4&Nl^D@1#A@thjd^KW8O>eZD%j&4b6dY3)a)qXy&P6s8<9xa&C5!RqvY#Phk_zH++cay05U5I%HpgWU zsveENk7Lj|fsEr_V-M+T*t>0YPoN+Xy|e~p(mAJnZMT!GEuoj#WV7l;W+5aSP?2@Z z#?aK(URJ?=AfJ7JL(X4>V*8ZMmgRKR)6-Lx#Wb&JD8Cd^fy#OW5^+;d$y_(j5gVAR zTqzlm5t3QjgUZI_APfSZd9~cYz-o*`hh11i1Zp>?xO&@(%l2EHn-v|;n4?m)g?}Iv zTSnzAIaIgmFmT_EAR8uJ62$;K)wODiDQ`HG97ae?W4>3dLgP8-fccR^F_e58!)_|I zxn);aTRyCQfiPXCP+B%Qo1_*2O;7+XxieMm8b1)Zs&?ig@q-0BCs(9)bWc}TH#Hw2 zgtf20BJe71%(p(6I5{3juwuC~!Lv0KA=EuL(8+XkYoypRQmu0gm^@0BeeaD+M@l?k zZ;Kl7K3L(&JI;Y&y3PBL!IQZAX1+yVhJsT?yj&tG`%XJ;zgr5wjsm50As1I6IEHH~ zUDByEkM~$_rWL(!S#KT@WD>Ek#lNM%kzK?ci*o2)W?#w59ZH^R42py}WFY!xdQ|TI zT36so^h{du_psxf*7V+|T*TjC;}EBo z8PvjQF{CP)DV+xmIu4fhGomvW15rAC%p2GWy;DmS8$|74J^Wt$M@7uzUI$i%+L|sO zbt;|Hw8|q)dFNsrWX_$^bCE(-`0QVe9?KYdSb@DzGz3(gW}|uYw2!E^KJ!VSa(#%} z&e3={7wZz&*toe}T*~MvzgS=yF$ZY&N>xotidfC=A36;>x62i> zV>*!*$ce(192qcPPYugqNEh7u-laSxf*=8)!czXD_TgzQsb+YQ_7Hd1-=%(3o^D0G*%Fs=*TxUV z?!d@yf84I9bo%t^Kp>bm0$Fy(tUKv>b5SACdpkzc3~#|PYYXJe2FC}^TT6Mhsx6QD zYPVchfzIXTl7@`&w(#i=ywojqT`mT)FHhVqnaG za;Yj^+o-63ox9zE9JsB4+1a<|wt&;eLjoL=7(R!^a5l{etGxVL8;@cpEuj7Cm{XDv z5zRe@ic*!EoF^rxC92KHtpItx5?Q0! zY_RxyEi=_HlRot=F&%SHZmV~RQ`+X_yerl@QEAb8NkLZqvAq|GQ={&{URQI4i0FOA zOBD`rdEW1s4ntW4=|c%{;>2XOrk!xN^TR^OX}+SUNvUM1ZIPB`lCD{l=#P@W*8Z@c47H=9f^y1Tqh$P`#X-F#WM*t=d3FuDA(Y3xPdr_L!9OE%vKOS* z`eDf!c)sBg_ue-mp_;Yj8Fzv4$TbHwl$ZR#2b0=LRCQmrgw=4eA*bPJ?(%v~rj?%F zfYne^dk5!|;?fi%eA{N-HJlv1m%Sw=)J=lF&zE$tid<2T^TfLn<8~wGu^?m^G6L(& zh`SeZRIk-*@gH^Z59UdcY0Vo}SjP0F$&!^S5o|qC##Q99oqkO~-D4iC;(MR;NmEM; zd!$oz8byYDEPwxD&L@)ChZ$p4#Zk{GlouTz#3p3~cx$J3e}E%o7}L7=a(>T3ikn{S zYK{`A=U8~nbqhC-m6&YVyN91F&=d!(+j4tNb$ppszqJiG?&qc_JV_tPwi=-<@)zr$ zy{S2T$NNeK={IL9+9^w1q0Qi^{cQ5mt?Kk%$70SGa(=2SBc(2Z!2W*DH*m9RU)R={ z>vTZ_{nAm658c2{M1*A5PQ)1r5T00M84L#Q_eW%1@wEX zZ6_6t1Mmb9aW;zFk+nM5udr?<^z_u;c~5$^e`O1T5;l=A4Ut?GtId>C8b^=1>q>Z( zR#z&Rv`*>Bi}b*gM7zdHs=9gm!BUDjn%z`GKu^A5qvo2Sv4>j=Z+C8mO5xq`%4KrK z`g^!2TA6p>e)8p9WW;V*xRYHulW%rgqVQ#raME#Q(QN#9C)fMFtmo=ntXV^1oq`+w ze1m-#ueM+6$xapa+;0MU`wcZ+*VJbMjCg5WT1z*&xOgqyYV`Xs`oGDlX! z8taxDb+b1<-J|1M1Ui=NbEK~__m7L7%IAI}`mmZvaR0-8@@tcT0UE5_gG=%QOTzVB z!OD?8kKN*zYVKpiYt9u|97$RZY4511zR0P(7M|`cT~=jNH8e0_thJ3)YwUqN5KWp1 z!FwJdasP4`M+a{`=5sE3i=%f+YR*Fu#T_|W^0q|qT(F3?*RG+*cl%NaUF$RVi_TwI z(3eRsvHaG@wXx8zrpI*(O>Ma8E`|x}%43`PFcIvFW*ir=@l=Es3^mWXxalviI;)|4 za&&8fIx;uGe_FAS5FQO(`_&%bdupnMIN0CJOgc17V~x{}7VqSn&}n2pYOV;ohD893 zfUGb!Vr$X^%RCZ$6>|(SCi8?(tx)40bFC)0*q11&&xHhB&ep;lBRj$=o zrpdU8T6UV7^cn^UnczIr;druHK*4E|ZkFS!5pGhoz$w6nDF-Zap|T%v!Jy!OgV9GzlB<%x!S)uY-#&hZ}Z6H^c_y#ho}3pDd{*r z@KaIM^XXP9E^%i#UE4Y`>aM7Bi?yDMc1_zlzjb81azVRree)<`jRf1#f=P7cNsdM! zDSiJs88l0AtZ7NRxm9S}H`B~Iw@+5iMqA3TN>)bKLaV7s8-bek_pMqD-w&ji70|-c z#%X?(IEBQHr8=Hl{aX}kD+g6+Tl0aatiGM9TsqI{qRW-cy#i}hhEiDb%8!^b^}EeG zHMze7f0T7eWhb9>x!2!iiL0c(WbVST`Zf#_Y10Xi|7FHH0H25+DZ;FSEW%vZz5Rz% z&O+uLg(Lqie;H4q;$?A_@kf`Pha;K#ys!^U#g@IY z;jd*3D<4;^Ns5x)`a8OZttuDGCMO@}RW$6tX7@>F0Z|&T^LI|(QB!ksf>SoSaRR<2 zb;)wk`Di*+Ue*2T61Po6VeVM*n9#&Z>4dd)?}18co+MD(EId1+bq^|fR#9Blfw{~T z^GT$onOP(0!nwL4K;I}`3&U12VrLzP($c8>2Fwa%i~u) z!3Jj3{o>rst)cC^KRuP=t@hx3j$-2C<(^}gFJJ!PNull3uTs3)SUp6$T*UODu!Fx9 zA7Y8YQX4&T6em1vJl!T@Y*GFk?ZVHTh0>N;<{Q+pm$GEVI4dOz-LwnkIi6K1Ihm*Yxf-(O<^#8)7s^GnfwMF_k*lFHY(BzuxnyzOk}y$s0LW7oL55?q z?e-P!%N4@$5Py$o3%tQz-bdDI z(L*_F9PQ8gcB15Jg@A{KH2+yss+GF0jOIk4RC|eH*{zM=sHKuPYkpARsOIvOFD&Fz zQj~Tob8gRly+vvvkq|N1<&;B`r5Z{yklmV5neDi?TMsy82jxWhW1Pha z^JA`PvU2`DalFmLp`yhhyS3GIOVR3D%98egk0qh4XG!$~kBL50EmWKI*p8*-*`wti z&G-A!iw9EaqiQ+jnL}O0D~)6QJ;fU(*-P`#Qo|mfIc`#c4rBP8{%&&eCtWPG?X+Gm zF0+(&VSN_OWJO;k(!&8tth=$?U%S1R0Ov7B52(3hy@a1w$@HOg+)bLfa+&?|1Jn@@ zdjoj}y^lnx3Wh>Al+DsgQDdkf5=?Q}UY|tc z6Q6#-$mE&e!xKw*!Mz)2eVEk*>iVB_!z2Q>UOh-mB`GTUbXi^ye;N6Ev`0ain`loY zL^&eYD%XU|r{79j{}ayxQv&%Tcs(Oe>p?;(cwq_r=eOv|zkiAL8OKNA zpC1ig*3tj|@ySO~ePuBq;nE<9uK!g&MTD&mxJVf?cM!^0DI*hTlUoBq$u}J=ngZZ4 zyGa}I9#C7+`3Zl2$n7I>o^LhR4FM%9@ILX{Lzs-fF^YiZjb>Q*%rN01z?Qrf_`|V= zetv%KyAZuc0k(x0$RO-YJ|I;i#JEs++(T=IOId)IzJ33m5#VdHI!$D-;Dye}!J&lk z8iAm*y=Qoj_bSfIXXQx42jVdXT~nE1)lnfK*O4X`2r(O6tIseZ4T*b5e zZFP-ei|}Ul0Du3@rNIh|;iAlQRdDykx;tlof6G$?fTC%KY!-S_g}oo4Cv(jY6{cSy zBy54_5<9T4xs9+pw^v3O13V%0K$^zFKr$>!KTD($r2>48wsH~fYrj9LPVbOpAgLka zEYztuqoQ>Hz;ZnL{QQD!T@k}l1u)t6Ja7H}%O@`q8#2f=b-oQ-=7sn4D3;GeXvGXf zhKD!TcpjvzJ zny)8h2CAs^+6L4YrJU39Q7OOQcf0I{Vc%DdPC+V!dKv?iXLg|clq5uP^q7aF989`ZWj$K}*F6VjTr+fW z9}54OO7(ce1Z+czn)1rNNznJ7+nA8ilO_YSmWWoCTI~|65fcmVnk|&pI?|m-6(o2A zifvw$f4?__jo}95&*|sHpq*@hR-jh}n5YPn)H8vw?LdD^qr!3FVXkFwS^%ORfshH6 zEAvDeP%n=U*57Q5juR4)(AX*1b<_$4D23pMT%sh`%gf)R*`s)W-#j#vwp(_Qt0W=i zcFR>`ib$&Kw28Wg7o;zw7G?L*89KSG zw%MPAKOd~&$B{-H+MKVGnaF zl;*-58Xf4#(D?s>OWg%9GyFM?R9HDOXv0N z(EYG+%2;cayVS5U%`v=F{%CY>Co?{t!Ez_hNub2)p;nYzgYw+OoQG53t#-$W$62o0 z4pFWhGIJA=9`3K(M)*1nZ`*gcI>dPE9n=@ITQ1%DY4lV4Y%9em`w`)u*VGP0pHjzzn0MC$487E~3?69gGfh7wRz*Q@S!d44mfJEhUBTDXtG&&A99;N4yp?Tl~_tZLLA*ZQa? z;lqaPkPsaHY)O&xHd~LoG2u1UC$~_N;r;&fF5)Ro$JBo|Ts&ti|9^J) z|K+)b;BD}_J$Rk<{O=#HE#ndYa|i$5er)W#KFjY*a4vDRi5uPdda0S|R(Q>d()8+O zw=?b`XlTN>8-Aa;T<=Oe(hrO5OWTG%s*S%Z*0n$XMnew`5B;C#@W6L8sYl9tPGye_ zt&10;1>}Ph&g{XNevSI?KUFxyHr`07|Bv?_POjfJD}45Dy*5e;7w!7>Z#oM|nRa8g zJroLdl5jeuzzO1`i_`+p(5g&M-|f9*NW+--jx-q!?*@7Vo2H`T4G3{Q6j_gHBH9)J z-3EFgw6sh^ z(h&g%1kycG%tkKY%sB^L92dX*{?2a%jCj$}>TzsRYHny)*zh($rwSE+^y=sXf|@p~ zo!gQUZ*4eB0ku9}*f*9g!3dIEVI63J9l)a8;g zdlQ^`=ud+2g6qt{prd`S%5`JZ#NVPm6)H>7tj%zW#dnb&TSv z#)_J=!OsTGBJ1c$VCc`Izh%pU^7M72aS_f`Md*>U%GIj%sy_DyD*0m2;2>G@Yx35q zW}9!wK1@oHlySAjS6YB;z@#?pV}JZZKT62G?`=qkf!weEHyA~LX*EeooXRMK_Uzdc+20#OLVkW)yX6CA z-{hHra7@=sG_(K)*8S4jBUw-~p@t@m4CfKx{-9{-1@w+@S{ zYu~?V6a`d3I#fV9hLXmc9=bbZ=#(0yK@L%G>E9Zk@B2Q- z@%-n_?Af#TTGu+S^ZZ=J0ce*0yEMK=5QrEJ3<3>*2%wHQ)|1i-_XFj;_WFPCB0SC@ zp1x!{M=SpR16q1^eARryf4^f9JeQZ9oj+?cH@7rZXnZ>y{79Xhod+OqLudtFX7Ilv z9Roins3hk8(cnCwngbe-=(mi&Lfa@86BR;0Tnr6lCr7D)D(nS0;&N zzXJZb8fXfr7oS>#Vsot-Jm#T>%>VwiqmB2+dK%u%hAz1~QGJH~Si{};%zQH-Ug3HD zjZ|6*4u_*=j~l@gi|hkx#hw3qurxVKaa(Mo(mm~PS;aj4)})_^FJfF;9kPff*HzcWDHbPWBghF+3@xab;=*sOQ0t$p`!QS3U!@ zP?RS`0oU!~8pl&AaD0zRLOeuMSBjEJCI+0@k>WlTo7!!l+YPdTe532)UK{tiJOBNs z?VS3y_TvaGM+AX6{CSnAMYDI?&H#79*!L+WF8mP92gB_}`5@7?_P-?rmIiejr$Rv# z%D3-7n?745uB0Q0x0u+8Pdu-)ImWdQ2pwr|mgpH8qKw!ZC{a>U;(PkPyEV{zSMpi% z!WLA+5^gditiF-wz|Ms}{ zC!AQ0-nsRIJygUCx_-w43G~$hg~O&XF^IS%R(^=Pj5zf~i~PNmyZ4YaL6xc|?tBZV zQ<-?KxlBEWChu{{mSWXL>(6qE_+?k5uPLawIXMN)Hc+iaj`NMETF3F;UBedQe-|n~ zPE`(G1&KuR&>^bbX;z~Ymmy)J+zS2O{`ktK(J|^!1F!~cFJ$mGalSsT5PWH&r;L<^ z+ft^7TkYx~h3F4LM=;CRM|)@IvH(y^L*P48G7{YX{&;H-RXGh+`K`3aFHa2c6mEQu zwFXI!OY+K8)%||j)jwxvy0_*s}LzD^8 zRO}l?%OlWw6I=(AXIY>BJDkeozEDHP#@02WQyCQ~HDQ8F)pwxGQmon!mUHq={Iv6D z0a5w`uDrJXTAY#ywOPjR>Cm% ziKKJ(_w_2ST9Fc7P`AiWzEO(~Ovbp`Jv^0fOKbBCJwG5#*apb@cyX^!D#0IF+mft) z(j*h^1x-FG_+viL+xnQVSNELnHUhKys!LyaAwjkpGj8|%X!8j?IW-lNmqeEt2}H(L z`i6lJnD8>F>>UG5|F3|1wLiptu^P|LxmDXMRzU9@gU*-Fk&{>GuDilNxXv8#nD}^t zZj<#i$C0iP^LIM+^)c2Tb06D0nn(MJ-b-^n2o7oi#k>Pi!E{HkDC#s7n z2Z1g%2$&U_`tPDJ#?XyUPi{o9qiPdnxY)-Kbv%xuOUaRb=ESJhBdiT0Mw zRbQjJ+y0*RP&Ldrr@z4o0ctDlGrvFZ9`=voflP)vD~ZdwPJ>jEn&fe-*uuL50-P^i z7Oov^Y(}U|rbK#^ARx(7p!x(kuquhaOJRfmg6y#{(4@Z(Ayl{lIvf&W6J-i|vInVc zm-2(6>vXX&5RYF;gT=%HX`l!A?ef8)AA|KlGC6`(5rfe9zawW+)7;kw$mW-&-{lp1 z$DcGpp|3A?zRl#ZbSh^g9Z-i865U1+{09$Gj;78%PcM=r+)#~epGq@gppq=}_CVQM zE(lw2ge+C;6%73DLA7=`tk*yvYPQZJ__%0Q^mA zFUK8)S1YUjJ=psWF1?gNrY;2|>kpHAH?IV?=*dycde%G)OfW$v;GNOZ2px%P%t7KQ z+-IT{@GMQ9`TcMp^(z-2WL!lx7^ng{$S4d41GCUy{ZHUoYB4JH z>8p{Zb0!E_h^MD%K2P}7OIqj&B+w0AKMm?!?lQ!PX#x7lXX;#I^W2Ieen-aDhNj;$ z0B4a81cLDjX`mcs)?fJllRMh|gPrH>26^dcj9MFCq}|;~n^_v4OhSx&6tsZGREqLJ zyV!8dV6~5^bHRR08jBdw)G{`9bPNSd27{)7Ev7b~ee<@M@<`!Y4?7^;pn;pxc%Kw= zGd|H#Wj*7Q^+Rz>cR3d2eL27~bGy4HrEx7Ae+BttvG|HEdD@@F2=Fm=q9D8h^;N=UkU!IGOt$kq3dA6_bIkf z=1zkL%e}E98&JlnHI*KGgcfe1nKEr@+xcCj)<=}b$fjxYV%t<7PSYu=h}?C)XqjQU4cjC zA2Hmj70^EynZy`GJU}-(jeW&srZye~E%nrax?EKQ+U`BoHhmHVEXVNR*ni(}%!TGE zBxWG6vA3-Fh>!yZeybn+zGj={n@2Nu-&>Mhy5#Cv)G~~2P*CT-cu18c;ABV1w-gn;!>8vj$I)+B+d|E4Ob^lTE+WWP&=qcx|C4Lq~Ve2tbJpfGsx6)!TH zyYk{wS$aAKWrP7vU`G6a#Cwv==;~AzEJD;_R|f^e$aU`ulqPTMUn{b_W}uy_%J={r zkjJb`J#wa)x8*3Im~w0X--{G7Ghkk$SirU5`ug6LxZvR?he{M@HdG^eq`jMm?GFb` zmp_XkbW3URQ>e}+czjre6b=u1MbdzA!P6CA@eUDDCd?18+7<&2qcL_5>>-+gp`ps7 zObom&_;Q(XEl+TEyyOox85s;>0$d~a8vsc|P2`M0(ytpFL9BtS{qI0N8 zEG(=jBUgR7`(it;m)IrPqHS^vJHYDKPX;F)VL?79xG)=pg50~}CtJ$K@?C7iX-jVY z9FEgfG6;}oEY$Hg}T_;;SIPLn*I3m_=v3DiXRdRjulfkm)~XWwW%F8iZL9oFG~ zob2p9cznm?W&iHPJ=bd?r>uZROQ)Qn{$gH-CxvgO*l=>O?;;#E1*^je zj5jHsm)OkKtfqbIC9A;-{@g2+$I6gNVOH60-_t?ed+TR=QF3&HRVR_Bl>IOhsOQeT z$Zm~0_h9OZZ6L!vcqjj_DiVBbjI$fDDn76Za`JC#IGdy5dOIKkT)H%0cF>R>x4t~1 zTpFekoA;kTkW1BjD8@LHjMb=lL@%pLG?z<=xS%dCQja}0hX>KjbrOH^{?4>EAGF&a z`VlICSMuRVvJ@9iZs7S*JSd<|f$5kV@JED&SAWCVA+;uFT;DB62~?Xri1I(@^&_zf7B9Ry9(o38FV+dgY^Qv%ew}N< zGsC(+G|ik!vfbZ@-DZmrF8#Dt?n9l6rx3<%Ngv3nmd?E2a{A$LA2>w2@sv>@)>JB=H{TeY4=%kn|V5Fu$MwVjql+#Dd>=h&YQTPLzfia z-Av-}6y8Z*6|c@Wm?bxV@K^V_d-2M{Wa#?CKg51?12p45{!7DCklX~X-e<~xJGv$8 zElWPDQo(??W2peOk~QLds3H>w`M1~EU6(mfoo*=*xBSJtNm~2HQ0eJCs<$^=?^(y( zXj+^s1)DxjDXQSlYW&|xP^$V+MKz>M4cc6uPUjGcQ40>V~#3_)1sigstvc^Xm)LYLwSF3Yb~s09L0dS1tGX^ zS(0($;Ui<|P>ZU7-&vVas*MZO>>g_$PrMuVe^Rrf1*MGF$`+jpI7X@NwAS+02h2Ku z7#?o7PZD?{^=QKUqVDCdL!les9q0kBV#EClaHzTZzs`%g@i!fzXs4+$J&PI~6d@6k zWZP+i97gRysB?TM&>61K&Z34gMbQ1Oe<5mt&&EthsVOTjldx!|ml z{P*uUaTn z*KP?-w(P-04sg&PpHR}Zev|p?6|KXhL`P}N5eEv4c32(fz5^}cCB^x%p3Ols#r)-; zqVb!8^S8Z_c(qxyRss4VQ?FjA;X}u7Z2ENMVW-LIg;&Gys}_mC^5(Nczu0=-g=!0P zF}ILOA%zT)FJ1P&U2IvKVSVP=9GIJnrTb$1ciB*_V=y#|lhiqme+wX=jspjp3PqBq zij<6iVFh$=%G2SGo;`G)u^V*DewoHBBKpNFIazdNfj6Z$+oWb|EV=b3buM8c`pK)E zoX zSp|#XF>;&3tT5JR`-|)ry}kU)vgtq>qNqM=Fnh!`Y({dHHKTnaYIoX0E>&JOe)Z>9IwhM}`v3ZLAN|0G7NAX@ix;40d11Q8y-N3y zP1CA{jFY9_S^GC9m9$wpl|$5X=B}5Pq5Z}!KN_6vC9dbR&7JBrn#k+xPn{FFbRO>W z8Ret!6;9V4UU+dx3MqtfGG%iZ4}BwwiUG0lU%!4?_a?D>0QZIpKnkn^178YIzZyWo zd-dIum|OoRHPA%jQkZRK%(!A?jilluKnoORh6zq*^3K<_86$GOjA<|Ais!Y|tmUD&CaOQ(2kPC!Dq6|eB#TPwT%nPV=L^k`9X{L}U4lub^G?gk zNa;P~GPE3gs+z_G`cKKiY(kdpSk!>V397B97`V*lWTgeu!#h}8yFL5*y-orgle8W} zuyI$>1`89NZy3$w({z9`uI<%~NaEX!8tbzYI=cz#fYbD+B9bID(BX>=z#D9g{kUsY$}DFm`LeUt+`5;MMC}kthn3*{axQ zx)?a0=prysi@IT~c0g=x@>Eq;K8s-y5fQ;W46Y8WO%HW%+&g*Lxy22AipGA%RX%j_ z47!a@hiGJvYY=!t%TK75{puj0Bcj`?~l)920Uu8f=cRa(xx_B?L*y7U+F4NXPD@2J{zW= zXGZNg%Z>!FBXDvj#xQW1v_nkL=@2x(Gh{HpdR5?5G@S$>HjJeEYY*MlIe>!MK_1rv^!31%-`_}6r$L~3YrIrVZ3F;cdcm>zMa3k~ zF^!-L=$O(0TLVU^lC&NW4VOch6I|BLmLyJJd>g5yhUM({;jf9Pz(z+lSRaW(@re{ae z5$dCpIKSi3C>!lCMn+(r%!JZAd@MtfI-Z~P5$85#?7vFiEGkj^UT!bp?0Yd84e=az z_1(#!@ED4eWFh~jL~ISYK#te$qg(WLd+>yMsvI`)(s_l{@jLY`$IPYw`)(wpfT_pY z?E$NBpeILXO$g=%Y?<1IhHT&cCG|hFLRpMon|lY08)lQ*-~L z&g_Y_#M0X>#(jx)411SWm{Z!27u4Im_#1pWtRmr8&T857-4+JvgE%kIKW~i=)_Av9 z&qwB+>5q;l_O}J(pS~C{M9unyYGt&nO&n;#YTbJ(Zy_dxlck33Tdhf-FZKZo2LIH1 zEGC4@f(Zka3n&r_V@pG|vSQk}Iop~Wb>CdKb)#LT4|dV?5SPwDC`u7^Jx1|OypMaC zq^969{O;K-_=H0eqkWY8HbW1LUea%t{Zy^AC%_8D_d#M8hig zalYQ@-;lECq&HN)e|T|A%oa~O_Eq7<*OqcuS|8zV{iaVzHH`Kq_3}{>g>!;)st*ZCd^Q4+@O(@@rWGtn#om%cyh`y8*G{|jWsv^ z^%+5KH0eM;gp|CepvCv#$cJC;Er2cOs!FTbAE;jCWgn505YI#v9*OY^ZF zVm$H%Y^*LEEv(;<947k(txQk?ihg!^c>eKUwPkQ}X?Dgw+MAkY_J5Ex0V3#f^KO!k zI{*A8=pR^hZ5R$PfG;$w%^z(Zme7$hQYOJy(kEw0UNn7jbyjVq7$f@eax462P-(L8 z$tLwd_B3r?S^tY(n6A)4Tngk@TuQSk){-6Jw%QD*|1jnA_~%*aePj9JHXQ|Aq9vuL zHBZ-_r#Ssd$a~VwM6C#EsKyuBfqXTMr;vT)9xXcF=|p)q=}(LCY~xu1G$uB_+~|y$ zhccvH#G={+cq3maU@F9!1keRi!l_P0inx6cl|EZZXWaTgQwkl>6aKt9oMw)F98aP0YM&7eS_aL&f0!z(nS)D<46;r5<)5X2-?JA; z!Dwzt<#4#uBRf(@-t7~*-{;rz`6NcQqAKQDRTC_H#{9{xku4AfvwGQi9-Z{>-PJDB zYhvPbI_)i&IS>=|e1nL3&Y=fp%{Fb__rWaV-{f%`4@=Dfy(~*2rpom_qSgYBQO4dU z&uxD>=ID5&!iu`iHkp?UYe+cR}n??c<}yr16!4$x}Jj`Aw@#}k>T!iUiSHg@yEo7$86x& zz{Dm+EdW>h2S2BNS&`sFEE+(nJy*}KhbT<$_fTAk7VEtKZpr)5GNVy5(bg@ltoa}; zfN$V1!v3PC78@yH`xprCP7sRvOcKkJ0yr(6bHy%@l z+-@=HJJ=PiE92jyDI4GQFi`)5EGYNxd0~Ms?9!TN%<@CUZ}gcCOqa~ZYA(sGIgoG?K`OxwKLM93Tokw%W;eMr>UY|=N8EC^0WIY!dPz-GzY0rIAJqh zHZl>mkYJq@RST3!xEII1VdbBZTZf6hXEs9qy9|fad2}%c&R^b4EtO{=OKHu2tsPxc zAAGOpTv-?L@9#lBW}lb-F%qbjnOySJ8YruMvscq-v01t4_G|ADuA9?Kj_M zyH$dzml9WrD5Vztd5o&RXO@ToE1C@;kb=gPVZ+kvJDFyi>;1?l7}Ho@VPT)>dgc?5 z{YnP*;S0YitagXh7`le_!_Ku--zGte>Xa8J>5|&of7A@xEFI<_GtKP|@j}+09yvT& z_OE}a7A4wPdZ9WfQw1S8cn<~by^w7C^vs_|d@x-z;j$X`0ZV#9;Wxrh<>&tf>=zR!F#zx<%@5Ud!JbUh zU4UM$`0|Sgb1DN@^}BRGWiPIQ_5|4{Im4{tX!ScdastcV%2_ub=lon=fF-w?O61<) z0mlO6Oxn&$t4k-z0}he^uj%yZ{WM~%|GxHSiB>!N*A*{M@pC^~5f^4$EQ}7Qc@^Q@ z-%l2Cy?(FiD|33ZJ&%EDd`chAs--fe_%XIFLjdOsES zSiAjMQZQ$6XnT?5CWz9O^OwYIrL!t}OL9!Tzh4&sx*mX=kMRV$3}0clXM1oIz$E$q zyhJ|xY!)q)i6fu-Tzu4|O{|pr;^4n)u$uK%Uj7m8{rZ_Cpv(prtPAEZ&ZOYKLhkzh zLbN_=i$V$ZZBvpQ_+75vAnkB+)A?z*1UPYWpEO?7Kg!8sHt63#7avuCw(MmNfz80z zU?OO*Gd-33JA4HYdbU%?vOLAP-t2K_#qHqnDv5KlOnSU0vubfh=6qx`;msk5)h_L! z#+qll8F4~>ehx*{*z)2P3bYKmo;$4aBt91{O1u(~49wy$QPc6UrPOzf!p{IJ{z&Z3 z=h$#m6HbVNY5St9cS+gcQ3T4>aZqG`fb`%2_DMR_P0gYncl$jG<&1^3Sc4O!k99BE z2pcx3tr~iCSv#(6x)s+K;eESQRidWhi%$kHBI1U88siR9LRn|Q{&&G%UMc+58Sv@=^;Yo_J@GyM%wKp`C* zkg_}yC5ZxI5L0G@OaJ4=%JVwj{oC!jeqauKZ0w^;{L{a0uy##=Cg% zv*(xfKZ+MF2_wSuQ^ijL z^>#B-2|=y-G*s)2xKpDanYax?_@LtGEsG5+TZ!7@2kH9kAqEi+h<0P)QzTQkI)AOY zN=ozdrIqh=MKYm6Rl?QP%Iw^-dm{y zW47>9tj{JXW1+7fD67?;>Jkl5=}A{kKLId_2lpSLbG;#LUXPob(6bwN%9Fi^4RLez z=cVQ;Np=j)C#NQlemvbKWRs+->P^FvnFs%BJ%Zl^o=^H8*m|j4;;r}h$3o-41<+fs z#)FAPtRIBW8E*UkIeXgwgE5#3LnfL7lIdXk#(UmYdv%b&Z4t$m;j77m_R`fkHS6mA zs6A-ksF5xcewy#CI7Brycve~~4l353KY_*>mK^sSE$nv%@q0^rQYV$15Mp5i*v5rD zYB@GTp^p!j$uFK_dGABrvai~*n7i=7AQzcW4z{p$v~H+XpvX`GXA3t4xn7d>V?H9% zfL6-)Q5=h+sY!HmqWgf|tY2a3Fo?~RR))wg5Wacf6RH<80`pL(9mt)2R~j+f|^T z%;8_K(T=#KsDQ(fK+{(tpD`$Nd3x3H;g;mVw%F->@#!jni^^*{@TnD!%(9R2=>m~4 zTKtl`!74k$OjFg8TxQ-0p>M75Rkq8%Q%X`3;>ihQdM%4F$F97^rc*L1luxu4Waww; zP#baft6z~|a2*qdee1P1hw)iIrWJPPt8c-W(SUPm^3}~9-sa&RkPapp2vt36_w1miU)x6}#p+0x-ePp;|P&dXr_!2`8`v2C11zl+{fR>pn~IVr3(ergg=g5b;{Wq2y*N8c`XUBUMuuT zY$QoGUG=)P7#xIYPL}fv3n~{}(}eih)^A&f{=S#_3__ zDz0M0dTm(xfhPBNJMUsR;IeaLlIue9$44NC--n4?Vjwga--XbEh@|8^BapkogN$w; z_lG?PYjO{zapCvAk}N{ia-G(dM`_s2Z_?~dCcJA}okfHRaX#=Hx3+Gml76hv*+eA~ zQNP+Vl6Ey()xuef|4@9CVtaeFV;B^r0d{rB*HMflpou`%#$W@+Q+>_Egy`Mf-Ph)} zbV;!~`l(PPXssIk9#Am?Z7`rm_GO{;G#56$VS}w2$(XqD(9CqUVq#aAehie(F~cZysZ1S;*Y>I+ryT zB0l-WbQdUHg0SunKy32`WA423>%kd)5RLn3A?X*R1$W|p>aJIcX>qR(N;{rf9MAc;j)^wD6+~-b7Z704tQF%wc{>x)3|*lHDpL{t-%M z7l`xyzT*imLRi@1LxF2c%qbSWH5R#fh=o&7<&|Ime&XME6}0>>&d`WYKfJ)quy53` z5N$#@5=!(;aE&H619G@hnB24iT7~Ee-`4`|lv?+b*rY3O5h%QB<>r8V_2MBZFes=@ zrVy{@G^-EruR~IQG_o6s-%A}TQQ18a9um=;t6!t}dj4&;gq|=@L%h+WL)O+`e)GYX z)O(aHNWv%_W742lu%`Cv`l&!WbpcbGtsxzzX6sGBLTNjw>Jv=LGix=kVJ7HQli%w1X}h7z$DPe|AUUKyA%6f zc-cAJ8sS&y4JGqzeUdSFbc>`(JwG-wDk-h#oq~9>lt?=^q07pIp%X$%%KL4=)%pp6 zbW9iLAkCCHgG9c8X0<*>HvZ9O_Vc9fQI^hXe%sw!ui};$hElu1;YV=Q`21F=mTo1l zqtLrIcHsA@t|`-ug7)F0_^KV|Zy7Tu+aX%{RWj5j=Uz6l?q9Vki`6SRzl<}n3v3DM zU@C|;=tpRdQOOVE<^t2_%?euY`g1*!BacECY~c?uC<( zr`U07!N$bPb$Uq?gmMXQ5r8jcKr>fObS@(73;N`^$4|6i75t4aG%7?HXSfH(iYM+W z4O~`LvkllB)_-U+Exc(ktqgrby6%#aJ-HKqw6cZW4Z2tj>u)}wY}L2K0{l-1MDs1e zYc;rsERVZ@=CF3TQQ_dxR~11Qu&|-DIWF93T}xnG8g<+r>-O?#<>Q(x#K??*Lf@o; zLpyG+poN(IuMl`bvVn7fi=up#oPu0h!3!;l^~U!o9ubXq$?Dt>O0JYneuM`eY#~t9 zaG$evb=k6MZyAgoCAYCa`Abc!if|@PJ z(m%VjT7P4lzON8E0=C2jJQ*-efd@mN_};}Z_rfXU2*Wby&z80TObC~PS&Sbh)Q}77 z#k(~X@6oPws74ptYg5>uEBL-)BWNrQiz9DVeXEcHv`Be3m5Vc2E+3ZKVtG4E|8|HL zQ@gt2jPW8EHKRxswf=fDkmSG#S5|*seos`n>JdvJ`7Rzt+kAwij3i(kPa-k*`FSfIIPy(uABJd z`B>LTXY=;Tx5SWuUND-kHxqE33$!)Ia&)%A&fo&t3x-&~3AaCH79TBuz7(v$2r_WL z;~J8257veY+r9&TuC;szK_Es}S_PDhf^0iM&YSq6Pl8{zieuJ6`MbI-UDxfS;?w&* zbT@O7G9{@O$S+>%9#DtT;O1g;oQ)W==$0`CdlgJ} zIn$8uj<6bCyF+>H1ca_!);|ji-m2!@7I1RXKO=Sc8MCHRq$VS6lanJ6d=?YKW06$S z_7OLPHy&hE!(NqR7g|IeFEv-;2M_<94&-P7LP$KhgXyX$PFAGN71j6@M4?_6k8xJt zdgx-bCvY~sy=dZ*%jD(KVPKncAuy%yFZ}mGvMF0o^KS=PYnR{)yXd^eMb${j)~u?7zA*8C>d>VY_#;oU~97p4Dr=@ZH*DGc-Rr+JtsKqG{r|HY|yCwdMg_ zPtXI~)L}qXzN;t|3`dLOFOlvqs**IRau zTV`A^a$S}&v%c8Iczcn0p@u7&4mg3H+(vA{KY1&l1hHx!XePsr46G3o{Tz`8ojESm zj&{DbnN8{46MFXx0?{39eU+tB1$q6C-q%?p9i}5twle{+J1I{0G;wonY)B%3oEX+E zExaeRZVgq8L`pN0NN%-vpVM#T$g8YZc{2Kv5vp-#^&v*7y1Oo?%_R3B96!|K;N zyh*^>jv0n^n+6OPS!J~})d|#R%G~VO`7<;GA*@}15)F@yXz~{*HcA{`aR_8>HNqP4 z{oU``ra|}p(exzN=8)hk z&tlu+uJ5j>0v&Ce>hJuH><)c(& z^z2Vh9Jbeku`j8q#*z5&I|xVHnrj(fxpm#wIiKH#Tw4bK!eaU;h~wH|igmT&Vx6a3 zQI3%ShTb(v2R*8@bA+x-sZUoqNBWW=G3b-Wc5_ zL8EJSjX+?KJ9setJ>Oezq3O|rJjUVPZZA%r{^8dG`#7^S*CNZQhB=3u>9cNRd~u{U zJUwV{#_-#0dy$^S*cOi4Q+qnjF5>?WeYRO zGKl5ZANmc{(VXtwO{A|hTRaYN`EiZofF!5 ztn`_VR92hb7@Hn2vu)vS7cnp(gRow%%eCArINZeeD^D%7|M$Cqb$ z@Q5Vxeaz>1e+em8G}o-i`}&@`GSgV2gmwCn5q;`PLZYaCkEh?|@V&Bxqu)b@F3_>1 zs}VNhrsqtIz+k5d>~@?;UwPKvO=H@jpW3Gtm({03<)K>h*T(|h3*jy33L~=3NuKQg+gJ$D8`isN^ilCz~3d92@|5w)xjN*Vd|r1LpxUN*p^cyF3K z>|WKwN(A?ftDcyo3q_{XvD3A6Fzg(1OCrEttP~W~%FL4N69l(0D;(XSeJmnk0OEB3 z7EZYq;6-BhmTr?7UIbwECwAE&k6fsQ6cmMHgk}rt!aA7>o%h|zty^_+_rHG z6n;;T;`Kc?vAn4pMYlvtRPG>Ae)aY$nVZr!P-x24YlfREm)spG%?9{Xy|i3fNNCf& zSb~5EZzm(CC0%hMMh29cAL~;C#|?s;SC&d|Rw&NxXl$(l^2*LUE6R$OuN?R1O-aul z&%#qJnj3r~k@_Z$1NBDL$DiI059j|ilC$#hanPL>8K@C~E^hp11lboca;hmCsm>Bb zi}*FII8U)kyA{2dtD7+QAtA-f(w`LZ%g9?@>F!7F;Wl0mbBLInE9}QLtIb+>6wq|Z zzw?4rqP)MbFpxI(R#sjr|8g~TFP8N2!ZK!65?I*sNt^j->>!?p_?|1|3un;F}SZNJnhi?o!sTmo|HI;`t^jY zoLC{V$7Q(KkJ<-hSEF*(f9R;+@|CmGP*XeLj>*}b&P4OS@^!nDmF~x$_N4`ot5W-+yvGy=QYh3pUwZwgg3u zeb}{mgMgwGvR?0Gjl7{vHl|y|ADS0Ut)_tym69@fP=bqkDpI?I}(r$6U&Yka>{oKRQ=>l)l@o;#Ky1JFwKp}?i1&!)d zk5;}7y7<(UY|7nkqC^r?#CcdXF+jd5JpTaN`#m{}D!?900uYMO9{*iVQ7QGIvUzegl26}xdpx_?d`vU>G&0)JG$C&z~0qbmdsDbm56fZ-M}30j~0 zr?yypdDgr-WY%`!t*(+3Bs^yu+O<^l>6_}e=WZLPeeCs__1&t|rBI*C*%Qh%heOir2}Se+m|EQsJMR_OBG){7VcUTdYO`!c&42qEN3_^2 zTq0Oqx=q;0clGswYeQRQUlQlyne54WDx;Wyp40qRo(0 z>oGIG6zfvoc%#v4QGD6$Bs@C*f1XNITs&o!?4 z(@l_GWDqU)X(w5T-AG`?cpb(9=&p|L_VgYG%HFXcADrB4GsbC8u6V+YzZ3(Pl=j#3 z_iUSyQ&gBbU?TmT5k}v!Tc4fQ=AQjeGnhrKmCnD;S@_)8N+`bkg~Q2r<7n;+@y766 z=6}%>b)z(u@H_$EB)wpeDH03X zzwfenDH7}hk1l`D!%b!bYM{s{L>zAgEk}!hc5`DkTvfBL-SLj-)wXw=8q&7_ zdwAm;nnf1~0r4QLp;j*CGG9cjQhwPb(+Y+bYVvq`QnljU+e4xwaVWUPYLrGYYmO6 z6O|VqjRE=tlQrJTEzF>F0NJ)G=v9>jbkuG}4~>7Sxla zs9ApFC-F-pJKZIV4nNw91VA%Nz!P{)zu~Ru1OmGv0Nl!QgPEK07(467BC%iZp7ft# zh89*S$#i2rt;Q&8B(Pz%PY1;*8PZ;Ttb2YBG>(b1yNbpwIa?f>jR<_eh{w;I;&$MB zTaP~nPR3{p8L4p?3|Q?B4h}LW2ePFXy-e!m6bL^=3a0@L2%%9`Elg$QhrB~fA{MMkD0H8eqtP0;5vW4 z>;~^=f9=^P*$DkU4pbLT5iLkLj33qB*~T)^tBzZakt<>b-^k3wBrt=01D zd!J%ON0t|&bztOJA#r4JPvXF_QrXxzoy+u#W{l_>6&cm|-I3d5tj~UIxHvAghXPkD z(~4oJVrfC4*>Ez$=ZJ#33Vxc#P>FJZ#&wzavloWA`Js0a1-jO^aijTRwZk!?AgT`D z3J(l>!E6l!hgVioiUTn9?%rh1f9CtKFst_0FM|8Dn4#S!oW8COYw zFZ-I4Ul{uM$qr~6mH+{B@F?FVCWOGZO!Z3B$@cBi-nlTlury zyQn|o&(6jCVl`}vmA8Vs)fhqYu?!`cFe zkPGl3S>l9*8zC-+LQjtg?=;~lcsG{WzIbaDs5NN7mv-R8GmOx8l$$y|2 z2_QB9L1kk86BNKmaj8fFJ1D@N%z2HfVfY!Iv!HjeiczJob?BFQUJ|u8g+vS@P&$@l zB|b9}@wi^?BxPl)rA`g>hR7u4KcDESP&BIeIju!d*9Ym_WG|BVjje`2SoZ2ZAy&vs ziZ~58F&qT-z=Tjt<_)M{?hEi<_QGy3w$eciy-h`5I-QHP#R`G^2aF&yjvOuH@#d*_ zLL?m#9OB*u0EW(?8%8{X*Hgkzb4!0X1ED3T56^;DMlv+2elom64L;*xTrQ@QgggY; zwg#Y1qX8e$a~8Drox2`T;cpchB~k%vJTs<|lXEp?C|}rGF`wo1`m@;TZceVa@;)NT zak&hkWyXVb0>U(f?e^*~e3^LJd01-;(Ub(QI3zF31hv6uT4=BFt%hRoIQNVJI|D_O z`7eMf98|t1P>!AGS=$GZwN(_glTA;Z z^PvwwOJHZlSxtMX>cfdP( z;S-AWsx#f(r4XpmC$`<{`J^wq?BShGPUDIAG_*|OFyb7#-f!+Is7PJRTz zvuh7ILga803W;V^gX3G4(&Xyve=5hxeMHl(QdGq6$0TF6*+V4{qqG~P$(=aBFp`*R z@mAQZ!Fk04@ZWkt4%ixW%{)5rDW5f=hk$Oj5O`{)tf7&LVYf+L{{99%-Nl6bu|HRW z^ak*ZL|bzNhE!9wys)VZckHs)j&np5Xu`;&lgykz;GO|1WSBfI_nu1$W{`>4%?H5x z6K$9Qv0PL4PKxn8e}Kgx!(4@~6XIbwJX*_%BhdUKh)*XoL}!|T)~b+3&;NIkD}X1E z0u*c1!YSA^S~H+Gt2w&|7w0Xgn8}GqO1fz+o)ehD-%!3DU`TAgPRX*+g4HV9S7^ME zY9df5U#%)Q#|4*jFV1ZMY-9pp9l0klZ_KI;e3R43I&6e+f65|%*BM9Ao9>vPQVP1Q zfz&dZ+=kE!e&hRigf!c_=U)@rt&RNO14t6qn}T5pA`oX7&tv}HUFj1OZ^fIDZu;j$ z0vxQdpeuN{6yf{y**XG1_oy%!Sq8gMTHEf zS>1-~``f#*V)X8|~5Am?Y7r2i+pzets zu+J=``Ji9(q=}7#d+3}M(6+$GbZfn{4gUbfzCZviKp6^6z>pXTNEw`6GXSMU0(2f( zElXf?`0C(=T0$v1JPw@A@6G{Oim_BXm0HHvE}V6C5nlg~xVMgqs(=5z5j(IILCOFm zrIE$}q@_EQ98zLX1`sholu%MyrE8FGFzKOZ1{gx68A5@f<6Im4{?6~5^T%1wv!1n{ zS?j)aAya#Q_WoS)zFzNO4H`D~I{D_-_#0wd z#B%4?hvex5e)>_9IygAC4AictKYUiBd0F@Udn0fQQq_~BTS;nSqo>6dS_G{cUSF^d z3c*jqmUrCS=7^m$b@D3_o7EXHfmq=;V;781xyja#A)_1yx+nH5DflNuekZ`Fr`kkU z-PN|;&)kAFxSk60`ePU2e1`$L2HbvZTGiq3DM3t(0G=EXm;{}&IRMDK%6>G2xO&eK zG>r`t-%K3CowC>T4&g=C)EdnV;>#+pe)(m5O-C_@}njag0!D}eP!hTxFf;v z#g|ve<>F_UEe0P3uvV*<#LPeS3~oW*!0Hl!o4#GCI^eO=RAMe#)CIly`8D7lzo;ij z#Ivi>+5;Jo3kus=Ie%8Q*I1RK-fHQ9OAQv$#MFf_a?u*!_bcSu1jPOb1f7vE?heXY zH{{mQS^2=(>?$&}{8;`D<_7s;AJ4x)BfM>Qa-Zzv=l#q%E-fjF;Vq4L@R=HU$%TCl zqkBm^LygDNgt)p&{9x2{H`Q^?|4kc;jFqtG4#$|>x#)2N_+nHykeB>-{lfC)vgfS+ z`jbzKJo6Y7j#SUs`Z$s4%#Sn@9%<>TK+Ad`_;+%0eXqQuwoSy&wafMwh3fD%3V;~X zI=Q~chP*#%8lFhypkvVuE#Oy5N;ppkZS9Y=aIaZ84pKgNl-z9&>Ue3D9|dHcXZxgk zk@4mpc?b81P3q(`z1chI0?*&M-ySE}Mh^}iZ9wi`LVsax=I3>vUh=}9h$XfV8}^``?FyPyF{dh5cp3eWb|_ zNguEas=Fn3+tTvZ|M_V<1?aEtqB{QMKff-qPyX<$X4|SgRI&#^?*?!G$-}SXOPFT9 zPTiH@=*RLih>6q)oSb7z_ozx1pmEp;bS+k6x&2-{)77-ZrE20gwQwr)U7E7j;rPI`B7zlYEw98Xq!3 zhRWdb?S!Vb_GLuYh7hGA?DMKiG|jVlE$jt=RlP8G@yb+U%^)Bx7VS&`QPK<^PewCq z2qqShmjU(l&$@n@0&=sUdhEzRN{-y9amr)3!j1ZEA;xg@Pb3VkuT1At?J)&^GoP)u z5OlilqZzPzgt__jUGZL`&_~1yUJ04V4H9g!FFvrEo5TuKELpa8_uz2$D{ zVENQ=kUgFSAi4!!%T5T~*AmR#It#3n-FyIfms;PR`AM8rf70C@_oVC7Ab7m7a&k7s zR9|!K6B2{0GX>B{t?O9h@=nX!A+Xnkh)-qE+XqbvFnLYqrRdz)i4JT7*0p zhgI3k-wAL~6qsnVr!X#On3a;s`aS|8cteJZ!R4~Yhlc*x`3;8`BS%ZSLg@whfjNh3VM7};)B z(SgfwxuW;Y2=RTmUJ4i*u{ks0Y}Ka`vBDYGuhPUKu5^|s?RZ)hn0>k}=TNwIxN^IK z5NDbAnCIwXAi#KnKU#cq&-F7zXAI??%w6ZYChCAJoxo5F4yg$BjkkFC8qX{Jy& zIZc>wxA0U+rHkb`LH9OGJ&kO-O{lb<=I9h^?G__YeBR}~k3{8MpM_+ne@_)<^F6ti z*8&~aK0S&^Ts_TbF4|k?4g7~jN=c*h)4ii&;CFH6vD4n~5yE)gbe+mhZq)?WY_vF~ zz`7E}*G8Z1$DitVj^+u9Px<^oVC<`7Yqu~c7>0iuHGrn0483nQLDOyi{P2p`Rc%;T zT+TRY9k>&(ks^@hamC%m^!;6}^Ys1Q6hQ>EavOhiN!+nQ+CpmYNnuhKG@b{{d}+RM zCdAGu32LF~gkR`If6BO;V_My(!cQdX?{55jjy(18>(OlON7v(=P^c%zWH#C@o2P_U zDuVDt6T$n!6FHxKH&4jotzxL3BiruINsbkDHl1{)d14!-k0%RX`GE5;l24Y~eK&S* zMlo}7(@mwQ<&t)z zpg`vIwAhb<%!$WU;5uVZudPy!Fd4pi29wT@kZ6XSF2p=K0N_wcRs&LFwoM=N}g=CdSFr~+i9M; z?|89DvYfq}-Pu&@Yq};L3I2IB_o0k6C0UXNBK}wmW|N*oc9zn8^ypDB$=yW1)TlQ| z7pI|Db0mWH^y#;g6S3FHv4M0A`8uPO2BHMCPN8HqUG}c~Jd^K=-SWUtY@x!geF-i| zSAt#f;eQNwmMb}rmGIm}5Tunk9#2NIl1iPgBF0xSt=SYdVV2XJMX2B{FJrQkUG;5% zA8pwUQ^aX*81$y2>Bu40izqinoB&NEaVYjnURO)@J_+HEb^}iB7b`wY;~#q#0=Z9*Cy++jI8)+M74=RxIJ>+791v{JE$bZI4o&l<2X@Z0z& z6Yh1Bp|(gF%F!A?bFP6Bb-2p3P)7KE28G^4@Urmn5{F&^#M4i(#{jO^0y*JNspy+G zM=3=5nIfCx{e z=+Kv8x1)v7p~KpiDYtY_<8l}DH(49H+@HNT5a{k%h)3mYiEDq&6MA#-Amw~E^PSXG zX}a?rDS;ff9qR0YBf?G&1qFvjzj!%$FyzBUmlZn)(HTBp1)r*>r4(>$;A>yGtT&EOhY7GfiIkyy)Q-ldWj|aq| z4pZoNfZzw%mrk4^@vXm@nzLFuF*u{X!laS6d2r|wCt>%{rK~xx6(*P8KHu`??}Q2) zr^{D{;1^=0Y26)`mu5zv{TUu@ZVb0tQ4!4$ChX$lg%9z3E)Oj;(C6FYuVUY?Fww3;y$B z23!z;2W#1*p`aKfo!DHFxi(-@I@Zo7PQ=|FDiRYD>nyZUU&y}kt{$T-K{|sqxMb$xVRlW-W#qu* z?>_t1TD?W)AN=pa_#S?A_Z~OTAJb7_+ILwK&|n9X|51^>OdeBQ7K7t?c@IM<&l(u?df2%i{%oDjdq!=tC`x2E0O35+5X% z$FHw5^tyM*G1aHRc@OjGwHkBX`t`lwXG@`Eh@;B@x^v#2j2r!e&$b#+WcOl-1IA?M zR7q`Ivz$>$lVP;km2A7MXtz`f*grP2dM!6iB}=Klx-CZ0My>ZGj}<&>&tc$eAMQyT zu2-%cPegB3Z}7F%9w{web1&Vt#g<{OwswCpN-DR z&qjq^K7B@6Gjim~M6_90(W6b3`^^;nAG9XJz=wREpGc!-X@~IUdy0SB8gOt|`-xJ$qI>RJe%AW@Ka4^+#)y#WJhV ziGW*g$FnB&FI{DmcQp?4>*TG}4hxmQZiMP&v(EXSk1zERy8rH5WJCkUtFtIX)I3K(q*sA8j6OTWF1*D9}{oypkhA(?ii8{xlvZREK1-rsO>eolR3Kg`A>Y zN(|FPjh$3>p$%<-4pG7IaKbh=cIkwd_4brUPNmN`EVJ+8wUQE_Z)(xRj{~7ymq|bQ z@{E?W)OAP-ReWpb=svn6A2F#^6R6oTbp18l(i2Z>=2{tgrDGZGUFE(>JRdfgmy{`C zl=SeMBAKXYT&$44u~ZCGA4$ zvMIJVljHOD8nJdmxv38T#~M8Iz~ccF@mCYoCW2&*p!u zR}Q3r2Gc}dXOZUMnF~4=?toUE2a`HaVGr=m?u5JwBNZTDFNJSwCv8fVlpLt2w8Cb- zLCPBNxN|DE`Mli?K(vDaO%r01hVO9(pT}0e4loVVIuAuaHoQ~ep&QH@S1wQgL)TJK z(etKICnFr3X5Pw{?0iUi@)m~cp(v!S9dXc7S4UstO79`3HG2nDNxb3j2*tl)JJqx7 z*N1|b5aZPmbvin_v=f>-5+2ih<5+`?|7mq|*S1Dm0Z9M=oZJ)Bh+5rT9!mHRgcdOkUwy#|08ba&z-+N>l8_XZstw@ zBX2;&vc8djFz5@HZb@c(>$2>XdzA{;;J>oFaXWwh`}^`y@E7=dW}h+s-yNj>Edtq| z_rUPqd4R9fKf4Pm#A52de&Iiv7uEl7RvrIOzRv##pVs!R7@%HQrKJZwm7u>L6m&%F z&$m;fV94-i%?14J>}Gwx(ulq=fl?qsHv>N;D8eZglOU`2TyUNM3KD632(VUTOURAg zbKrymg9d6-7(B$0xy+jZM%>2#PF*3f7a@Q9FPqB-{E`I~wa4NWf&bRq)31&1S zD|XNG-FFc+0LFd%&*HCFU{QQ6PSjasIIm)gGaw-WGnHvA23FgpcBM!--|D*zI*6L>s@s~4*WEMi(+J~ka7(Bqq?m`PRz6&01>VP?o(Yjg4tAsbY> z8N=CBv)p|EPPDqb^0cykX?y{{7|)*I9(L(Y4cr#!&4%Gic9ZMHhl2#O2DYKNiv6<* z2g<0l%?m)pt5#xw8*f>GuY}p0J4E}}m2kLu`4&{hrKAB!9zVP!tuC`!bP2HNGd{*L zH^*8NjL6cPndymk+H-+0?Ld6<_U%MZjgQw(D}d_26#@CL-5=y$U6zp)}ttwV_RIo>W>P_tH(;MLsd}u9;9p)_#pPw=v;onBp~BNOC1q|ElR9d3Q`{}2V+c?u}RPI z1oj;abYHQufS@4GehIQ8bp&P~7#vJ#UaJ6tiRxH8h`!8tk~4VezSE4(jk_(cR1BWBg8|lg3!wm0lZ1b!+ zi-U57%cRz}7IWnyKG4E}x;X_x26X@u|_A@>FWI&t&bhrTJ zh;TO6rY&(kX&d~-vVQ}Hv?72pzBE$QSoMS;?;;C!V!W~CcbG4RXSJbNrjpRw` zRgC?qCG6F(Mryi7kq6eI7;!v16j5!?4pfe2B!`5-=`*FCfmN7ipfGDUtdq+#VR&9$ z(C})-#6>NVCb`^>kp#>}AzK4GObsqhG|n))LeHKbuc&x~f?)VnM4)H+GM`i^hChFLC z*(S-S#FM5tB-zPLr<9IJ?2r311;)7Hr_`=p7x$RHl9`#wRyYUd2m*82{%de?;Yw21 zg$8Phu)kr;Fvwg=0abFOffKGNx2Zx^aA7ll=K1`3o!;C*+)LdxaYLav_tto^F+nw= zvJYxgbAXArda+Y{h0GeR(!y|iZQBlEzZn35Z5E0ttQ1nr((+k>TEn>ZqixOhdMV_x-=VQ||j z5}o$9!d+|hfP0=j8APciDZUflf!-OZ_jGv}(oT3hkNAkZe;m}ohX>hIxaWsrPT{UOsFsTm?Mu=<3`=cA zO>wdD0E@GRh^zU?##y!JUIO?R5{0RHB z>zTY9QZasO&Ig5ZH7X#6MOu1Oi-r%C$!whJq)h8bRPJ^A1h{OVsjN_loAA01Ezq>6 zl-a9r*hfIXoU{CJU``)gG%l47VoAw_m~QL{1&E<&T1qu`UD3xO_e2hwPuNSNw6MZM z<=A2gv1dq>##4QwR?AFc^GN4fi@4GCWeNueM=O6UIN*@9B#8F=a-3w=%991@!DY(Q zE`e~%MilWe;4S+>JwE-aZYdG$h${`$KQ~1nK<+0Od)^rAspay45P$)RWQLInVDWSt zuSrE2u04~(Lc8#yva&K)a4q;qw>bK4lFhQEqND??$I3ce7#U@Jw>IpCp>Tb7s>}pJ zDbvk3#E@h`KUOR&ir2($YYPIKVTNS7ThXSN%yLPKx+8Au$c{OaZ#Sis1pW~Ze}CSu zT1{B93^Gjke%|nAp0ul*8_tax+L}(z%MpPD2m+L1^pwm5t7l;6Sez;+hpndNg_~mg z6b+r-h;&ARweCil6*QGzt!v#%-SxzJS1DA2u1`)Dk(^o5QV3R`>2(fjW|l!yB3Y%i z(3N*`p2KHHmsh8WKcyI`k%haDxceNd>?A5I?_M*svbHAJLzM-+0j*w!8h40qJf~l5 z!A=@O63YGvsWVOQ68*w@8ZZ4?;%#N+?4l$>3^i!Iol^M;D*mwB)@NuWGeD=a(qe9~ zIs|kAZ)>YpMb+N&eGYQN^*9901*#>8$FvE+;b$>^%}+mOGAS&fNp9JM)=ip|fewgY zR1yT;g~D%3fWx$SvJ$xmfSYYXU)%BEN0QJRAN%@CFzk5 znZXiC+g(o3qLeV@vm4TCBOxqg)!441V;QGDRtwQbf z_>e#1;h*o#d|g}^h3!(*Ak~n|aM^jSw8ln=Tmp&gjGJw>dhTduAsq(t-;AMC6}&=) zNXmAAc8~nm2(|a6X`RGx#n3VypVxf!{s|d=k)}eg!s;47mKs)J>FL`pUrvq z9G_pwM$aup&M6!LgX^&E_1f)lBv{x`-Tk{FXe}P35g8GRw)p48gU7b?TOmvQy63Hs ztwqPXaHr*2yJ><|RgHh_XgYi9`ZMr&YIL6?OxJ_BpWx}4TTc}KP+{h3g+Gg?c}R>{ zoiQ9QdrwL<=|=Y!FS+AoGsro6d~4Zr+71$P8Xy~s%ej;5lbgg^AyYJQ%ylg}=22pq z)6=t?rMBkUPxC-Hrk3ohK5^v0<_2<YlJl)gVcJ;5XPpsrn7hWU&uOJwVSV3_U5`-o(2@ItpK|t}Fu8OR+3yd`vsfH`W*;kW7P)V)(Btgh8<=*3 zq|6AVV|R)XS7OzeO%Oe=O8@;2S~kM3Tp8HbA5dFhb)?ojmHnm@()rn?rP$c3<&|xS*SXXB z*7{ubKpIZ6=&q_xU)8r_l)p&b{Y}8_Y?VfD+XW=2qog7~g(7x-WATu3Xa`TynWmed? z0u973Gy2(?#`rO1Bs-t+f$aPl#d)Div7lEW0v; zJ)7W!w^r9_mY=vrmadpqD*T#dYSWQf(#JOqo!19ysl>@2a26t}7CV(`Oj~1-f(^$g z43Y`QVECbBEtNaRn9ylF^amn)|9+q40i^E`ejRH8NvhMJy7>2=;zj>-%0we93x|TD z;=teb(6S8S#2%ns4%Bs&ah>F$84NdQ@6_+xtuZ%nRPOGC>*Jivm!G5#l zS05aI4~n|e?b3e6}UJLQMGp;J1{rP!*~sy}0b$^2F95s@5^#uFY8CRgDy zdR!L*G9*qkH_`giF{#Bn$uUjmd5ipBTB8R%NRFRPBjkfA=fofK_*`OVR{*Ixs%u)Q zSsK19)z4=^8dQ;0pq;VC*xnR$|7j%UwFF$0D#OC1PnaXLRqWn3$uFG-Dk6OlYxyrH zk#cy6cC=x!?6n_K$Q8VD_Mhm5it27ac>(gG@c44{zy0*D6cs+^|GfbI;b}G1?^-|- zDmyu0!Kl^`|JTLyvHT@LK`oF8UJPefhr&WQKm9(TD~86#4AbBLz9ZEX1JMv%V^(&9 zC)=l{)1Ws+{jER2zyCG_&*yJuh#Gn4@&DEo`xN8!URm>N3`ZNeTHU;iv@IjP$$b~w zUJ_E1lfR|p?e1M&Hx@k-H+rA_;>8=Ww?s3EJx{`@Iou*VjYFQj1)T;DH%~-ofo4(= zeiT2dE0-##sj2%bJip=*S}OZj=RFQyuBAr(0Vem#jD?~p;x<&d4;!S>Z#|C}w#Oxm_q&HWia6jj>^q2Ly6Buv%?~2~RpE6#IjEppZ`uVDv z?`Cn1Nu{o)mKHKcFb@?)6R5c8IHrz*kF}P%6e|cWQ_Gzp7sd5l3Y8Qvw;~zq%9CxS##ekB#;2 zHuXrya#DZDOl+r)v<$^h^`V*S2hrb5Txai&$2KoKz(p&kn@+h*^Xzthql_`T$9r|C(ztcAV&l=Qan~|^ zdC5@m*n9;TX!ebCNrqIH5x={V-x=XXXUa*q^@i}mV^T)5V)IOH21E9jWEdu0rW;Mv zqXGh?9C*oIJzZX=jHEyMp4Q!;iN$I8)0dj^L$AH?xh1v)E5$fJ)SOx4%sO;!r!Vyn zdx6V+h;Y`h*Dc)ofnXHck)X5Tm$`Tm2nPp1=IwXaptH$UFNFdeD~jX!6Hf#bLO8+Lm(5mpY!+6-%?K=?n&KDF-_gvM`5(Q zcTYLGB8jK}_#XMghVH5ieb(q5#t0=S)9=ZM5{zOXbneSEHNO-9(6< zG_2p`6}wh!5qmLYOpcKb)jE9d;;4VefaT)dTz&G+Gg&fV_~kyiRh_ok=5UG9tCVYE zU`-SF;<(GTTOQ-5o1%O?HPd{x-|LOiMk}dZ52Vx6^?3o{FD0?DcK5*c{X0J4R0mE% z_q%`1$0Y5R{0o(Gm$xbG^+Zn|=+<&@xZOq>eD}<}+3GVaYEU9&6y?+$1qOmSo;&Y< zT*n-q(c2Ka@i+IdKQ@;wcCs4EPTm2f^>#T*8e6gPb1;NK8Q`d*==R zp?9&2>xWx}R)WSJ_CW*RC2?fRo#md8qRte)8zi1oI?d#MiFb+_qH+o1O4QvabV^3hs``xGDl?%NBO{JN83GMGccikL?iW*zpXM=q=#kk z(&l+7Y;{d*Qso793L)OtoC`ECeYINW@8J&acxN;B}56_h`$tn4Yg^DJ3ekka1?B&_!5*UEzH zg`%;$wk$!-7;I0qCjTC~cgSxv!WEVxEZky1s;6w(4ycatZ{EQi>oE!#dd%$p(5W~MSGY`)`ey(E|hG`WhCq{A# zTf@S_P6obumF;c>eXJ1#SphAkIDRT>8%is~s#C98Z$u1xHHV;C#G z&V6oh00tP_lGM#lA9xLpB0|?Y+yaWC-vp#IKT%yzK`T|o`&ug4oV8)uoeWg}ozHpU z%sCTiGVR#z)$==&N`%L~mZlGa|KDB5%|zoE;HY>5h7(obzO7gd-t0M4oyn(dN}dQ3Uok6uxpEZmE{{Q2DH#3&Yg7T4P z&LPe{R!DQW;%e>W#p=m_Q$8%R)Fh5^S z^tDK7>^*~*T*we5JU$J<(+rSRCIj^iDXYyM&K^8qq^oNkl$+KW{+}<##q#D& zXQ!rHVtDwlOORTj9z-N3ZLO@V1fU&)hz*ea>?9*251jeL`aj4ufY;GegdH;#)s^P@ z?Sw)DOi?kl=gVHP+L=v9-hYpw?8QsaYZM#@s>z>g*5HmcR^>L{deRp9o#EiIu8w#) zBZ1q&5xbAwj5PhwJTyegPphDTfX*Qg;s_?Tx!5%dG25XA{ZF)X$u0o8*qP>pH#iXS z_?A3E3Q<&1i2$WY69jNs16y;x+ob*|Sh2`7uB-%@tS8VV_Z`S$LprA$uq@`G%G?Nt zlNb6@uFySce0K2E2%?4%lprKo_}y`=(Y}qqJm7JhWmjO3ZRsi^9yhK5F^_5hhK8zU zbu4bIz%+dpdMb!&r>7Tj#s-y~fMvsLC8E8@ICN%4#;rMWVTUC=SG;vyJIQxxU7uxy zI$5{wipjs*>Ek`z0|IE*klQGYx|PW3dJF5Isx6SMY9tC zXi?ecphH0S$u=fg*mq{@T`eW%K@x_v1<9Kym~zAuJ%Br3VDNWM-vCUH7*yX?JZvq} zcA2OXQSuHfb<`9cTOhY+)Y+=Yy^b1F-Z?}pmb;UK4J<8FP%lANq%$LeThT_LP>&dm z!a>g=ICq8 zV94Ut149;mukC9_a`=_3g?65NApg?@k_BvUjLXIvb+KVs@j0D=7B3<9&rUg227FW2aM6K_;4S0#YIm5RRVe30J{q)3JGB($%&~c>I3W6|G1nV6o@_v=Z)e55zHk$nHD`Ob3PoSN zt@-F$ZH%#R@LqcHNK-G2oPw_N(iv_Dygt0rGXA2C*?GU?H~chcB(oqR)||e=&#!Ul z&>_h|CPoua;bNx&Lu7g|kBMYv;pY#Slqt+Jc?HK?h4Q&5F!@0q7fzg$LfR>wFcRFO zga5&PywgwPylQf(?Z=X`5Bz`CgK)RLERN(<%3v@y0_`)jbtv_LkT0DAfrS?4LgJm= zfA8wDEPF1CpJZmfiVVh;dn3pbP;EkLaqYM|&8O#tc6^YA1Ulsb?i}8#aBAc4J$6M) z_QNIQa$h79)!_nRLv91m&rpWYJ<~;>l47xIMM2TPv%MokYtn)^c}U=-SLPw&xQYd@XArAZe>4{9AKW zORqwxe#d>#TnAVMw+7ckq}~Jo2=$$xpXZvetQove0vue~ ziF8qkiB4)%8`1pqw2njc|9ubV;o*sdp0qY(6LQFqZ@mjBon#Uw{q8%|XrIU_MtD8Q z0}g&QEUyuq=ukvT%4I|@?$B4Hh}d_4%?%;X>?-154|AG|2!W^=w$ps{0xOqA(YF(T z*V%cAyDTgnZ$N_D1QI3IKY#uRI1fr9quDJKgdPCpOEo6g_j;?nO5+jy5Qv$9Lh$&b zC3#G_HK?Q0{OaE*;K1%(fgxZ-I$-G!{od`V61Je91 z@Si|n&kZ}=7#p12JmfY?<|PJwaQNg&u+o<)a1V(8+Lv;Vk1F8Y5x+wR|*PUA5jkV#3w@xy1i z?}w~HQx#V(a+mrCvu-9gmI*_Z$U(`m59%ve4Z}x*p~ub(uxJa&<6+#^QY{K>HuOM3!VXo$s4rh`&ztaG%ktQ2x!2D*R7eZTW=Uy*$O1D^??l@rbN{m z-wTTCJ9o_0D9FIK8r7E_6Y9UOJ(hl^61NIWbnr;6U#T4ulaFPuD<{EZsQ*?Zud|@q6^vNa5HVryjFcKpfjg{ z)1!)yl-nIIx_%Dfo>%+co#5i&370b}xD4nl#9=09(DRe$k!o;3kjXsjn8E`K_E?2}B6gNlR)cm$i(NUWhv(!}KC6jzOa$=8`>!Fu8+Q$7g#&%O_ zMe3@2yG$h1wvYLPf%$y9UbCe2p4$6P+mFFuk;>M%M!!tQI)^hUWO5`!nwOVN3Cld7 z3ILRYJq6F7(0IAxR;|8@H@~=P*c2y|7Y%kI&Fi(!VXp!_jxPP$Wt%eQ%&8ti*Z$r! zmAqc+Y~4!hgpHTnJqN?^2QGPF`Lk*h$MHtwc(dsBiS@`Q^QylTZ{N$nAN`U9#`|T{6s7%gpF?K43m>#;was$e z+a#&aard-hUea*Cd-37P?eIb68&?lLi=vub^-UCwu?nANi+k;0{lIr9lNH!S_b15>-dD zttv-+50_SY2kjGNOX!M>ru)6i*J1E#@t2z_Y1Xe32ksY$3Ez}9E4bMy=*B8^?e>)X zbLHHRr&xuAK3mC>F--VVs;XBdmkRX*`K01#+s@I@9E_S(B{zJ?-GABB*T2Z|wx=*@ zzdZ0Fcmj^1E?4s19?k)Wq%rDuQ&yv^^5)vL*O<80?qFu8;I%oDo@nCt}Cyp#U2VNEZJBp zKGL|;Eq=l@3ae@{8qzV-^0`PNcS<4VQ^uK*r{g8e*PN&?1`n%6usodFaB_V#XPri; z_0vmlMaJ>%e18j#t0a49^ts===vOwHNr!QcJc1|bnC^ubEF3!2qL`krKQwyvq2n{j zZ9fe*b3=k6R(a5v#kbi~{SA*?aV1Z>PvX_$#N91j%UxzyF9G}^wo9c8 zbZ%6i4(4rD80gN4Yi35@@+F~$o8G>-lW8Pc)$%y~XsP#nzmrv~#U`QMvo!U@x!e~P zz2$6o1`13a7ZT$i4%{@hp0K+k2P0W57mvQH;~vC(GOi~aiZlsO+JF6gJwB}Vr~^j= zef`_vxu^97jP*ZjoqE`1^3)^M&pKWe?6tW*zYB=>nryk}fp9T_Ow*kWW%)%hp-Rl zLuw9H%<)ut^jkU2NxS!VGc~)uan#h^r#viPJd$j`uB^T1j3G6yS;jFe`cdD+pAG)v z8;tv3#;SG|qvGA|7`X0!zV2!|XFbM!jcO15rYV8S(o+A$#{K%^ZokQyWet3R!AE$6 z;}0Do5H?7BL!caJlB2)h$Yyl&@U|Wr0 ztyA8dIFM=LUa&WDq3MJJ5d5kjUDV9hAWJ=);g82ZA=Y>$|wG4z^GY>l6W+aaT ziP~}<=dN9ittOMrT|&~G-n|Zoe~ats26yJ@UmxLq*PEIRPDQ` zHzPi+QO&Rt?W{K|LX;}1)1+IN{5kkJxtFa{DE-RsM?zPJ;>C!s)-G|V)|7d^)+3y! znjZO)&!82mcHXw=ia4FaeqD2Sl3#LXwZ|W8=HEFrww^bh9QKVK_vD+Wmc<$AcCVTQ zV2MZb^l#^)pW4!Oy!YEExSM3WzLk`weyed9+rL45N)q3R8mW3arY|&9l!ndGx2@ytlgY(b_F>H5HvH`YM{<&(-r$&A=cp# z;|J}Is~3Em&pDpclC;onQ#tB5BwJ&WZ&c^5_WIAjVRDe6Sx{?&`r+-Pt`$DNYF7Gw zvOUea7?Y&lrq3QeemR=+Xvn7w&uny*`-?VW=}~NVV{D|vv6$&ty+?zV{zrIvJ?^ZX zwvyc~`|hLK28Lc`A?jO&+$E3B5XEKdJ@VO?__C7OwU6GM+(nY`maN}c!nc)L+vwJ* ze(h`MS6#Pit@9cC)rx;=q_a|%@XmJhA!~s-{LIL_NK_RQ1+*LEFZb0`#b-7 zAbs*w=0Zi&kU}v9lkaG6qWOyc=7sw$>Pmvdr+wYoiw8rcc|hHH_M$<#r|VF3_DJwh z*hu-o0o6%=>hXeOa-$6K$~U`14hXN`Z|=x3*7|K)*ev|2BImsdm3h6np>ka0DU?PY zUzx&gwzioJbMtO`rEJEGT|J~j=?)!DQ$rPr+^gf~dLv?IMe=WvC5|}~-*vX_I=@dI z=1uIeg@uJUw2I2l>+62IpUc_nz+6kRh0nV#c^#;%oIGQd$1K0~2?aI$nBsTMCb5&amL zdB&P|&>Am+M?c-}bcz!%s$c!q#H_fEqx=v_jZ@U_F0YPlT99!PQYB!dMVgcAZK)P3 zw2T@jS`W^X{k2!kw?y22DGx{&!%oTvKMY0MyyH|d9b%cf$b#fx%T=A3> z`2jc8x?*!f7>^vyV;gL*J=W`tuy_`CZf}37)!q0byMQJsAQv8f_;7>AbWexzRhX3F zZ-dGlu2|&mA#JE3g(EsmAoQaO>_Cll6~Wn{jGt%aW0vopIvt$H$cFo6ANT$%imtan zc;;=n%BovHjJgD-#Oa)4tY1d^YnI7gNmFs5PI{K>oh;&CHhMZj+(yxhIULHR%n_o# z$|4CLgr6$;rdYv8zvL}sv3QGc!+h(SI(&$hcyAJ;P{M(r3hCiY(jdUSEpJ!u3u(<} z<>_;kHQlr-WN2*RAWSHh>hN41l*kXE!!n*QTzR-TEfT_&Bk`)K1Cx}<^lWl{q340U ziq9P>4*vJ*f?YGN|AV==4vMl5|9{uQ0t`d}r36G;VnIqkq`Mnbx>dRb9z{t7sinI? zS{lS6mWGuErIu7u`dl|Y&-eQ~GiT<^`R_R6=nT8d?!BM;6W8^AzusdrJZpyP61F;C z_Kb=BI=2p^Shr7|GON;Dd;4)%!tY$xd|8M`uLAk$I&C7>vNQFCxG-xq=%)OtYRuly z`^v6+Wq&4_yDny>vGZVrRwjo$v#l)Dy)TK@-R5C^R{OYEiOFu$V=+^uIF9Z}s_dQg z@NwSd2y{)@2l{1G>8V#+p71Tu@AT9TpaS>ipGjlrAHW1o{^)?sRo2bKBFje*5<35}jmB<9h;WU@6{) z@@fiVS(}mDe0kk28?Ui`-r9xwY2U&JFZD-_W9KfmkJcJR*$C6*xlnVi=~%8tooZp^ zichdUPGqB|#J(_oe_kRWZYOw1r7F`Oy~A+l_X0YTJd}Z1=)vmh`SzXd$7>9;9qSaI zh#w3nZ-i6gt#!q%Z+ofi_l7SYc)nmAdDa;J#A4=Vc3$kO>#9AY>_*fJ%WhK6HWnB~ ziGNdjIxoJ;ZsO))Vpw=C@R%B^s(57h7c=j#WyE!7R+w{VC@ua(OXXs zU)EaZ`W=Th(~Z7We3_cmkYNE`??K}5yDzc%>vV?Bb?gP)GWLhO)^4WLP1Cv3hHpD{ z_!&ZR z6?Q9X60J*!NV2bQ?pz-Hlt&lmOeJKd_znO1K9TZEg45Rw1LEh{3vktA)NCVmerbta zD3|no&pJWudvO=MawdHJZJZxSt&q zdgg6hKskCa#(ltG;W_*G({Hs!xWpk@t3Wd%5y2Fx9DFsUrLd523c4`+5;^L7Y~p%9 z$+E)y_l2n}f|HI$H49x&@qd0@Aa&0_nCbhf$DkZR_C;-x zyfxkxK8E;`KuHbVo6Y_iQl$bt>5A!0M$V6t%&4NjJhxxf)td#6!^+I#D$oLEAU-Xq z8%YCT4JfrEC*@D{Ly9jO7!Kco1>#DaH|zk~0Lw;;u*194q;TJN2Hzgcb-6#IZXais z*$u_y7oYB!vmo{x?bqp>2%Oiyc}F*#YLPF{VdoJAV-I_Zqxf~U@WN)ak72Y`$Ljo@ zeRK_R+q#BFdxo9*2TN3{V84grit>`6x|f0@5Rc-)TJ+dsb4*K-*xGi>?*6Sixw|ez@fH!x8K+J?rUQ1~&GiSdzNxn~ zhJ!GDmQi)Y(|!KS7(1?PMqX#?RvvQmo!EZN_|Txni6zYT^~G=P%zMNY>;j`B&aER{ zxF!nuGwjt3Pi|X3BGC11S?}_{;l*AzVB;jbof`Yu-Dv66#n;%&xC zMpSb*ZJt1&cthLvS{O#1U@oqcA0u7D&F7~=o)f(baF?4riwcP)l**_Ri;eRBN zWVuFZsIK^t*~P>z*79ah<&8IxeS{uAWRQAdIg=5(`P-4N$h69_PrGG`PC?OXjwe@9 z3hKH`$z_%o84k&Mn@k^f#lW^;Zt(L@jx+LT3I;qtRjWizKy$+Nu<^MnhXnXQvCyN- zI~yF8ENIB)oiEnCdA7TR2@M(ER*^zHw&nVq$r3pU@l=GiBXN)XB13X5ZkHS+3zq5` zI~33Pepgyhg^(|U%*@xz5Rf9)OE$HVHY_*$rTCVEll71azo(Gayi|Z(z&~UWrt`Jj zZtW@~p&V=AM>m@=2%esZ z*djICvO?YW+0#bf&+{Wt`07|VV!quf#x`vvV$0S1p5ywPxtL`k8%eycz67@;8kO$! zTE4O3wss!QeKk|`;rs@uJY0 zSN1V#B5(F#!&;#bgE{--wqvG@JLHFZos=a7*}ihWx6Tw76_tX&?Ln#Ffa-rP?p zW@2de{HlG-D$nlm5Hi)8T*QWC|D5Z=XkJ%Z(!i%YVQl(nsO*&{O$Pe zty76uB{k)Uey=K36JBjPY$(hbIjP^EG5C|p4 z)GKzlZMZn}^lv+rdjE_a4n0h8^->yPCp4S)F3qQ@C9tIXc5DdIxvEYQb#J^Pq+yXYY~8s?jy_TV7rRMSPW&(M%%;KQgj;qlQDvn*Kd^77U-ZLX446C?M` zsds0-FSxuosL#zMoaKmKewUkClwi1S+qZ>dPMOV1W0?HvtFk122p!W_XbxcmN}xRi zT=((kRJRHZ=E839-z+Z9C?)&(<6NgKm5{dfry5p)t&$gRWgH-A>&lBUj5w7KZ~}F+xZhNJVMx2g zg)?aJY5!At(*^u`Q?FOfS+7>92?~As?KQuAzBkf+M#^1#GTQyUwDyzt`XXhKI@f$W z#u-Bw=v}<)?~lbtn6O7al?|$1#fM}lJ?P^p2+HxT4sYg<_YW3Rr6Edfw%w}d4JM9G z?>j!|F{fQTX&k2(7UN{6A=0C{;3;wVz-_IPKDOE>tLhz^@Yk0|1sN`DB&P;8z7C}P zlKoAYx zJ^X<4FWf4^YvFb4DHgjPzLoHobPu=LYLb_F|4G2rCH^~;z*DtF`mo;Dl>1MM7*sDm zzL{>VvVB7M7jCv2Wgy;~4%o0gWlkw)2zf9yG z{jwYHX3ZfXw<8t^o1JQ{uEVc>47)x(NIlT#0Z#-4o)8o=24OK6iIQDDj$_XxRQn3?q?NE#! z*BdCuxtMDFUgz5~I!)giAF^xmdw{2ADO$T|CKhJl6j*|$aQMoBN{@A}>$ zDH~YoBE?D%so-<|ZttbDL!}P` z)FKS`M;#42ZynpM@|*QLm&+nB^%1DPx1hZe-q+XnxVOB!3sGVs!l08k_mi!D=0Is@ zoG{L%0y@$pzxr_IKR&&VBo5}a>XSwguxC#W-Nk<`_sDSbw5>{3Dpl=88%x$0d&Ciw z_W7%0WqYb0xKHE~89UC_&vrfKexM5H;|u9#cjqojyHHMTv?nGi;0R@fUC+?(7;kp6 zoo?^4{CeTh*RWZVhSUTfdObegMFl>QaC@q;FV5m?JbBtaH{MBTZ5N%;)zm4C%jI&| zFx08sdZpi0<4QzDbOtXI#9hr>82;#~2IZ@Pj*^aezO(y8k!2!jeHV&WEl4V&Hmu}a zv!hPmZol6p8bXjoe1nZO;GwC$h6za@O&CGAqKQJnw|94+K6kwtk^2PG&s6#3)mwIP zd+XA>N`55uHq#yAB^GVIsydB@bJl4^1&Qslf*4z_j`atT)&Znu zVU}$A*q1EiCIzxiH=C#S?qP-OW>mt>t}lrB+Xe0HryOS5?uK1$`{7V7EU=;`H?9@$ zZ6tv^$BnCAT3DNVqO)Jv^T^(LXm-AHoFi(H$B9G`9f;QP?-2j9;q;8~6zlJyyT`dh z8%EpJY;-5}%?3M&40)da?3lM6KF}WA0J!mUCZcelpb%P*x_zM%;0)|!H)m*QeMDE3I9%lbupNi38Y~A3V zch@C0UyCoT!X=#OQ9@hxLE4ejq^h!Do3Bq6!|ARu-}jLtm!C&kt$5m zqOaD>9=@c$W8e2MW!Z=_!$jYtG^+9|+uH7k<*|V)lnSY7f$^o{1uOP>ef|;+(U%`v z4VpEq$a6nWDE+-brXxk!D1sb=FO!m*O6qB$izL)|vbVsT&i^uCYuP|M$p+M6C4dP= zBt`O#WwjGYBIk&SqyGGH<$sdwIBUwH2B^55Y4)$91dmV2pU8N*FtK-hd)3Hcs>Vx- z!P0H=4mmb?#DQMcd4~6G-p=Nm6yZJVS8g;EMuT7LR%^~q)D4+GU#iwDM~anmc*AQcmk~zI_yW{1B6JH(9|T0&hudqfdBPTCya2sw`-x z`z!w2D@-BDy!~TcsyF8uIo^;ODyVWjk`mrf{UQ`H-NY@}TAsCIUv(bEXu-R;Awm_A z+4T09L#<{HcbiZ2MwQf5!S6Jit=${Xg)NlJs$CiGVB+Ih5miPNsDqT+Konx_=c`$f zuDA=vUna2df53k#Y+g7K!khYx^m|z*exI?;9lM`!z`Y_;S~~J)GO5JbzWb8(+h1=k z@@Hu5KX+qO)_q)eyVPJ&=p7T$sQCPI6m4;hwkJ4iWM3874tN>4D;9TZ ztsWSgxi7rrytiFt-A^M)#otz%x_GI6BdY0z?qt*I(6m>33yH(!n*twCGK`v%SZW3k zbUOXxH(K&RrM|vkzD=u=kavS}iC+RmCne3HoHlPkOu-|)3`XzATlkJcAl-%|GPSF$+B9HLO&bBm`(+DuAahg! zh(UhKZI=6mTBiKlPoEx;-+l5hq%f(d`w^M-tq}C-8<`Kw2ky17+=&&sQA@zfrbHuw z-gRe4%)V|JK>b3Hn2Jw)&Xeu-{K&=DD(*U}^%WskdCVuEF*rZx=z6Gr?LukXhG2$A zsX%|dV)i}ARnA&Dj`Ed}s?^Y8x?0nl=@rYr-IJoiDyD)$e`Jx$4Im~RXkQp_Xc%@Y zqpKm16t?7=3M;Idp#Y(ROZ$co5GF0VQ(x4Ao*mK0xAx_zomgF*Z0T`->V7QVGV6Z1 zaKU@E6od41bBs;OOo&q7>^g&H8bv``9+CY!XZ3;cKeiN!7GDqnGLuH7iggIV%{)uf z>wKo^s78+CX>58|ufAnxnX??d_NsYKLt8rrQSPjMB2oHA#5Y0%1v3O_9$_m8jeLxu zaO2_ew9|{E5ROtlu(M6U_Oa005DXw4BmHt9(Rv<3z$Z}6c9G%QDM`KG9+#eroR<6= zMr|+9*%`wr4&wWCNU8n2JYH#^RK-C{D+WG1Mqg%t)+Tyw>Np%z7KZ2?Gwz|+q}jHU zX};YvPIS|lrUDZ+tCs5ere~IipD6S=;B(aU>JEG@abUP;JE(Pq%(n5hBg*>*8CW{s zf<&(cK?S8rhJ*uCL=s8ya2N+_fjoESn>QD^O@st6w9%+srQSc%z4E333MbO)F`T&x zrPTa+4hlsC@DV!CP6c1F~Z%{g(CUBz`-Z+t_X+v*c z%TFeZVcw;ZW4p%I<@co0zUy8los4YCM=8rju6TiZk=@}Ks-|b+C$)^_>KA*N&sOd) zUl7Ro&%ILBN#D^chbqUXe&PrkroCbc0~U46z-yjL^qS*RB3*dY+oj(EpP&`kq3{^W z8Zp~?0h`vsig`7HaK(@NlM?plcPo1knmyC5T6 zhVW5S(~JNk$2hNPBI|(tNLbMd$3_~RmAexvxfLQWE0%q@AdqwR?fl4me?s(<3)4Z z!U#FuICyB*6Cz??&j_c-BcVhw&jJTp&W#;^-T;zxnu)pCKMuJxXL6dyo#rA2Bub1^ zJmIn5jbeJ0-D_IdWXay^EvcchnL$*Z9FFOQ_#G6Ak%=e$&UJ3ShCkU1g75vx;b(tX zRww_eP-r))WR1x3O>YaMd?fxt>z-3QUP)O$mHKq}A;mw(f2qOa;Z;!Cv?&rHA^Gl! z&Z*721t|xpiLhMH3tQj~!Q7&Ex7~)Qb?i8(F_AxkkaOui- zYf(oa3jX_YPauP?GfEs%nodoO=L+4SxPINb&}C^zqdy2nILKv-w8i@!Y`#a{V{B-A z+kFmoaJww`+FgkMcGw3Ghm5CZZ5b3#5oSmxegkP)%=`oylPx>)WYnLUJJ%uKj8!v_ zfEo2Cjrl?_m~ylbtkY4fK&SAQe(^4#%JdwV5Ks152-<=_$04%Dv&D=&?bNALFML%% z1@xyFo|+3N5*cK=+XnBr2?Qc?d2HImf@Zzt78pe=V#C;0ypgA)_FkaXNA1YdAn5!~ zbEi-g8CNWb^_8LveXW{t3~Lb8NYjv1(Udl?9EY zin4*8=?{0&DM|E20dQaqLPlOO9;r%$`A=fZ^(WCbaC+!DIlJ}pj%*^&*|;Q^aGp80 zjms>uMNog}ik2qX>;F6^xnkRs{Q`r@1t6TT8^FUG?JypVe)DD`MC0g{8hOR> z4z>gED~CGakq7N8zTJ>1B&V$W#uG?b5Czq)x&;WEu$g-a<`C*Krv2CYVAJc_QuyE3 z(&L!}rh;dmNwvNqL7xccAJG;=wGPE}`}Q4d6yl6<*aZQUvyT5?Eu4=D4noZcSU_~X z7YqHNc@QhS0SP1$1nBu^GGOM%VvigLf^*ZCAd^NlaiIJ&_bHO27-?7y5Jow6#JQU3en z^7Z-tHvOZEb;eQii$x-qOR_+;pxhgN{Jr_;Y0Ba@ahz6YqsK6Xh)6yx`v=?{tq8Pk zqRD@{-WB}tQ9wR~=o_;T?2*#*mx1O)w>MhSvi@Y_;UzNCx8V9~L7*l8vX6!udQ8dz zgu=P(rWWW|slq6xgBOhY4Mb?*Ivyd>VYMpy_ocjOHP;y8OoMgF^B+Mt;@j$IfE!eRq(vOXuL>2Tmg*b|W9G5^$6SVe?3! z;`Y|jP+epwv$*Y*h_wpVJxY_C=(M>+)=xVHmEeep2qBUTq6j1qH*97fh!+a`9f98f z@-`S(cszH2$976R;or|7m5oe-26zX!cW6+qYQ^(Hck~J6i`E!9jmN52u^tbT6Swpi& zvi?5-?^7F%LltUz^#0yhm&p_q6?t((PNS*Ex``Gg}O&m1)^=88DkFAshMh!xBalyJ+YCS zSNLqNfG8g3Qf>zofEkwdGH+3)r1*UzwM+!0-MPPPb*M$@TgKnySdM`%N? z^F@JG)7?1o=xt;_g+6tBs%W~wls0+V-$?cXB)8;` zy6LpjN6$T}3R4`Uj9n#Kx(Y8{QC=M%ZT9*r*l?zr^e5H?h;aKkNP`;UHA@|PDUPY*u1EsQJ#N$`6R=M+9B+M~6oYOH5uP`gJ>?W|7?K-#+>%XRbV=ZK{B*yO%uyobRZfQ?JP?dOmwr zA_QpAnN663*x-GFbZh&Qe%bFH5+$KW%N~fe3vTHDS;E8?TxW*Lw=aWo=nv=LQ!JC4 zn;Vx!7Y%~J2y#lq4FiSZ4I$Z`#`ZNZy!`haygyaIY1Pnp8+oD#JQ2VyCWX7OkULWm z1?K`k?0+AKZ7t*CS-Q#CkiD&X2HAvg6fI8bR@ms+b@f&&)$|>0RxV155YIzj{CDW!~&nBBRO^ zEYf*{`KXDa`*pwR|2uDuC_=_tvGLdHczcLM&d5}VzuPTDb@T=JbdX`0DC+G_yTOk4 zBnqyAv9U4Y&x-VBJ$k?a#F&-&y>)JcZG~?mY%7jjO-DZx#TTx(Dkk8|x5ICh5cGPU zMZUg&Jx~+6+Ti#J4dIe3AIs6$y@J|{)Z&s%Fa=~HtY}OijgN!{(y~7{aviq3ADf%& z>X?sSwC}Eh3bT(!lIg@~z zKk!x8$KdXU`Wk6O=*Cwxr}nc+g>rzJ+;HufE*v8bnaINbZ-pbUibD?4J zC+~&8aJLFkjx8|DPk~419Uuz2ujd`zl%gTmzh@>^v4V!~R_*Tqq0Ld<*cwZtoNZaN zx0|y0bk~x4^l#o52i)=M+W7~&qB`Px#~O{?81A^wb4+&U+*?vTGCGOUxVw6q^H*xAwe-JJV3hc+ z5YQekwEd)smwVlNWvKmGw4ewR_GhDoo#AA}3rOaEp0p=FO#%}EH<;#~%Mm{|KpLtBo)r z-eZP^>zRiSh?0XT1qY_RfJ*xAZ*>ECf#(rgA_5=vvrJ9gu8=vI1oyxVla#mGXaQCx z9^0YybgH8(hsu?#oBQ~1u0EL6GWw$0@*cmN1UA~w-|al=PdS|P#=sLhbF`B}ExzN# zEOqa>NrWT;N@il^?*7p{^qe|Ri^tP2pvU7TpU8OknRBQ;au0swEui+ag;|W(v(3rV z_$APi&B(c<^KTj^iUwsBY-R4Q1XcpkWblF`4}VT=J<%6AM#2y0wu>}>Ln~1iQRHcf z#2J-blH~C8aSb;uccTjPPAVj87VZsMWIK2>^;bTI1!m!r$Dhw3dBq?@l?5*w?(aS& zS;#2}!?CngwyjD9v*}3!E=h?gBr^>{_~a1(HfUAfz==r?!((|99K0oM*K7fw+k+%a zg86p}5X4z!HiN1NlLBb_8$*BBcGsIXc^*8rdLfGmKubET260J>`?>la`-4Va9=IS8 zV*Uh2xwJs4oO$5}cBko3!R0p~S*wDhASK6lV9=Vt5WMBO15Qs_xF->-pU}N~_Y9$| zHu&#G_<3M9H6^XyCN&5Fg>p!*{(y<@4A?XO@N-E99Zv%*kMl{1ez=j@z_k>4c(B#* z+3*hbrm*{Z_Stx=U#c66gKC8ck#0ItYwFiz^S_&V6sZbF>zcsP7b6}NssQ(IaWu)% zwC(v6b|(-FLH9Wm8_(a{cL1S=PmA$sQeg;p0;y@_c^M+#7m69fsawLmKW`8)eZZmB zYLLVhU6aWgttVp0{PR#)3;YYMBS!~B_P%ipqvg+=O33s(k1JZ)JOGWJScUyKI~>+* zi}Ba_?B0PTDhgv{?XSC~wN<<9i0}D4?Pz;D1Dk&IyN->;)@OHQ^FFR#9hI=-mZodI zpKg@rZfGEd`o-IDF8BbPkjP=um1hMEZY#WMJZQ8A5R?UgGad<8?eClhaWudsl~q?) z&-n`K_Bd;n=CbOUqX{oLb)M8T_@Inkf~(}&bX$g?g7SK;p~7@(J(l5i_8svu=;%cl zP{@0!ppG>b9ZwsQIe;SzObD>q{t#by>>Ci`dQVCGf@%(wu_EY+Xh=J4V&XyKV z+~nkBzkt0CMEI$t$g3isM)aY@4XrkPpV}uiRoS1a%coyE)X1v;WAuUm)oAQpzSWyD zSNJl`LpF}uQdvFVrw`EKGUYsFLMYH&V!h!I|8S#ea6a*9NBcbRZjBTP%tmjUzdF$K zHHfB*PWj!#o(VjD#=@JFOqd+%xY5YqMr!Iwo~y6*AqA0r7F4WB?TNyAkX9$}aH|cX zzi%Ly{peDw|6}%I>`tGHhmwW(V};v2nV(U)9bnj%_lW~fB*9dh>_hbjC8ed)wQC&_ zVAB5v%Lgl1#Z!i_Ey7kg94Y34a)3Qr|Jm!FVByH$;k=pp-n%FZ&@QH5Ln$v` zy=s=I7p+FxnUO)}Bsj2?i`J9hE%qI@4!uoXVX1& z032#)Lnr_24J0dJ8YHD}fD;yz@Es~_w$AD91Z_G_f6v~cH01X@Kk(R()yhWm`y6;~ z185KXv?26ne5o=cH>j15(xNwjEA+Ws;#SW8l0D) zdKNf@?Uy{c6lLAw9+k#S)j**E3!;gAZ3!&cdjSMO2%>V0Z-mZR{S%=9E>i&}!El61 z{Mj%WXbq2jRWhy*1EbumLmU5P5F%G&h1^Qo5*+{gl*wHaDjL0uZ9vzQfwgi9fbkCG zZ&(+K3uS-dIqN7OsIToS+-t2!VFxaijM7tA?WYT^;3 z61+#};y;mgofzA|5~OSbnnWK}6~x(G)i?F;XqND5f_+#_atJSKVDfXv27yy`sEtim zvacf~uKW4<$(kDK9wSo_3JeKB>%4Jastra?mXp6Q{bsP1@YUU0=XV+;&EcHazpfH+ z5e|mKgtP)}jFRd72+hs-Q2a>xPeG=$VbQ7vw^%SLuKxfHVDuQSCy%v{J0>lQ7JQtLC}A>y5XchM!-J2xUn3|lc;(Uop%W~B;vFN&#h;~&)j zQtOVtBd+X!9!Nliev{JN+VJPcc@o!dRt2nTZ*uT}_#RZ`(jjWy%(al4$PWMc9BLnc z1-DpK+XfI2v01g5>3j)K&~I4l?)Y9NlD8y?k9yO)_#tqBtltSo3+Z?VHD?1YX=G-5 zQPBiZjtO;^p94wFKCF?O$3HSG4c9EtHJuLtulUksDk?*`>$k`XSmuS~$D_x6C^ZYG zPv`qY5O|C1%+ZlN9Jtgd+eSOrAQgd?C738P@hkKvkW6SJwRqoOy>#OhYYTtOf_#!1 z-dgJ_cD`@c3Z|f9{LYY7vdW3wYC<9bt*I)b%)&|bKiSif47}I zitOHYZLN&+LPWQ$o-2=E-*<`n?B0;TtC49EIGLgtu=PK%q7ir-IvxHBaOm&aW3CP0$kY!G!GgsVR}R>GlU3)%<34Bs^M8 z_QK72`1z8%CqH2#Ge-*&0{FJzfc~~lE8OfCoBoHcllG1u3d{;%AdTF{ZuSw1BuJgG zr}#!L=xkFZmQi9+ATjIIH<19xw5thxI)m;*tY0QoqCLy=py(M zM>)n0k>vW|K{)J^ww3kUOwP?k#Z-#<{bij&l(lMKGiD>O!mP1h(ai7Bug5F&MB zX9-dtCn54P1f1-xZ(h&_iFve)(O7s5PU&ms*%vA@#hy}D6zL?vXFO5Yfx9{zY+pn3 z*PZX0MDQMg(&pcTQWXDlbmK3c-WF0fFX*kp! zSC70wRn(a#c7MU!zUSd0>~HK#oAegI!tyFioLU8AAR{uVgdT?7-4Fr6*;G;!3V`9d>nK zR9DJYdkWG_`Wlmo$(_;Z!&dQSuv|-}QBzVCmeIad>>Y|`sKG>6j+DIYuC&G}LT1kX zkNwK1$y~!sMHaUPVnL1^=E~DVno6-;pG|rp>t%x~lBJ74Lbe7Qo!+~SenMKyx>A)t zt&8i(Cp0E9iM~cg;;u(uCYXBBjSkfgLSU7v!9(Txg9tti; zd#((NpEtB9$Iug6;RNpyy;g)m_Qox=XeOG)D}lhftRr>cf)goyZwej`foKKq{Gdn~ zOZ4H>NEz+1X9nG<2{>-ZC-)nT`LI0H=i*jBkv#N!q}ph~!K$mn>ZED+CYI zmLP&EC)a8-5oi&GP@VpxTW0ad%Eu0x3w_q9y=BMLTArsdc4m;rsHmv0pC}w?7>wvx zhZc24CSKW7gX`f6zLp{eSN%cD$g5lkQlbF&9v$<%ig&i;zX>K;H6H{@qGqQ|c=X=W zQxeyL9<7&E=ERGkbxH1&a!YZ3wPSg?zI z4F*Jf(3T$)9BO&;lHHv5lAeeE_lLcOPKy*0}en=y<4nV072MHP_F z3GyfV3egjU>Cff1YC@9SlN?|sV3oCsY!kHktZZr0h#bl`Q+gi0;XT_UDMZm_1=|U& z@l#laU$k>qW3~YN6@^e;R)NCPcJkDQ@W8ecc_Eh@ zI(Z9l^szKK^&J9OQlhmm5|3dmf(VwuhfMPjs0lvgXv z?JStflsco;)cwIqR{93tF*xpQGjau01_- zF8pEE4kIZ>t0ulGoA!q`QPz(EQVbu9+@~Dp)|U3`SIF_wy`4Bk2h(`koUYn(D;Z^# zYA-S8iYw~sYN2Q`c6< zXl+W0nJ}}Ey?6bUcXRbxgKi8w>V5@o=g*}lXTRE(>C}7MDZG2;^yVcAB&oP336hEv zA*q;BC1?L^16`ouX>X}_V_Uxje|(yKWgH?)Z5@1rSAK}c-q7k$*lJ4Ja=6eS$Jp~e1NZh?J;uM{)T`IZi? zbV20FUft6PnihxNr6NsSVb6R}C#8=)6-Yy!SDg9nU59ylW@=V$_%Mc0a}35s=yzNn zNPk_HuwVLfg^h&$U3-6?LC5vW)ErB*hm(IihDm)Qjf1Y$?7!`LI;i?}eAvELCG>z- zY6M^Ao$zzBqhXvD&uw0vP}$tW5SlQuyi5J2xi_;ap>mXZjlOK|bko>ZstEtX-A_xk zw%$83ALB@!edOFW`T2h>*VxV4?l19OR#|U08*ezI+com)U#evjIh4N{6zGvvw-%ay zZ>x6Ljbz-%D0SyrVv67|fXlA_A%W*@ep$H8?fId~Yc4ORWVY$_Xvgrn5r>RT_pfj| zkv*E4)xvFDclTf*g#z}vy+lm<;6m*g4nd`Qj z4LNMf=$U&f)xI=x;2A2%7!tR!}<;X(Az6(>r; zs)T5-2eg})7~hB8{)9xRaw7;{*&}kZ*FT4Qp|AL7Z4l<}RKDG4dN+6H(5AMrl1;v6 zcV(Cmw5)GE`Bp56{Y1w@vpeuc&GdWW=(Dl)l|5#v-z#tF(zhyXx$ZIz=KX$ET0t-O zV7MYv(o%!&U(ik?5%waSNfu6 zzczZjD@VO5gULDXcaZaS?rDR3lb|Z0x8hs z+=%;d_1~Qg3iS@}cb@&Ck6O&vzsMvM>exU3!x?}6zd@Ug{~KL70btCd6%}>y$($@T z3aPyzncx6G!x)nO{Ymourv)Ib%pryk_#6mIXV0G(%rT{BV2C|`QTqGxrC0FR;`_|e zzeoSyvy2N13=D_kwR&5LKhVC;<(jtqf>cpFGDZ!-GtGb?w*Uy|S{4tj#n$nI zO+Ezor0=!S-~Zvu0WFjw3hOq9xMuMXT>&XL!bmGw+8GwCwk2@RA>ElE16GmHK%hn0ZDt@U0k9j7c=Bm}UR|nYvxnC3Ed>lc9Hknz`H()wu^iN~X(;M) zbXAV_XQ-q}o|21V{cXXIBnpRBfr>ZJE5YkCbPUb|6tu(-d19Xzs50z~TozJSLIPjC zy6s}P04DYKkK0yyGJ=LTc9GmZAtaHYyp8~&2Am-xVvB)FnpOp=R$Jk+ZEyc-oBK+h z9LRvgpgw6}cMVi2EHT0!&KXf2P=NP?OgsLla;R^ruC-&(dCtao4c)dzU63aJ zEjcp?gr2@v51|MM@>~{Nm31Zc^)$l528doOB4-QXU`z{vhQuMRdoW#w#9<%!fw)?Z ztJsItAarft%~*-!){6B+%akTlpsmzd>Dq^fWt~Kl@}@mj$F%74c7Zx*S4koV`A^V! zPysYH(+L8?H&7R90TiEU>pP|r&+TUsO_@pf?Ck7w4FRA`gd|Q;5XX7v>~eE+PlE^y z-I=kb9s3Meo@gD%oj(X2z=^r4;NIchT1FJE&SUFb0d-(ru5Bkygby;TWDpThX=k5J zdoLt_j|_Bv`tdnv8YCrep}u{^qa8kiON2tv;AZ8hbKIvdUocKRiz<{7#b#KFh}Q;# z)5AKTecJ;@5D(oM9yw3o(W2`RJ>7HMB{CvqcV!%$c)pJFjXFk@!O<5AdW;Z98wJ($ zsO>*UDUgPl`4&5S;RsH_Uy&hz)%v0Oh3`vym=%Dh)*tLxR{?&Nft5A?OGV_ocm2HK z0h}dR1zna|`R?R4N$=-diS`YO9Q?jO@=m|nF{eF_uk)n6P&p)us@XjPCDP0gDG>_k zBrIBA{C184fA4{R5=Y&7I3m`|IGzR?or*E<6A48=cKH#q6xm4SE6BD1G-6puvGL@} z20%kEavR8bdqU2f*?4XeDN$%Hh}sPi0E$}XeUbABC^jQ%NmQXr^^7B0r*rC{i_p-}~ii0xvSQTLwNpoQ)9#wCoB&NOy#8*!&o;9D76NmKW_{7dG$a`Sr+?qS=fcIfiY<{8ZQNLTh)mgMQxv@ce{g%A$&eH~<6r2o8l5xZNq z90#s(s499i!QDjZ)JMT*w^(5qzh*2uVAES2G48dMkn>2l!FykI<6Iwx^?2 z*XAW~r8m)dM8PosnIB^RW^c6V)qA?-`1m_eDtg(brlrXhNJ&deHzBQtnQm(r^p&l} z!JjHb$}ag|o#(nzM%=7QzF0t&={#WR6-EWjO>J3VD491wk3+W34C3moBL`&jTWw20 zE$FervPNFe?)(_2kS=16A3qkev9S?@Y@@GX9?t$SxE`=~oO`vTf^nK_b&kJvcVTGop+~w*a-8)K@VZmUP_-sl0pz z3q)FZ|zNboizt8&gCNO`Tx^0Yr! zS%Xe*q(cE3tJ=TOH4ReN zj{M32Oz)$gy3Hz<3KH0F-K=z8`3`kL1xZ7FY7RRr zR1rv#6(=@stW*cLu$lCq6BmRYR-9*>P9F9(6Bq@?X%7hLm<>8}zw^p5uhbRU?F@}S zX(qZgti!#vKAxVWQxK6YZX%qb^F2&>GsbPSEl)2`JQZT1)wpXvfbw;wG^Z$iYB5)i1+>(vT1XyYigx9ukTTX^1Hs28b`xUEjLWY*)M1EOH&VLAhr zZA0?v@v6-sYwW#;&|o*)*3H@g6&j!E*$xo7N3>)On@YG@53fabDga@(y#k7%N|>#I zo%)_&IqSI^yR;~%0FvgdEXrFmSkGjml}w{nScXd?A00r)k2LQBBIrN)4<@wzBb5Q( z-_P$Hw6ALYV4rohO*sd)8e-3$J<1 zNS%3m+;a`9+PM!?DP|Yx@Pr*;5p`j#QR#zzQ1>cmO(HP(38X{}cR-o3p+N&|%Om#9 zxir82Mpw;4z+2={d^ zgQ78LbPKLotXIsMher3d@P52jAHpIX5HBT3n4XytFf^Xs*O%auWT{!&-kJT90D9** z8#C(!#CZMbGdjTrWY?1w4UIGs$H% z34(Y(zn_0TUvB6~j&#+cZ1E$IE2&15pcH3e5fx|VZ{-}$`}MB(-iMaVZi?3!gEVse zYJD&ErPTr*{;LINvqxW|{Z$q?HbE9{@&jK&LPEl_KJ>0X(vbV1lBP}@uA|M=^hpAt z?Uyu|5m{;rEC?{%UJc9`w`r_xDKqTIwd@vlEWen`U_ziNqH4XFXy$3{9LHC+fpTz0rsYI>{%%>yKcz6T zs)&en9J7-pW?9_c=ZjGYU+kmFq7t$yy+HKJ>bd#_!;MD66KMiM>NVELcIY2D=idXg z&F*({#s8H5tR!y0Nm3$xUM5cEx3upQ9WzA8r`Z~+fnH9d6KA0DPu`UTe0%%3Lsi(ZRroDAb(-MCBc(3LNC} ztBwb`d-2gDO88lEe=7zZF}_(Z9i8~=pWPx7?(vx}Ekq_x^+uS1hK-xyrsMv)Nl^cK z%eF_S0MpOhlqAJoT+LL4-rMQjo{IckyIB%HxR!`QDv*oc{J2{LK|2umAI@j#_?Cpn zU-*VL&Nex1H{0=@VsQ_)+~%Z*BZk!RiNk4z30UqnbRc%Jme402$eKtLj8cb7`*pKiHCKfJa&lA|Q|BfJY zz3#S-Tz_ki(><|={RMsO%M4ZSB3^C{YOzin_D1$z_Lf&O4D+{kLEFnynV{sUv$b&0 zU?QxvU;iXAi>qxb(smnkNHY)Kl~W@E%;RT$RlbK4llKuSVRd^6C_7?V-D$EQn$YIVTsd~b}kJ67jbVLRaLmPkD_1;n3Mu4DJk8c(hXA5 zDBaT1Ac{drOLup7DAL{1AkC(vn>!bJ&i%eI?j65B?jP4N&N%1jX0!KN?|R=kpZUZ+ zLc?5HKBksHM!f9vl8STb+xxw>QT8dO{*Rk!$kek zxO-{`A&`}=q$>w<0so)1T@RwyUo>bKH4u{Y(TleKqi+*ID zfJrjBR{EvtgSR}Fp$jenq1k|iTiT_la^GQHM}#4R7GQf&%oyIXM3Cd>3{}HV+{GGc zbWN3s7azM6NzuEAgSN+&66dr84RGH=1^-=o3~SqdYcuc7)bmZEg#$I1zt$5ajD+$^ zp>vE?*wHg}C5B}X;PJpA)3!m2$R4gVEHq;fMrS;ebgr?{!0e)&cL?KLK7rGh`CN_d z%5d1LeS)VY`D8_O2?|OBw>bq~4k~-RFPCT!>GhUq=Pf+%6C5_t+gVmI*iG0j-~`8( zM3jO$F6~qt8CRiI1(q|Ao?{p9uQ~-h;}cg|d#Onur72Er;G^a2Bqk>2WA3Ps#5tgj z9pz%gnzjBajC!WPqAQ{as%}l?f%LM>Z9|fWD<&eo-!``oKuoHY0`ELz+o# z+k%tk<>I_JUqVM%=-nu|xb}jYjW98bFnx~R(i%!13WIm9)n2VgD`yF>0ndKtNWB+8 zyXu$dJ^1SIIPCk&+anqZX}>gUTorLaM8m@;3A=$k@QP8@2d&h1+B3nk!cY=4TmQg+Y&aH^^^+@ zJu_Z?Q#q6*&Ojxh=D)D`VO{Ij@}ef-?xkTSq(eAVX-30Og(ir{iX$shl&n?rRyTEc z)M+cqOC*(A=}}d7N+czjovFMZD|Yip+oK0e<+#L9hdJ(?^Kr`^%uI4jgg{v{>D(CL zKjj&=AHTZH)$1A)OqMbP>EW)}3@gChp<17}*?M+w_x+@-HRQp6mlJYA~*)hWH zxitG_{SOjtr>g-XlIY5)+^62)$Z0|?961<+j}3q&XB-g-TTAN4htzpkuVmu9k-feV1qX`A;oU5uFYmFv&*<}~5u zPh(%fgt>MtNQnY}{jNzX@>kpc14x$n{|YZl8A#@!rE9`4a0Xoa1HOH0YW=GNMvL(e z2_c7mKhV5-Ke;7TvcUkYE7m|tO6p9<4j%+fW~vm%Ujg!$7=_T?f*+Ry4)|Y;zE&Hu zJ^h!Sp9#$2wQm1sxc~nVvX1Nu*Esi97@~gew{pyFRDYek@%i%SH%BXGym#;4p(%Q9 ztY9=gUDnv{PO5sjfcY%!sVK-`w1p7GQ7V*tHV>V{>l)+y2-U>?@&NlU-LkGjiOkaaP}dcrW_$ z&C3t-{kswqTqTa-E=yu^$X}peOuf+Oef9`U@Al0b7_$&QxfAPq zU)8LK9yLzbomi%{jjarZ++bs&&Ou-$5Zc!6Ai89tPX>9bDA8D$E~>5`U+CSM)r#5c2qOQ0|u2U zS8d0o8=N1hqwH8p_*8cND@$htOE#+=4=OCh66D%e=viGJO4B;EtW@UsIw*Lu-ZL_j zmX4;w?sWXMQrU^^s3B!;UtV^+xf3(HIXY#s~A~bctm@Isd$}OmYG@Gr!Y09gQUGG*bvTgHVABr+tU zSYHr=pC$4DA)_FSyQTMXuGe%!WLBW<(frlHuz>4l4^YEBS2x62%`7_0f=@Zw_#p_4vIPuE6A&T;tT#E@MNQl-Ct{?GHP|DWbaXU{s< zP`<7gu(*YQ-Jb{`MNhG*YRM)Mm#u`$>9HN6ngBKq5~@@ASN02AOUuJ2RF42=3q6~~ ztWB7o_a_L71d)HF!&2G?VB$)^ZIWFEYsC@s*%s=YAi(HF!8HB|hVF>`hgn&Enorn`;HU&9H+z_ng zYsfisf&d?Ys*dPHDgNWQ|NVJ3V6p2?H~92`xB)yWBIJ!-fuLLSYwOHScn&~1#eXVh zG&FXOK0KTFx&i2=O!Ju#unMMHzKHBR$+OZApDDTTS{lnUM8yg62s_Bl`E6NFR{^4o zS>As`%gc8#grW>gJ7-%%BPLUAQ))n5k_h#=@1d2Yr36n8P_JLm>&VLol?^mV_H&$x zV&N_X3TyQ>gDO>yrqvR_j^uLz8qfLpVH_aw&cj3UL2rMTqppo#PPTn({P)o>IsyNd zru!Fy1ha*%O<BAi-KW5PMo47Iy>ySZfVVTR*h5eJxIe5^IV&!mwgoHN6RSoJF* zDq)c!^9K z5Rq>>!g`y%u)IX6!L(#d56cCF8KfYW2!Ms&4Dm;#2DPVQfFghH|zrEhEzQh#N-}E zIP>3FA^goAQo?3o8pwtj0D3`km^7e7dHm>%0oq5bCP2~^0!=~-P^9T?mijC>YB~UI zo(MPl6UAv00KUiNFE2Tox1k<`ju5SS88urmvlc@a3SkCm_wH(Bb@o>&M;@8#sETIZ z(>=kuN)@SLP4-OiHYd-9$4`OxEdd`J>CgP>#Ob36K==KZB9Cz%b!T-J{nl)B?+Sx84=JU{#U)qUJBI%BPyWMK#5!I7VWs=SB4?af>FZ9h+7vfRcI(=)u$Ccy9EUupQ4`Ln_S2TE~g?rGZv|CA8?m716X~gzIbv72GavwD(BJMp0)x5J#PR}#Jid2+h`Sns|cgn zD7zfrR>M8>AW)Am@o;gCV2A@cMhmk7gm@_exPY;P%{jEEEV;k^9!}|fHbA-G^f)CF95wTh{6d&?d_^o2U?Z6~=l~+fk#setvG+yX<9Dx_5mMx2 zg-HxUy3t@ZEiB!fGWasJuUoyl<88Wa3gl{WkZ(t<3)k4KRe?}^z+zg+YAp5{dQKJ`HtR%SpaAc4 zxwLixg6!J1+rpUCSv7kM9bz_NKdY_z%OrVPdqHTV$e2Pu$|ht67>y|CR$IZKx?RxR zkC<hIqPm zZp3b5sDRpes{HF2t7VUFMBkeV(~}nn8Hf(^d?9s}&w0ez26@+*6(+b}PqhlBWe#qi z8LJ?F8!WBkJqLPU7-1vX(!RuebQkEbo+C3oa0063V-n(DzJr&v56>m&OihUKqS(x% z)qrCP8`c2hd<472BSZ;uR7zddzutuP?r;CV+d1Igjc`en^`msNfqGzMMnOti8fmw; zKBb|_2VKi?rvIbj7QCdhI9hmPLOg&f%q3+Cfi|8GlX6hvWi28OG8!SmVCw;Y`fG(e zdc;N`madE!U>*>yq1}iDnJE0zgURy=m;}sE2IJz2rA&|;@G1K&(Cj?j69j=41IIJC zma)(mqM`NO`0wpbWmR*4d~aRk$$~OqaBB(HJ%LtZ_SX0BAADj#uzU|O`9fRgDJDud zsmIl90)`0P8Qi&A%8$7EERL2HG(Vy>{PF$@a>b7dshwq?^k8KwJH^OBz&Mp%Y@*`U5t_0JD{%T&h z3x~_2!O`!L_&<1tIAM%0bv_!NELJYNB#N`%b6uU)NE`ln@}Bx))&p9Wvj=nH%#Z4q zN;M{8x{+Z0cNv8GU@?qB5^>g|7@zfm%;I=e1yU6C&4Ms1V`~HEvn0|y+d&W~{1O-! zI-C=l;0l=!(x1>xVoqz36PVYF1Gys!_$5fn!aj(^t2m+jaAysQ{&&%n8&VITs+-Mk zr3;LVjAWb_mz2!fvO)-$8S+?t~euPvCEGnfOV$nbK~CL&?% zi5I+vD9I3yoobLua_o*;ok>a0e-@glm@1nA4gqJkAq#J8MGUkVAP4QK7x^eBG^~12zkDLkK`YFwI4#!*c3n} zJ_DoFU~sbx+b6c4xmn zmHAkCKTQb~kM+s)n$>Cud&kHnOFaE7di3un{t`|ov79|g`xI0kUn3B%&W zP;XR#fH!B*WexOl`Q}Zd$?fC_Gse0>7P)@=1CRp%ORTz2JJS&4l{o|Ut-u1r4ebpK z2oSsQ_8i)JRovF=d;iu}46H3NV{lF=8?Sb*C>w}dq2k~eVvq*vR~go|YZ>|TRY1qo znQH^3=Mnf}X)L)=TlM!BzPnZ5kZue+_W28cKO%-2xKt=z#l#FoO3T_w*p?+?*O7G6 z_E2CIAdWeD<4_GNG+Rl5YhT~}{_-Rssl3QLgff?mm35$xhZa&P`RB^vMgL1r>*P*o z?5*Mxn`xqmTwZwMAWSz*or$$*0QAB=uQn0&ieV`;aD;AO@2{OgN+M7Ku`fcP^^pE7 zOzwlJvrB(if3^$$p4M3ggDOa}GK9)83z4f{CRJSTMm#GNtK@#i!~46fLG2(9_of}= zxiENToVdUQZ#EBjyin+c>j@$%sjzvSs*M(EOCSU^>xQb6Bca62zpZD1X~)8UD9*}% z04#$fSdQcwMdhGUfAIAegLdE5p1GU&u$`efg{;8nE2tXkc3&Y0&<~$wM}bIJqAm%( z4B}N__oDL;?5?dy6w14J*u$zvVpDsbX0kFOEGvlza`%|4fXAaJatbTTbRg&NZ<)ab zI~v&^sU#xC{d~x8nskKyfBPb4p%JDW=JSU7`mo`o0fhcmKj#C?PEFV2erC_5UwH{v zhI!#}J+r!rO=bhx^n!G`0&@aj_B31C=^`sKj|D+dB z=ttBpKsP`RI%Lr#;+&|Fb1(z}52QsIhrh=KZ!0V0)tcNIg({28aJgIWN`lmxJr44b z?jp<9Eo;c~%w`5|dnXBUBAZBZ)r`<0=>_nj+T9^NN@rz6UyFcwH0tH@;O;}ZzrUX} zhnVWN!6n`cJ^$};`dPbhCX0qi#>~u25QioNjt&k0l8vzk20=dE;Tn+Hh=%6E5yT60 zI>c?@_8n)$HUMPojJ>dgG?a6-+|PFL|FHwIRFy-F9znj~wFvql>i_$LaX#UMYM8@m zFBb{3J^AD3Dgw1e7!f}HW5iSpsdRv1FpCJbzkdB1uAvSmczvdKn^wf}>$ty1_@RmI zH`&oMU!sf86uz>UWTiG3KoXQUF`ovj*(fE_1Y>4{ja_Q?tE~=qk&4*={2U?VG(Ux+ zai!^w0)$Tc+W*~-kZM-k<=L~mIF#+HEI+VJgFjAwS?CGi48%AHDVuDAAb@5YY;JD$ zwTGIo&F1CHmp+*!@GE%1Ar0{nj2vh&__uP$f;A$jKX`h!K&2zO`QahI=eMaX#5dNk z`VLgm03EPw+QPw>tOO;Y{2(}B>3aX$;+=~-@Fc+&2Of^GK7xKB2~Lv};J311hF-+F zZ}2YeBNkzRoHUPukT;%eZw9ms=`UrKEkapfzM#k9mS~suk&6SLkQYxDuhpn|2$_XfiqJBsBd6{*1xp` zi`MA*zk}u;9{9v%!+1}fTsoQtp|U=cty(aRO5R;WD$P@{lUxa5Fv3hq**z-;E1+g; zhcG2Cx^fZl1k?q%As;-gIBb`{l`X8mxzP$H4Q<>=ZLr>%y=D8iu-7?j!Ia_9l#1b1 z2hJ`vus@kffEtQ2XhFt_rIEg5%EYi$%fL28id_Rt9~H3-{t6p zi+%KlvI<=F{ypnRp^xfI)6Pzaj~@jmLVZMRf0Vl)0IqKTy`5OzzW7W+Am^5_N_rQM zPNEMF;av6Pq!!0YVXu)6A(Y9Ju=fo#{&)2d;=u*W0*6>pFwCR^dP9tkt}e%F?+EO3 zel)7Z5=4&63_wV>@yVu!P9idUOEEH}6tS6{tax5e4jklbRu*L7h8yj;%CSG`ft?6a zZ@u9!cd5a*N&yNjQUFO9{i2?{dKWrK01J@f?>?}8Si1llI6`FA?SM1)5#mtBD#_J@ zn&z}!7D0HEeEffJpbIfx)m*r-4vbfWU7Ua(7n}{XajL!TnpjNd%d;Iwe+Y7A+$G;d z!2Jv@KXYRH`%Fd9!%C>t)72R+p@tzDGB9&%d4z0XPO%NCRp%3)3D(431t%Beo@uwG=g0^9RsEmc6+zeGR~Bc+E&Z(&WcvSeQNrLG zm!%?19>kuoJ*gZhm7++&Hw6B{M?kss2N0?v$_`cuqw|6FKj^U)@&7F{>fcy`25qmx zDdX(^s#L{h5~RY%LCy$9Ati`C2Lot^!80u$VuWC&9))bb;P(&!s^8|(GOO7SxEfD- zN9}>T*meZG>ril9v4{9zi5oV66lp}@0upf5dze<68O*Mj6^*lqk++}Fj;J*d5O`n4 z3yXa)hKrnBXxw>MeW=tT0hvtec2sD6KacQmXQ9Yp^$TOSh)0|_eIW{iZS;+Y5^^HS z%qXibn&RLUarXB9r;@zleHJN)lG#C&7H(Ql4~HcW!k(zBk!eqz0E|s?x!&85o>-1Q z?6TR;#+6*((9i;fJ2k@5Gyy(5;kj!D1F@v0GL5G6wJGbuZ-JMlvvJCm@WU` z_Zk6X&d1!`qp}m5(9|+XhQ5%KL+~{uJ{?*@+RZ<+W10%^Jsu)+d6p`G^X43Y7t+t0 z!v)hQ3l4RUcDToncmKc&@jVHMqNA-#ZByNYa}=?vi!s(pm8%HhDyv(d6fy13UA$jlqD%nZ-wpMofp ztaQY|=D(Gxj#gjTz^_QK$x7g=xOfZMkf2Y~Q1=5@cqZ^>@PjB z2+zDNZ&3d2RBP9O*7XL@Y&Z9iB^LwWfGFiy?v@+GP( zP{Bjez2SJJeNwb=#SdupTFusKpxQ|U3PyeM%6OG30L^6o9LVt*UcblRSQWErM?Ho| zx?)ZO)=2+|Rp9LBMbwP{pQzbzxfKO-$0k7H8{MY^w)(<~ua83Cjdl_%&eBk162*{? zA4hPfe^LtwS3QOV_m81MY^x+7&APsF8DXU>s<%qr@!4X-fs-a{3YC_Y zW;UO<>hhj+R)h85nawVr zNwrShoX`L#Sf#>C_sHQLKVHXwge*P)7<>IS&=Vb0=^a#47FVE;X@;llFT8lS;GJ*r zeKB)Luf*I%z@14|lQAdJiabOPkMXo zt4aIsrX6TPEV&F%OD6clu0q9VrE2_H2+WQ~kn=6`W!?{+V7RuY)6{>PvOOZ`wN(%g z7sSZ{%8(oEfJ^W;UH#9&Dq7r$oDlA<Skk*~`KM#`wtJO(5PjagfEa!YS zb=6ro7;>R0J`8MKMWc)$dZRx#S5~>Uh>x1ByD%a_-p?n6(rWdlLSx5Q?DGFxPoxvL z%f^OR9UkKB?=DNmH|~V1CE^PaRA28g-Yoef#V{z?o4Lg1IbK~Jk*-iIN0xQ(I!sR2W8KtO!5vA&3d^K`?tYs|Utn=n#AG=~Sdr5}VThkh&c5AUuMduU3DlyTHO7;VMnEq2I zlB;4TO&|~Yz(G7t7WFXr5MIHe^Iim*&43zE+$uZ=Zc#?l9&B_*w5HMbrbbY>Sx`*|5{tSW*$VbdV57*_I?4W$3fV0=D~xml8io&segNC`^--L zi82in#X7s{=m~IqG7JKw#P+=#1SG9cJkG$V$gZsb23l5Q>A~|X_uZG)`+K60So7q= zBgmlM81`yAbKQnV@>1^x|5P-_WlC~;slMvx~^+hU=gy_ykg~c@x*Y%n;Z?Xm8Y|} za(61S4!yqCCDXCmoj5!QeeE8-Dx2mDP3P@k!`z^+UmqZNAq-Fv7?&h)3q&e&CxCMG zZKXeQbFS9;c)znb&`a8oj)XHZ^)m`{xV{=5R8g*QG7kGh+H>plP;(y1=}AQ|cQ%DI zSojlN5>)VVklpAi{o&7XEwDG;0uu`U2e~#4sVo8~Fuu03pFrx%3w_(~Fg@*Ep48+f z)&--h4m5F+{uKI_>2-{+Jcou3ZuM0kdwPSb%a8KxYeo-hk^;g4>77*@S}zT5F;SeK z;&4H^!!ved+K=0hwg@);NP0R0{|X$NFY1iHJSEt%sdvM%lM5`H;^bYtQ6IXCa1VGPK?0o1~!ltnzS3;}cJ zISN!s4DZ}CJfF~f=-U#@x}Bl3ui~c~nliA=E1^~Bxm0&j)`9~|tI=ADVO7GSA$4`=E z8|=(Yj4q--m4sb`nS_KNQN7;2(_cI9MPF@D-bcM6@7hRc_bq#ivlBEvkU3DEtc34E zH!G4%A|7YnTK*c>XZmUZE#!k_l(8SK54$iXs(TQqFrlqaK;K!1XG|ztyw!Gp-=$xp zxuu=js#+Qs*B6U3L-@AvB0>9329@eAaqx1y!A`ikaJorZDuAt6UOCr~X_F!6c;Rv( z`=J%;oc-1a3;E@2OvY{PgF$y%IQsv4-Z`m4busK4e76;)x*Ca2Ik6H$6DXO6 zA58F>ZMMO^@+X>6B_GBtES#goi2^=@T2^emO(9r-Z4STgi@Zdsv?|6J| z>OfI`AVbu>W*`3ESrI*7Z50)v;-gYl!SQ|PYtU%&wnFed|CfQ!LSLip&Bqn(UkwQP zMO@u6SCD31roGRgqx)9z1Lorot@4o1oSi=#pU)yhjf=N3%q;ce`Ma6!NB6z!tg;$T22)v8iNq zkdw0ua|HIe3zhy1D#11#>CdeNQn}ygrM%$3@R57d>!|!yjLDFzyh^`*>Q(3^AuI&) z1{Ak&bce%j+tf4%_+-NeX)C5W(o9higGx0OTMx>qn1r&lGKRkem@&R2c`h9k=We&m z>tu4g)yv+B%QNvr+$E+9 z%APVhZ$CBf^gdO9t@nzjf`V2dB-7xh*FXlR4)Ko)3htfzEkOs}Qxx0LQhi-7jogDh4T=v7Gik_h09P|;7#`|8b#IVffq8cuTY_n-uYo37z* zf30!OZ&Tx~v54I=kg{bt-}&)cF&H45gqUD+-3k?EYLDiuiHLliKyGr<(t(Wka^5x= z$iMycv+rs~*YuX7QiS!wn*%8y?OC1stsTKDmwZJF_Cyv3)1xc-Tq82aZ3O6eXA^Pm z-B3@_&yr+#_H}tQUtHK=cX0#CO{*7m3mI~~d&BwGhY#iS`+VJ1Yo#D)I8(j>2;;|^ ztjkP?GbdRng}UK$Zp0Wui18m{_(e)e+5`px6vR&J?f@l@jg}xTyONT@;APyLu8=VU zGU>-VzpmUS|8`AgJB)6JSZK|}X_q`1qZVSoo8dn#`fONRr2{xxY$}09+G3u7yBagWU|YULn9GVn0Fb%Hez7jS#LF;Of*~gkeYLMImfr zUK&7Y$QSP2dd`Ga(Jxe>D(oO_z*B;R-FuY~1tDSfQ-lNqTU6)n#*cjn zcTtv|diZ@~AlSJ*H!EFhX~(5PX!ZDY(sf8_>w6}!olCXY6+F&oWKHo?{)nqkv$Z_= zW+1D1J^_1F=D92s%wMQxFY_k@?!Zhr9Is(&zm-WxG>AbU;_NS zy>Lh$Ig1JN!le%>EoE=+=m-IvV=(BZ8&Gtv@OHgTBjt%bYBVI2XlQiS%np(pVSUnm zmD|+iTwmd!>llqD&Cp|pqUX9MgF?yAJW~oS_Ux;|n`>DW*d$O$_Hd`^rP-0Y745U} za^Xrt`BQqxpz`>2pP+6yu2IQ!(&>E?VMZ$n9;GpI>w-55uH&}}zV<$d(Ti#rxXR+r zr;d~GSWUIOD)(e!vUpopK=_m|ZCzOx=_Tppbh zzuSq|A3$z2|2@4^=n`5DgaWB_RX+PB*l<+x&P>)VQ z61_+zdRjy;`=_Ad@y7A@fcOSV=UdZcH<+{J_6Y0NH5_xpKWYr;lv$fA3iE!eY4594 z&)fXEwb5&8DJNDwtrH-~ROHB#LlVb|8n2$0N#1ucd--`fp+QhN*=fM7$8vLHwqG+q zN2ABZYD2N}0*d~1m8ldKN`kX0^-5}4jln^e?e?rY6W5=7MYm2tfsKtqR(bdLo(H4p zp^HG|ELjHB#^rch>GmB3gl(S|m5!NeR=uzH**g)f5<2)pt%g(^w9_5U!|>~$L?^O? zV+N9%_)@Y$3LKb2j-31_wfqmdZ<5QXXdFFbDj{g-d%gF@%~iPq^-GnS`4{s^Iu(=R z3Ihv|^jd&cL~QEP$fNPT$9)a7YaJt0^See=aSxyAHQBww{P`m;(YMCSGbA8bk>NnQ zGdo9Bq?@Wc7%1gEN^F>YMq2VU)6}2}a^e<_-hWk<;4dSpZ)&N}*X%!CW;WlwVOL{5 zu9-HglpO6CyZL$HH&6m+PfylPkzkK}?M5u7ENHPTfvVC1`sfFMzunN>1)3P*SdQ>- z>yuG6f1ryG0UKXQ$lOuYM(Hbks9VfrqWqvN2#tv`P}Sp!Uw#<;a66J%dn{*bGIb`j zK%24(8)ldbUlZnRjFGT;Nh!z0SPAvwRyC^DGJl^V$Eh zjHy`tS+;onE|D`*33WQoR_sCR;bJ@7Ig&?GwYh%coypB%O;hp9`M@T=$b#jqV)M<3 z`a+`{WZk@xQ|A46MXFI|C@%HE9Ovh+n3BIl7Vb~{STS20^vACz!t3`aVX455ShJz! zyK#BTD!B%ITVMk_<66Ppu`8HZ0dNS3L6Z<$&U(plWw1RUDuVt)N}W`n3N1GW4yjGT zkJ)U774&$T@69R|^PlXA9mob4p2^y%%nK#QEo2rwww(Co%To2F{yuj&Ew+?8*+gi; zSWwC{iTB>iU0r!A3k;uZ%{8HS`t>>b!C+fai*;hj!WHwZ&CJ&PaE5+n!r`2A_B;)J zyYaeJl$#M*^z#$;56CiPjpT-}=PbCm$YN6p~7sLj$ z)y1{?KD16~P;Nr|dUV`HT>csIdHwWk+Cr;{j*Rq;HL<-_n3=Y+YIfku8h;EA)O>K* zTQKuhN~6rLv$>V+lK=3l(zlh*Pbt6hKqmOX?r9K32g>N3mTyJm9OIxAHZMc_0aw!> zwF3_s?2by&8aHTKUbmKevY1*Kd0zRl{mOFr+Oc1GqzgxPtm1ODO2+{agal_|`{G>ExFXw8p8Nb!|9 zl5cv~b9|b+FHM~h{kpj#GmtkaM9GMEx0QZJDbI$AAtqv*);R`cLv^fnA*uY+NRhAF zr{rO;CTc!&#?U2sE^Yy-wH}eBAUr7|e4N)9`8C{C(m* znUA;z7K_gpk$uc^cr7RPO6V6)iXCN<>%n14s}5kw$#h00vR!R-%vyZTvu4dLv^D-w zE-~{ncfYk&KIL>zc&2qcX4fk$-U0@%1Z>#GBx#GvdNJ8s747@)E2a~jy6tBA*JxS! z{k9ACIxV6yxJAmRYZ>f~8p^5meKIMMw3pD!rd z_EE7R^*i?fP6YnKD*xRHX^x00UO`AvpV>Ldm-@Y!yL-hsJd8%Vl_;ZnK@@}8=3X~i zcL#bSOoXbYduNk;io#0`s0y~v9QSrJ}infd<4tR?ijO_lbWZQQWo1uPg$ zn2;vSp4*xrxGl$7t0mb>kT|;tK1r>M5E@T@k41Ag-i7=jxtdC!k9Fh+yJaaa#{4f) zJdf??#us^w%cZ*M@AFud>1ukGv3p@*`#Z?Ka`~fT6!oY|{!)9$+o?t$%fX=8->t7n zp6k54#KxF5@zS!+QB)#T8LxRF>P0)f9OU;bR7h5klC<$!aoDxd3{|etki)U>rb{huiht%v z{i#*L1*9KDnlu>OYE-vLkJAH(Jt%!3-6aSNC%cTWdDnQXN0-?M1V&I*CU_a&qtAL|6pr=|S`6(`qyPltS-<{zJtv zBK$#7z07e{v*MDAo_g71)PAl?d(JrcI5g(!-AWgWZ1`T!&D%WQnAx%+PtBv}k;^{S z%2Q*AO%f2s5MS~);jpfJa;WAS7;@xeblhcgQnH%w2nfZ;?rm#3S)@12MN2JbX#jjzyk`zJ~R6jdwd=IL> zQ0l#MLx+d(Y>?Ahuu#`Tpn1Dwh-cnSSg(4%i^g`uA!H+1BCjDq{k2y7GDoED&aB{U zM~1ef&YxnBjs70Ox%e=b@XGk!6DCJ18=kbz#i2@HC;QC@c@eJd!&>3r72dA~bgpy@ zM04jv2buDbr;`yq#yp?qeFsb-Ef0zL}ph&Cy;MwCH&e$wN zhX#l37}Z4mm)V6)zF#@4Y%8J$)nj%tRB)TRzWwlS-Vk{IK_o>;Ip3lA{2yVzt$sAa z8iGYoDYkSu`1H|m3Nm|HBM|-KYN7>ehdFd zhTXCCK&jwX7P&%pP}}u^MuXy5J-YiGg{sL;5sRrAFV*9*&HRbtIPxv_ABpD6UVPJ0 zeSPs}x$2H7CzaLn9r=a43y#*B-$)$UXabKTyj+Egn#SpQo@0NTxc)KX&VX-;7ooAO zxL?te)*NNrJ z+&SK1>5@=NCT5k7@99;oEpxK4eyrf*{ARzBfNu~*(Ae^rc3QgJEMb+2(Q1Q?spK_@ zU0c(qgb(iHfgCCLnp|qOS*lbOk*{ObJvL`|U~UjG6v51|S+G;&vak{LQDs7JEq*{o=j*W}ojOn zqvF=QsP4-1kWd}Nz)0F17qa*0Mmda3C6$JPfD15J4W>(t3_0{Y zglfRTAfwINdV_1MQs+6VpOvLtklZE#p&1{ex602)6U^1o_XpLF=nnVwGt$ImT_+sY zIR82N`rgQCNb^Isb^*)Ht2!gyo{XuIJot7hr*5OX>BD6#Rp&3ijG-H+!@f<<*YTC`evVYyoou*0s%oA&6`p>kl?tDu>pU$@z1SG=O z>1|G{7os+#dc^Q=OsIHb6)6$^qzG;EZ!d$9v2H?=)JnW1Tk6e&R;RaO<(a=Mngn4?hf_@}(#<#Cqg;i#kt2=QN}9in(v(%J zQsZ+LE>%3^t6~;mO-^o|aq*xUrIczPDwS4jYYw?$8s6-4yIg7SPx&$Jn61oCXcJL3 zA&!yQXL(Mv8jG09LWjIUKSOf?DH$c z80F3dd*HiCNG)wQBO!h!>)#={nyrNx-qYP?b@sAB-k zhn6NXqhd;k*vYr!NfOPhwJ!85r)y8MTe+_pOi(HgQ~Yc>2(lguumzvyPazx;J#G3f zFF(FF+HYg~>=%TYh7UVp$WPo0oz$-z<0J}PXqSW)9FC<7)wd-TA_lz}QQITQ$CmT@ z)QRN__7~c3jj8utmhpIbYWK|U_KeGRDCxe@&}U}zEO*VY!BQ%2_Urzr+BYrRp2hAC0x)n{?Bke4V6gazG`F`N~V2~b6XjijbeHt`LSC0rZN(HIRDel6!_ zMW*{QGecL#W;0i_uf0Rv;4u$jMU&wvT{0v^K-z=J zkU=d%jzcsoh+F4gK5(a>kGq~amjP1^iGm4)_?ok{We|dGfJ=N}IObb_TMUImCZp`3 zr;^>n>EP(^_~OQWH**9$+2}35)2LsQZh-zdWZNi$Mt;TMczv&Yj2D;C0=1*CQt28zEMQmA=DltTY zs7n)iP;&#P{}o;K|Asv^UOFNcs{!HRR7eU_UHf((*r49fOj-nLpEC$fW`R{6u(f3k zjst;U!02ZpTid!MjH~N?@r_oY?(tH_)cIQ8LdTsY(UgeEC`+O!tN0<0ZM!K`j=ss$x>SMg!ES?v{vUvFBC?=`%&xw6z$N*+;vYe zfMH2#IZxqp)BofjzV-~~>doFhCzB*C1`&=1o)h9p8nzi0NVL-ee-_ET}#j#R8C%q!W z%f}%}?oyya&=BO>l{0(M41c^>E6{zIZIox~{_p#V?DiX%1nlGr?_m{TmdV zK^m?j13vLn1WruG*1O?VmQ(jO+wFA!Z14&WYG_dKJWEe{P^ib|6a$=b&i77R3oX02 z5)u-bJ=^DQ{IX_09w^jKwik)TxW3g`%qnImSWM}L7SDfFqggfBr;_!_*XMQ1^3r&Q zeWg>ORz=%L?3Rs1;l;E{|{1xMb`^fhWiT4iiXN;X2$6iTgrKa zXLj2UXUoFwC6;eVWll3~Yo3ct)iffgC*-9oIWbQQ!Y+Ki{^awhG5PTo341z4cEr-7Dd5eO3PNTBh~iaQ;u$Iw7ZDVN$qlB z$JS8UegGl|?fWL>a}7NKHn{8F8pHJ6*Xu8#Z4c{Kwe!ZmoeqQEDnK`!diL&}rm1O; z{LM*=W?CxDfC!}ru4Y^Y5Dikyjus06UuZWb-Mfhu*ZfsaOWLfHlxXpdE}&@)rpab1 z-U|i+vRq>!Vbt_F_U@@aRkx<8kHQU(XJC5&IfaA=tyBO$Q_FR4y~AVF=6(!>#4+cY znc{cHxeIv4q>5iaTgO_#Gv!Fq<+gcxPX0<3j~F z%REh~@C@U$Lvstq7c++m4xKp=&D?O`U|dJj)%8BG-%PsDY>2QA_=o!`D6gWij9MsF zNM*>W+(P(R>lZ<&S=)XtU8M+>3nzq2aHSBypfpNYMur)Khl7}hZQGx)Fpa^2`@y6- zPiZjJD>CIQKG*gJf&Nu{4{nm%HJDQ*|EW*zyRKY1`xM@Q+LkB)I2UGL<{dq3NuywT zI8@3uw!+=3w8Ao2$H~0j!g&sDw8H-FJq;pYl+Yt&Vmgcq7nKE;tk^Yh4U}DSYd-u|hMv9sHT3Mn28G zn{f|&D$Q1??41GhX~Gb4RgQ6Z;og08bbcRI;iXgW%Wug|A#J&NttGO?-0N+bnF#e2 zOk2$t<@&Fq#Bva#A8@w5z`}p)&LZ!;b08mPZR_4FlI49YT!1WbGE#Pt-^L$Xzja{h z8UV}lzM6wQ!_7FxSR0Q#YijcW1YWWqq&LqD;U zhP>bwJjf7*pMATE5V1fm)C__}wOM*q$fwc)Z9T}nK>h;e1!6$ZA_ars(S4h7pgVv5 zv`(Y^9Q}u~va;U3zrR0GjuN;jBe{5x{`vm>`?p7>Pp=lE94DKN!$U)X-dbwzow24b z(f0Y~hl6qyO7jH-je#mn!J#Z0u>wctuV>cZ?w^ngEULEhq|8dfu~+h>imzB~gBPI# zqPekw*-lE2ySNgw0pdNp58$@Wd`A>HS`tW4X+bkbk5g$W?2(3$fM_5wZRju zDs0jeS`_j28n6P}Mx7rj2Z8r3_tsz8Lt^`k=IKFb^KYvT z0mPcQ5_qDFuYO=4tk^5@E-*JnM5+Vt7?De}j6riIBRl1~Yr=&{wj>>Ftw%)-GL?byZC~i;#GB1zlagRYm;wh7q$0 z$qr5wjWBt-p4rly86fc^VLz=-N5EQZEs*a=fy&{fKyuAogity0^Di9ZPQT z&|3K*yj(>X#gVxm)M1=ek%F3(VDe=dDRn(Px$9zLLsJqPx!cmO-!N9$5!Ox6R6>nB zF)55fOzkXZqFAjk9i^L!inL#8?;Q58**Ux~pDr+yxri_O@S;4Py`*}h{ zgHV=r?hYoa%|h4r>yHP$*^9uZ!5-oUb(eqTk>eTegCeJGedC^4=`(N z=zb2~;a0-6!YNS}4fwkMS8-<^4t3x5e`^X;xlkcXh3u8=yHeS=?0YiFGL&qQ^(xsy zb|U+}ld+TNlBKdU#xg4Vnq`RaoZs%_xu55c`>)^e9LLXJF=HC@{d~{;e7z4F-iX*) z3vn55pBH^^7!Ol)i4HzawtG_cxdq1zK;c*YItCIo?op|Sz3JAQ$Ovvme=^Rv(!KDQ zp*-lwSA|3ST;*1YMbj8XBPdJFo?UBcuCes}l_OwW*{*|CApa~yJIQw|eJ$zGTO>;6 z+3vb^BaiSYKM{Jr(07+M^0J`Ehpk5>h-({w#+9D!t0YOjCu{YdxZql$EiR=+Mut_@GA#pHz%CDg>2G>YFJz;FMCs+TrKpV-Sn5Cs97({8 zJF$o$QD!qqIp_R8!;rhN<5N33y9hM0tUFr)H-@MpBkH-`Rqdl;=5S=iA)HONz9-%2 zsUjyz4v`8Mk?UVpL%pkpq0QHw#&pa)chN%!dInKymg2Tv&(fJ2~+oCF2TOFFVXP%V}E9!Ekp{xZdaZ5*`%Rp3ocfK6zrw548@-5h3HdTO+y^> zFb>EMw^AmrH=!`MNSgWg@bnVwJv5v!@YcOQXl{@v1)2utlPw-=7RbT}tDnb0|E(_o z=5-mVZk(z2J{8F-{a}*+;CxS-{8a3{-j%P^y@oaxsAY$PacC%O!ka*^!hUod6dQ3u zSG!Z+00rF4&B5GAU zzZDr`SvTZc^5RtTVsaZ6a;43uzDi1_mNs(6b*ben&NlCpkl;WQh7%ViQjWTh%~ozV@US*075# zTbgNXf)dUsYv4iAL_F3q=Uk%_M@fw8EgSI^V{(Nv`6h+sKKloX38E&8zhlEHy(3a( z&{UCc->Gs5JBew)K_@u!24|6kbgliYh~rJ3YJ9nEu&iLF*QK9~Dq|MQ#F!p8@P*=WnfK?V3yipG6#S@mRr~yBqZjP_JQf3Zp*7EwCba{#C zKx}dR70Xq!N(%+aYK)tX`p(`?{)edOL#cO;;#eE*TFNh+&lO#f_x@{kYlV8gW4 z{y{dZASk$h8yec;xvQ>NygoynJO2%RBh>yvp?fxW$O68mtb3RNEyK6VC|422R`u-H zhLD41Yi1P+`@AfRhhE5)^{W!82Bkm4*1-DW;K}Y)$cu}v71Fi_^(-RXS8bbTyc#azDt~6mugu*s zyOP992A!*_+PZvQ<7(_==j(pqVl(4^7T)elAQAFchZTej+JTXMMeX zsr1R&NbC#lYMo;q#(Sx)G_Jj)J5vhqiHDeHocIlVX{MabaN-6Yo{_SJ;@nB(gN7U$uV~ zoGhskkl9Tc!*)pw-&!g*Y4%z}{98gnQD=1}4Mb#7l*tDCb_sF(*86^6$xt3rr^}mO zM&L%Th{32kT-}Hn%B>rhFX(dEcSdt{j`4+QyYY+OuYGWGq<0(4-dB{GmYRZa7SB0Q zZxg=8$e_=CUxXUVdskOc!ueb!nR&*|-X_8f6p_aTrr1y`Bjv8B7=u)ewL`=}-%P0P zp|>+|rF5n`#kO3j43w3GZw|U*nSn0~SZ}k_thEH@A>D91HiQ0z9iDfzSS3}7sUAQw z8u72|V5Pz)#wJ9SjfY;iBzCA^$MH)&*|bKCVSob>YpErU`QNT>i4qWZBz$10LsuXN?DN;t@S18dI#l| zCNVZjGSPQtZPARM22OiTSmH7>nDT7ZmeNTQ$SNdf4n_osKp}A#y&}qBj;pkgHN^O# z)`&dwg@hvR@%AF!#Cw!v~?eMi`t^MgwPw7 zyUN2DqWWE&AG+|MvQVgVd=E1n)hKsWd2u&gw<^}*m|K&DYhR%0{iX1jJ0HjP z%QyhvHk6%OS`D zfAr_(G`;&wucx%TuwBMlNGi)HN>tSrhwPk@D6^o@hD0;i!>d}!9dP^!yBn3TI7s#V zxI(@~lUJQpC0UImSLrP%jo92h%h2Nx`7Yn-ZUR^G4t;zOVWdn?pvVv*jw&$Qzvgoo zC_*Qh7X;-AOJog|Q@C72?8B>VdyCI7h)+V&&W+M%6SfIPdn8XBs9$d+m(D@7iG zFAhdg^nIp(3wG&>0Ai-w?q7?;@GwEzCVcM}hgIGw_K0VutK;$feusn>9lT1ZSv)@{ zMoI${dH(SdosL<`44*cK4+hBd(mqmq_6!1v&Df5Jq*ST63z+ZJ4CS@8wNqnk@=op> zHvhbnU6Tpx-o~%71bpuhjMBku&ehb8*9}Y(+fR6ejEPywg>}bVUcqR+bzP#X#!NP@ zM$cmNG>sM3%-><3Zq#loKfw>=38pv4lVSw5*t%z`Q4|8{UvJ-jr_nw4)<@~|e(=;j zXi)w@G$;%3X^+moE%wg&08X3cWN&9$*U|w0AGjPq}(w_b4~l@D3`U0gz>U2M4Kyqc_eE@8z95 zA3REI?7mXNHz0rfuq8*sRJy|3qaFLJP`|p6-S7Nwza?`I+|SP^=nS>Kq^@gPYkHDUVgH`jIwUq;jN?xFo}kDKdz ztB2&vz2kK{c#FynmazQ;iusqeZ>!U6zl4ur_YBnpw0?eD-J49Xs3gCedmCdaDmS^I z&vRsof1S15Q8O7jM*N1W67btzT-|Fv;XK-W?S3Y<P z&9{y(n9UbT**%YZ@SAgI;b~WWQ&OiI>lzL>mm^m|vn65-`}-4}!H?ND6Wcd`{=_cz zb!1`%rCswGn>J5+93*@U!`Mc?)xpQJ$$Sd+Se`FsxA^&r%YF5HHKxE<%Ou2UVthMm z%GUwES}$#nk8bkDzwp_wUFx9!%duOq2Ht$5gCYM!OIm}k_0CRwZ|1E*?8nuHkuI6d z@P^W_V;&mGw=v$WD>bHd2^Ov++e{I}q=ZdV>^thL+sBTy3IyTmmq+fh6X{y+fmq)~ z21KlH`d?sCI;IX#9F!PQj`$uNyF=H3b*lJ1By6i|{jYKW9QgIrTK|7cFWvzeux_)l zpJR;t7sag-9Z$E}__bZdP1TUM+2HOx|DP9LzI5@s1aNBSm|s6W-zpdQ{rES!SYnkc zDyM8HJ%B|OxM^P94{39DK(9?n`ti5tL-wOrix8|8@}*qe&MIAO{V_D2>742s`3q{s z%ElHgu^5XJCr+%A?VdPb^HpkjIn_HieP_#imkz#-iP?LWapOg?{ZL`j#wcyhoyC{m z-=I~;grm&+*8V&T<@4v4*x_68tzU!xmeTWI`!}GJApLLAyxsp%=xhGBw4ndm)jwPS z?=x*D?R#=5<93Og{k2L84gv~@q8!nq#dMBm;n1iD{Yj(PN1<;kt$Y3kdM_n8#b4GW^%jPXn+aPP;V?gUb zQ&KYo*UP5?A7g2AS~Xt44kk1SpB2ysIrBc8>v+c?~STVg}tvk?KE;}SdMG^MAIS~#Jqo7HwIQg~ju+@Zx z%M8ocZ_GWXU${g3VEX#vtxW7x#r>B^4PavqL^sUc^g}TRf$w>Ax1jH40;Q7i1l5qg zxExypDPeizGJvmAX1Z>DxaRT};Z9KoxN0+~S_)I#rdvWHqrF;R676zJDs2F-ykb7b z3W_n?z~DmUw*pn_r|7k0?t$!v?A+4H>=8sQ%ws(B{X7gHn)^j!<=KJ!c>s!a!!WYN z*$u<-npOS%pG}IPewS6f*&fSx?9(?n6-|To#C|Dr3HYT^g>k)p8ifeZ?qbCeew3 z$cOuCXyQ+R$c`pnyI<`2^P2&vPSwayLEN~sc{7MsWrIAymt+a@4x~r`L>#Go05Oa| zeGi0t^YD(Z$HOlcGV&W3LBSD2cptEOlS@jw9@74j8!xEEJlCzTY%ZMJYY^97DKaGg z^a#LLHBbk@0~f^+1i?)QK(nyr*DTA< zrNrA|3!5&W#8SWiS~AoqouCaJ{bI`&Um-OUs8MoxLxZ)%2<&{G`~fe{2cUv>NE0rw zno>BZX3cG&AtAJW*J3vQ%;A^;q zP9LYJ^x}$=T`)wao?F9Jb-?mcBVhyNygCr9Wc(Z)XSET!PY=5ohRV8O4)y-`i+Apd zy(Y`r%P|T_eo}yG&F8jS3*ZV@;u|{?#GEIW<&+dOH~o%w{G5ye z7U%6B2HKyp8x06LP41iSD3z+7vLKxf_S?AJU13&t3IU8woWAcvl|TFq;$n$eMfJPH z7P7;_3(QhKbPvookLRzt@}SSy*(5qqS;rA4_}Cw}p~N{G1U**AiHIO0x|=sDd#}H+ zdpDuw_lpg>;JW9>hMpS;Bg=?pD4n>*ezR+A&X#N|qlbW4b5bfd^!?}z7aw-+ zrSFCDRb^Jv$MG9}fyevRnKGG$%B)(r7mrU(-I1C+D>&!LUZGkw;P63NgCkceE5lW$ zj3nCrYUu4_jq4w1N`eL|fPU?z zi)+snsI;dW(bG4V15yaXrp_80FERwE4;FK$ZTS{Y?vnm_V`B6s^5)kY?WfAq%h%ub&N@}0T%Aj`!y9luD|#w#XYe#%+*K_k2vs@6iAchNebX9Saf5c$g*h0ifC zzWQt6)P3+rkLiW4p=hi_7Uv5s-tFyybe!eRO$AtvT!FmsBzAgK4h=^6b)cfuXDCfVzcaxmtzMZT^{>n5_~1ce{i>26a;ZWyUQ~Jq-~!B#{mM>s z>dA4%`qK8Ewu#uGa{#=Ax47#e!1CUKTJ|9Lv$Vy1zHZH|kNM)EM#DAEmS=bUfGd30 z$1&})78}2QVeBu)03D_5WPWgEuGQ5az`R`alqOI~UE78I@)EGG#!YzFdIVrwPW*@(pbEcs z?vXKxYAHZIIqqvJ>NYUu=O&LHw(}+Bh%ex5hi*W0Y-Dfm-op;!HW0{!t^YwzR3Vhd zj9~|9U-u`Ega~jA8vqhoV$&JzDEos4AT_~8Y7RgYU8Tr^il8x6tqgpqVBVw$92}Eb zu2&fJL9pPwBAewrZ+v+NC0g+SMG5rSi7C=xE&vx)K<%)5NaGpcCbO^uSG`je0OTsy zxdAwZI(BkB3$oz2@4GC{jn_O6llwVafjAZ^aq|GMY^#f^FNYscSg(xhcdQkPjsa|s zApFXA@No|n`!hmirYvBP%fU^VKo0x)>o+onUIpqlXDQctDpH!L;h^FETx52g|dmyKZ0}{23tT)lsZC0=7c(6XR5EDE5S}DXA6B;79 zOva*(xe5VfptMs%i7)hTkfYf%19scjI76KY!Bho5m@?^HbR)&Z2~Q73qQS|zuM-qm zz{~HqfURhyyNYD?Z4vqodq9bCQ|yQGb6rdjICwm;7Av>hND6Xjbu`!pK$KNHTR6r3 zN+EZuEr8fpvyBccTeE*{&f!58XiX?%iblabcUfB_ws0&ILM69oyojGa+1m_^n-CuO zBOVb&o!ba0wEB<*dMuJ#OO(NKqE82E+6Kq7e7X zU*q`nT`VPHBNQ~#EuV{ix96mG4YH*zm`w?-yJA+`Yd&H_2{GLR;gjtQuUz|1nY}ji zJJ==y8RyuMp@!7Cn`8@-LCU%p%Ua_9+V|WgQ-%tI4Gnqgg>ikRYzQl> z!KF4h_9+KwrY;o{=V2pqeEv$@+lM2iscSKQ&{;%cWJnuWk7m36``?0pI;O~vpZ#0( z8sg|b&0hH7dyoJ1YyT%tjSA6xD`N+7t3jR1Oya~BgvI2Ix^DZWXHmcNwaX3m&c)`K zn3@YDB%fsdME+6tStVWCk?01&p_xM`)gWDglwUVHSQOG*Bx;d#SXD_As82m9d5w!{&r$1$Pz&Go%=*pSTZ$==lvVmZD^MDBA zC@(>PoWeOWbS<#K3^kHNU)3J8q3CS?H(%p)l)1M$B> z{s^yn`6at=ImE=EQyf!l)fy;7u0H~`m(%zSn{A};PLlMI=1~xzDYh66(Pv7P{jG)j z5ISkOGz4|0Nj(0r3dkoGxo4h_onj1e=oxnQwBn|C2bUx;`67$o z<9KvF41JJ!MDRByLRemx@dH|a?l(WiT`MWdrgz4*gbIc?rariJ+r;G&(O~;4+)SmY+xJoYVTy>@)jF$TX@QU_L zA%)`Hfxfs>JFy-{4=A}cQxdZeAR8G+&c&= zWarbR$l85qPG9QW0Owsl15SoFt9?Mj-7!xCzg`EB6NS;L>EMr?SP_;#VFlPzAA(|?PD1`pj#$_c3_&|K_bgowT^!3oUrb)4| zU5HtiOcr-u4a$PNL7MUa7}MEu?bZH4Ri{xsBy~e|y$6!f=2J^tGDD1)YH| z`3MIc?u!pJiY-kAc6W zpNFHA4(Z#f*xEKM&f*WIKvP|PL3#lfv;dd;=l{OkpztcbT4v7)$}GQsHP| z4uSL78M{*tWU9`1bJxCGLf>_n67449yT6t-tHi8XwXfrQs8O?qq56Z@Z%#-TpGT9$C(7et5+nB1A^ znx}qEGfWd6{Z*&|eA<_fOJKoqd@MOW)?IIC&-U#<4JEQ`O|bg*F7=tM#i zCHtrHBtc`rs(Qufj8K2ijD3+*T5`btEAh!kOb*oT1=t)iSFpOSgL6y3jH8+NWb$`z z+K%t0coloXs8EmB8#)`~v|Cn?Z(aTX*jnp*qcTW}*M@$dOVh9?LN zmRZV6jEm`#+*T}j1O+44RzcKj1NSuUART{AV>{5*^`QTtl(k#~6@h_2ona5KrXHl) z8r-qbE;C)ma2q&+sN`_QO)Bxu&j7w!F&pTeQBI1;!0VFoVzQL+x-lCMy}=!QD;~LZ zvxITCE8&V|^!vogvwc`p-m+=aov!Ba_>gl&KAeA{RTi82f7}K_;mHSgy^NzFz<-23 z!C*9;tuhv$bU_xs?znf?{pkdB8J^G~og@3H@?L-Z}kr{oTPj344iNEh^5~I z6urI&8h6#}rK_zWe0*~jO1hghmLM5zM&bcW-opSG!vk-un?XNn8OFa|Fl;_seTi$A z3Z_wXBVIv=ez|yI6EEyEZYytj12GER-HW(12n?(z;ss5~5U(n>R3`+9&MS%UyC7x9 zP~igw&cCm_!yA!We1d47RClJy`&j|FRL6Ea!o2{j4go@;0xuuiWaDR07o=M=1my_@ z*f)+5Fm3X3;}8-O!tE8CRP#y1)MEADZ9BnjK>$u&z#yMqzIG@+0}`+RP;udIn1bOb zwrx2WBBV)?a^pL_S@DShN~d zJy|$CT7K_l!c1PV{Loi*LDXe-Q0#1I2~y*y*TZLk4_a;N+dpGR#xb-on0+UivPt>% zMW=HO!{(xYb^7u4Yi|KbZL_e)v7ZqMzmm=NXI{-Z*xt18!92)fuPoY-ZP9@>-OBg_Vuk1Y qpBB^qbK(9!*4_VqKQ(dcxBNkZlXlH7vDe7QpmIx7sq}_b;C}%&{OczG literal 0 HcmV?d00001 From 42a79595d2b822ba70106d5ff6451afc8111ff00 Mon Sep 17 00:00:00 2001 From: makemake Date: Tue, 4 Aug 2026 14:28:31 +0200 Subject: [PATCH 6/9] fix(aave): include accrued treasury backing liability --- .../AaveV3HorizonReserveBackingAssertion.sol | 22 ++++++++++++++----- .../aave/test/AaveV3AdversarialResearch.t.sol | 4 ++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol b/examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol index ce02b0d..2b58b21 100644 --- a/examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol +++ b/examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol @@ -15,6 +15,9 @@ import {AaveV3HorizonHelpers} from "./AaveV3HorizonHelpers.sol"; /// admin action, rebasing, hooks, or other cross-protocol side effects. This assertion checks /// transaction-end reserve backing from external token balances and debt-token supply. contract AaveV3HorizonReserveBackingAssertion is AaveV3HorizonHelpers { + uint256 internal constant RAY = 1e27; + uint256 internal constant HALF_RAY = RAY / 2; + address internal immutable POOL; uint256 internal immutable MAX_BACKING_DEFICIT; address[] internal RESERVE_ASSETS; @@ -37,10 +40,10 @@ contract AaveV3HorizonReserveBackingAssertion is AaveV3HorizonHelpers { } /// @notice Checks all configured reserves remain backed at transaction end. - /// @dev For each reserve, compares aToken supply with underlying held by the aToken plus - /// stable debt, variable debt, unbacked bridge debt, and Horizon's first-class reserve - /// deficit. A liquidation may legitimately convert debt into deficit, so excluding that - /// field rejects official recovery accounting. + /// @dev For each reserve, compares aToken supply plus the indexed treasury accrual with + /// underlying held by the aToken plus stable debt, variable debt, unbacked bridge debt, + /// and Horizon's first-class reserve deficit. A liquidation may legitimately convert debt + /// into deficit, so excluding that field rejects official recovery accounting. function assertReserveBacking() external view { require(ph.getAssertionAdopter() == POOL, "AaveV3Horizon: configured pool is not adopter"); PhEvm.ForkId memory post = _postTx(); @@ -64,6 +67,7 @@ contract AaveV3HorizonReserveBackingAssertion is AaveV3HorizonHelpers { struct ReserveBacking { uint256 aTokenSupply; + uint256 accruedTreasuryLiability; uint256 backingClaims; } @@ -81,11 +85,19 @@ contract AaveV3HorizonReserveBackingAssertion is AaveV3HorizonHelpers { uint256 deficit = _readUintAt(POOL, abi.encodeCall(IAaveV3HorizonDeficitPool.getReserveDeficit, (asset)), fork); backing.aTokenSupply = _totalSupplyAt(reserveData.aTokenAddress, fork); + backing.accruedTreasuryLiability = _rayMul(reserveData.accruedToTreasury, reserveData.liquidityIndex); backing.backingClaims = availableLiquidity + stableDebt + variableDebt + reserveData.unbacked + deficit; } function _isBacked(ReserveBacking memory backing) internal view returns (bool) { - return backing.aTokenSupply <= backing.backingClaims + MAX_BACKING_DEFICIT; + return backing.aTokenSupply + backing.accruedTreasuryLiability + <= backing.backingClaims + MAX_BACKING_DEFICIT; + } + + /// @dev Aave stores `accruedToTreasury` in scaled aToken units. Mirror WadRayMath.rayMul's + /// half-up conversion so the liability is compared with token-denominated balances. + function _rayMul(uint256 scaledAmount, uint256 liquidityIndex) internal pure returns (uint256) { + return (scaledAmount * liquidityIndex + HALF_RAY) / RAY; } function _optionalTotalSupplyAt(address token, PhEvm.ForkId memory fork) internal view returns (uint256) { diff --git a/examples/aave/test/AaveV3AdversarialResearch.t.sol b/examples/aave/test/AaveV3AdversarialResearch.t.sol index 769e74e..087fcdb 100644 --- a/examples/aave/test/AaveV3AdversarialResearch.t.sol +++ b/examples/aave/test/AaveV3AdversarialResearch.t.sol @@ -481,11 +481,11 @@ contract ResearchPool is IAaveV3LikePool { ); } - /// @dev The upstream liability is aToken supply plus scaled accrued treasury; the assertion omits the latter. - function testBackingOmittedTreasuryClaimPassesDespiteEconomicShortfall() public { + function testBackingIncludesAccruedTreasuryLiability() public { pool.setAccruedToTreasury(address(collateralAsset), uint128(10 ether)); _armBacking(); + vm.expectRevert(bytes("AaveV3Horizon: reserve backing deficit")); pool.borrow(address(debtAsset), 1, 2, 0, alice); } From 00330d9f80043c5f3cf60d7a0df636ad2f7eb80a Mon Sep 17 00:00:00 2001 From: makemake Date: Thu, 30 Jul 2026 17:45:08 +0200 Subject: [PATCH 7/9] fix(aave): harden v3 Horizon oracle guard --- AAVE_V3_ASSERTION_REVIEW.md | 577 ++++++++++++++++++++++++++++++++++++ 1 file changed, 577 insertions(+) create mode 100644 AAVE_V3_ASSERTION_REVIEW.md diff --git a/AAVE_V3_ASSERTION_REVIEW.md b/AAVE_V3_ASSERTION_REVIEW.md new file mode 100644 index 0000000..1c3e2c9 --- /dev/null +++ b/AAVE_V3_ASSERTION_REVIEW.md @@ -0,0 +1,577 @@ +# Adversarial review of the Aave V3 Credible Layer assertions + +Date: 2026-07-30 +Repository commit: `5f258a231b092120241e8d8ed629acd30c580f4c` +Review scope: Aave V3/Horizon only. Aave V4 files and conclusions are excluded. + +> **Post-review remediation (2026-07-30):** The oracle-envelope findings below describe the +> implementation reviewed at repository commit `5f258a2`. That implementation has since been +> replaced in this workspace by a consumption-time trace guard. The replacement compares exact +> Pool-consumed AaveOracle returns with per-asset PreTx baselines, rejects source/fallback +> write-restore sequences, checks the Pool's actual provider return, includes the debt-opening +> `flashLoan` path, and fails closed on incomplete asset/trace configuration. The focused +> [`AaveV3HorizonOracleAssertion.t.sol`](examples/aave/test/AaveV3HorizonOracleAssertion.t.sol) +> suite passes 12/12 PCL tests. Two-asset stable borrow and two-borrow multicall costs are 186,556 +> and 241,607 assertion gas; a temporary flash-loan callback manipulation trips at 211,875 gas. +> An isolated eight-asset probe used 586,471 gas, below the production executor's 3,000,000 +> default but above the local PCL cheatcode's stricter 300,000 test threshold. The replacement +> still cannot establish that a price already corrupted before PreTx is correct; an independent +> reference remains complementary. + +Evidence labels used below: + +- **Verified from source** — established directly from pinned Aave or credible-std source/history. +- **Demonstrated by test** — reproduced with an isolated Foundry/PCL test or trace. +- **Supported inference** — conclusion follows from verified code, but was not reproduced against a live deployment. +- **Unverified hypothesis** — plausible, but needs additional evidence. + +## 1. Overall verdict + +None of the reviewed Aave V3 assertions should be kept as-is. + +1. **Reserve backing — Rework.** The intended custody/liability invariant is consequential and not enforced by one local Aave `require`, but the current Pool-adopted transaction-end trigger does not run for the direct underlying-token mutations that its NatSpec claims to block. Its equation also omits the indexed, accrued treasury liability and uses one raw-unit tolerance for every reserve. **Demonstrated by test; verified from source.** +2. **Oracle envelope — Replace with a better invariant.** The intended protection is high-value, but the implementation compares only transaction endpoints. It misses a temporary oracle/provider/source installed for the exact Pool call and restored afterward, and a permanent provider switch to an already-existing oracle. A stable one-borrow/two-reserve case costs 325,294 assertion gas and exceeds the local PCL 300,000 limit; two borrows cost 600,747. **Demonstrated by test.** +3. **Aave V3-like operation safety — Remove from the advertised Horizon bundle in its current form.** The production wrapper constructs a child suite in assertion initcode, but that child has no runtime code in the PCL assertion execution environment. Every monitored call therefore fails before a selector can be registered or an invariant can be evaluated. Even if flattened, the health-factor and bounded-consumption checks mostly restate Aave v3.3 validation, and the aToken transfer health check uses the wrong pre-call boundary. **Demonstrated by test; verified from source.** + +The reserve and oracle *ideas* are more security-interesting than the shared operation checks. Correctness is the gate, however: the two interesting ideas presently have complete same-transaction bypasses, while the operation bundle is not operational. + +## 2. Complete Aave V3 surface inventory + +| File/surface | Role | Review status | +|---|---|---| +| [`examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol`](examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol) | Pool-adopted transaction-end reserve backing check | Reviewed in full | +| [`examples/aave/src/AaveV3HorizonOracleAssertion.sol`](examples/aave/src/AaveV3HorizonOracleAssertion.sol) | Pool-adopted transaction-end oracle/source envelope | Reviewed in full | +| [`examples/aave/src/AaveV3HorizonHelpers.sol`](examples/aave/src/AaveV3HorizonHelpers.sol) | Fork-aware Pool/provider/oracle readers, price comparison, bitmap helpers | Reviewed in full | +| [`examples/aave/src/AaveV3HorizonInterfaces.sol`](examples/aave/src/AaveV3HorizonInterfaces.sol) | Oracle, ERC-20 accounting, deficit interfaces | ABI-reviewed | +| [`src/protection/lending/examples/AaveV3PostOperationSolvency.sol`](src/protection/lending/examples/AaveV3PostOperationSolvency.sol) | Horizon suite/wrapper and compatibility aliases | Reviewed in full | +| [`src/protection/lending/examples/AaveV3LikeOperationSafety.sol`](src/protection/lending/examples/AaveV3LikeOperationSafety.sol) | Wrapper holding an external suite | Reviewed in full | +| [`src/protection/lending/examples/AaveV3LikeHelpers.sol`](src/protection/lending/examples/AaveV3LikeHelpers.sol) | Six-operation decoder, HF snapshots, withdrawal/liquidation consumption | Reviewed in full | +| [`src/protection/lending/examples/AaveV3LikeInterfaces.sol`](src/protection/lending/examples/AaveV3LikeInterfaces.sol) | Current shared Aave-like ABI and legacy reserve tuple | ABI-reviewed | +| [`src/protection/lending/examples/AaveV3Interfaces.sol`](src/protection/lending/examples/AaveV3Interfaces.sol) | Older duplicate Aave V3 interfaces | Unused by Solidity imports; ABI-reviewed | +| [`src/protection/lending/LendingBaseAssertion.sol`](src/protection/lending/LendingBaseAssertion.sol) | Per-call trigger, call resolution, consumption/HF enforcement | Reviewed in full | +| [`src/protection/lending/ILendingProtectionSuite.sol`](src/protection/lending/ILendingProtectionSuite.sol) | Common operation/snapshot/check types | Reviewed in full | +| [`test/protection/lending/AaveV3LikeOperationSafety.t.sol`](test/protection/lending/AaveV3LikeOperationSafety.t.sol) | Selector/decoder/deployment unit tests | Inventoried and run | +| [`test/protection/lending/LendingSolvencyPerCall.t.sol`](test/protection/lending/LendingSolvencyPerCall.t.sol) | Generic flat-suite PCL behavior | Inventoried and run | +| [`examples/aave/test/AaveV3AdversarialResearch.t.sol`](examples/aave/test/AaveV3AdversarialResearch.t.sol) | Isolated review harness for backing, oracle, trigger, gas, and child-suite behavior | Added for this review | +| [`examples/aave/test/AaveV3OperationBoundaryResearch.t.sol`](examples/aave/test/AaveV3OperationBoundaryResearch.t.sol) | Isolated aToken/finalizeTransfer call-boundary harness | Added for this review | + +Repository search found no other active Aave V3 assertion implementation. The `examples/aave/test/` committed tests are V4-only. `AaveV3Interfaces.sol` is a stale duplicate and is not imported anywhere. Aave V4 contracts were kept separate and were not used to support any V3 conclusion. **Verified from source.** + +## 3. Pinned upstream versions, audits, deployments, and provenance + +### Protocol source + +- Ordinary Aave comparison point: official [`aave-dao/aave-v3-origin` v3.3.0, commit `5431379f8beb4d7128c84a81ced3917d856efa84`](https://github.com/aave-dao/aave-v3-origin/tree/5431379f8beb4d7128c84a81ced3917d856efa84). The official [v3.3.0 release](https://github.com/aave-dao/aave-v3-origin/releases/tag/v3.3.0) identifies deficit accounting, v3.3 liquidation changes, and the legacy getter changes. +- Horizon comparison point: locally available official `aave/aave-v3-horizon`, remote `git@github.com:aave/aave-v3-horizon.git`, main commit [`6e2a51ea2e67af8aacf63f835d2e2b26dc7a2741`](https://github.com/aave/aave-v3-horizon/tree/6e2a51ea2e67af8aacf63f835d2e2b26dc7a2741), package version `3.3.0`. +- The ordinary v3.3 tag is the merge base of Horizon main. Between those pins, the reviewed Pool, `AaveOracle`, Pool libraries, and accounting types are unchanged; Horizon adds `RwaAToken`, `RwaATokenManager`, two errors, and two `AToken` visibility changes required for the RWA subclass. **Verified from source.** +- The exact Horizon commit used by the assertion author was not recorded. The assertion branch was created in April–May 2026; the official Horizon core at `6e2a51e` and the later docs branch `c1ec8c1` have no `src/contracts` differences. Using `6e2a51e` is therefore source-accurate for the reviewed interfaces and logic, but the missing original pin remains a provenance gap. **Verified from source; supported inference.** + +Ordinary Aave v3.3 and Horizon therefore share the reserve, oracle, health-factor, withdraw, liquidation, eMode, flash-loan, and bitmap behavior assessed below. Horizon-specific differences are the permissioned RWA aToken restrictions: ordinary transfers, `transferOnLiquidation`, `transferUnderlyingTo`, and `mintToTreasury` revert, while `authorizedTransfer` is privileged and still routes through the normal validated aToken transfer path. See the pinned [`RwaAToken`](https://github.com/aave/aave-v3-horizon/blob/6e2a51ea2e67af8aacf63f835d2e2b26dc7a2741/src/contracts/protocol/tokenization/RwaAToken.sol). **Verified from source.** + +### Horizon audits + +- [Certora, `2025-05-30_Certora_Horizon-v3.3.0.pdf`](https://github.com/aave/aave-v3-horizon/blob/6e2a51ea2e67af8aacf63f835d2e2b26dc7a2741/audits/2025-05-30_Certora_Horizon-v3.3.0.pdf), SHA-256 `4829462e17fd1593f75cc13a59a170c29c154fa73c62945532564ac7b5a0d7d5`, reviewed commit `04419e25d3e87327487517bf0846ffa65aac35a2`. +- [StErMi, `2025-06-25_StErMi_Horizon-v3.3.0.pdf`](https://github.com/aave/aave-v3-horizon/blob/6e2a51ea2e67af8aacf63f835d2e2b26dc7a2741/audits/2025-06-25_StErMi_Horizon-v3.3.0.pdf), SHA-256 `1f63681f73e8399dd1906038051c89e1eafc051e81038d1cbead326a6aca907a`, initial commit `04419e25d3e87327487517bf0846ffa65aac35a2`, final fix commit `417a4768051126e14e492dd088c5b64add4a5b24`. + +Both audits scope the Horizon RWA extension, not these Credible assertions. Certora confirms RWA underlying can move on withdraw/liquidation subject to the issuer token’s permissioning. StErMi calls out privileged `authorizedTransfer`, states that configured RWA reserves do not accrue aRWA treasury shares, and discusses coordinated liquidation creating v3.3 deficit. Those findings strengthen the relevance of cross-contract monitoring but do not validate the reviewed assertion equations or triggers. **Verified from primary audit artifacts.** + +### Deployments + +Official Horizon Ethereum constants at the pinned Horizon commit identify: + +- PoolAddressesProvider: `0x5D39E06b825C1F2B80bf2756a73e28eFAA128ba0` +- Pool proxy: `0xAe05Cd22df81871bc7cC2a04BeCfb516bFe332C8` +- AaveOracle: `0x985BcfAB7e0f4EF2606CC5b64FC1A16311880442` +- PoolConfigurator: `0x83Cb1B4af26EEf6463aC20AFbAC9c0e2E017202F` + +Primary source: pinned [`AaveV3HorizonEthereum.sol`](https://github.com/aave/aave-v3-horizon/blob/6e2a51ea2e67af8aacf63f835d2e2b26dc7a2741/tests/horizon/utils/AaveV3HorizonEthereum.sol). + +The historical local `aave` branch’s `assertions/credible-aave.toml` instead configured the operation wrapper on ordinary Aave V3 Base: + +- Base Pool: `0xA238Dd80C259a72e81d7e4664a9801593F98d1c5` +- Base provider: `0xe20fCBdBfFC4Dd138cE8b2E6FBb6CB49777ad64D` + +These addresses match the official Aave address book at pinned commit [`dd5a718d6739342882dd3327739dc037c4fd0028`](https://github.com/aave-dao/aave-address-book/blob/dd5a718d6739342882dd3327739dc037c4fd0028/src/AaveV3Base.sol). No repository evidence shows a Horizon assertion release configured against the Horizon Ethereum Pool. **Verified from source/history.** + +## 4. ABI, struct, and token-semantics review + +### Function selectors and return values + +| Function | Selector | Upstream compatibility | +|---|---:|---| +| `borrow(address,uint256,uint256,uint16,address)` | `0xa415bcad` | Exact Horizon/v3.3 Pool ABI | +| `withdraw(address,uint256,address)` | `0x69328dec` | Exact; returns actual `uint256 amountToWithdraw` | +| `liquidationCall(address,address,address,uint256,bool)` | `0x00a718a9` | Exact | +| `setUserUseReserveAsCollateral(address,bool)` | `0x5a3b74b9` | Exact | +| `finalizeTransfer(address,address,address,uint256,uint256,uint256)` | `0xd5ed3933` | Exact | +| `setUserEMode(uint8)` | `0x28530a47` | Exact on Ethereum Pool; L2 compressed overload omitted | +| `getReserveData(address)` | `0x35ea6a75` | Exact legacy tuple in v3.3 | +| `getReserveDeficit(address)` | `0xc952485d` | Added in v3.3, not v3.6 | +| `getAssetPrice(address)` | `0xb3596f07` | Exact, returns `uint256` | +| `getSourceOfAsset(address)` | `0x92bf2be0` | Exact, returns `address` | +| `getFallbackOracle()` | `0x6210308c` | Exact, returns `address` | +| `getPriceOracle()` | `0xfca513a8` | Exact, returns `address` | + +**Verified from source.** + +### Reserve tuple + +The local `AaveV3LikeTypes.ReserveData` field order and widths exactly match v3.3 `DataTypes.ReserveDataLegacy`: configuration word, five `uint128` indexes/rates, `uint40` timestamp, `uint16` id, four addresses, and three trailing `uint128` accounting fields. Encoding the single-member upstream `ReserveConfigurationMap` as local `uint256 configurationData` is ABI-equivalent. See the pinned [`DataTypes.sol`](https://github.com/aave-dao/aave-v3-origin/blob/5431379f8beb4d7128c84a81ced3917d856efa84/src/contracts/protocol/libraries/types/DataTypes.sol). **Verified from source.** + +Compatibility limits: + +- v3.0/v3.1 used a real stable-debt token in this tuple. +- v3.2 removed stable borrowing but retained the legacy tuple. v3.3 fills `stableDebtTokenAddress` from provider key `MOCK_STABLE_DEBT`; that mock is expected to return zero. The backing assertion trusts any nonzero configured address’s `totalSupply()`, so a misconfigured or upgraded mock can create artificial backing. +- v3.3 stores deficit in the internal deprecated stable-rate slot and exposes it through the separate `getReserveDeficit` getter. The local interface comment claiming a “v3.6 accounting surface” is factually wrong. +- The reviewed tuple is correct for the pinned v3.3 release. Compatibility with a later release that changes/removes the legacy getter is not established by the code. + +**Verified from source.** + +### Accounting-token semantics + +`AToken.totalSupply()` and `balanceOf()` are normalized, interest-bearing claims: scaled balances multiplied by the current normalized income. `VariableDebtToken.totalSupply()` is similarly normalized by the variable debt index. They are the right *realized* user-liability and debt quantities. `reserve.accruedToTreasury`, however, is a **scaled, not-yet-minted aToken liability**. On `mintToTreasury`, Aave computes: + +`amountToMint = accruedToTreasury.rayMul(normalizedIncome)` + +and mints that many aTokens. Aave’s own supply-cap validation includes scaled aToken supply plus `accruedToTreasury`, then applies the next liquidity index. See pinned [`PoolLogic.sol`](https://github.com/aave-dao/aave-v3-origin/blob/5431379f8beb4d7128c84a81ced3917d856efa84/src/contracts/protocol/libraries/logic/PoolLogic.sol), [`ReserveLogic.sol`](https://github.com/aave-dao/aave-v3-origin/blob/5431379f8beb4d7128c84a81ced3917d856efa84/src/contracts/protocol/libraries/logic/ReserveLogic.sol), and [`ValidationLogic.sol`](https://github.com/aave-dao/aave-v3-origin/blob/5431379f8beb4d7128c84a81ced3917d856efa84/src/contracts/protocol/libraries/logic/ValidationLogic.sol). **Verified from source.** + +## 5. Formal invariants and complete execution paths + +### A. Reserve backing + +#### Implemented invariant + +For each constructor-configured reserve `r`, at `PostTx`: + +`S_a(r) <= C(r) + D_s(r) + D_v(r) + U(r) + F(r) + ε` + +where: + +- `S_a`: normalized aToken `totalSupply` +- `C`: actual underlying ERC-20 balance held by the aToken +- `D_s`: stable-debt `totalSupply`, optional +- `D_v`: normalized variable-debt `totalSupply` +- `U`: `reserve.unbacked` +- `F`: `Pool.getReserveDeficit` +- `ε`: one constructor-wide raw `MAX_BACKING_DEFICIT` + +The intended threat is external custody or accounting-token mutation that creates more redeemable aToken claims than cash, collectible debt, unbacked bridge accounting, or recognized bad debt. The exact implementation is at [`AaveV3HorizonReserveBackingAssertion.sol:35`](examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol#L35) and [`:70`](examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol#L70). + +#### Correct economic equation + +For a standard, non-rebasing asset, ignoring bounded rounding: + +`S_a + (accruedToTreasury × normalizedIncome) = C + D_s + D_v + U + F` + +`virtualUnderlyingBalance` is not an additional backing asset. It is Aave’s rate/utilization accounting balance and can diverge from actual custody after donations; redemption is still backed by the actual underlying held by the aToken. **Verified from source.** + +Signs and special cases: + +- Variable and stable debt are positive assets of the reserve. +- Unbacked is positive: `mintUnbacked` creates aTokens and matching unbacked; `backUnbacked` replaces unbacked with custody. +- Deficit is positive: v3.3 bad-debt liquidation burns unrecoverable variable debt and adds the same outstanding amount to deficit. `eliminateReserveDeficit` burns coverage aTokens (virtual accounting) or disposes underlying for the nonvirtual GHO special case while reducing deficit. +- Pending treasury accrual belongs on the liability side. Omitting it creates slack equal to the indexed pending treasury claim. +- RWA aTokens currently block `mintToTreasury`, and the audited deployment expects no aRWA treasury shares. The omission is still material for Horizon’s ordinary stablecoin/debt reserves, which use standard aTokens. +- A reserve without stable debt is valid. In v3.2+ the returned stable-debt field is zero or a zero-returning compatibility mock. + +**Verified from source.** + +The one-sided direction is reasonable *if* the policy is minimum backing: donations, positive rebases, and excess debt should not be rejected. Equality would create false positives. The safe one-sided form must still include every liability and should usually reject a **worsening deficit**, not freeze all future Pool traffic merely because old state is outside a hard endpoint bound. **Supported inference.** + +#### Trigger and reachability + +The assertion registers only `registerTxEndTrigger` and requires the adopter to equal `POOL`. A transaction touching only an underlying, aToken, debt token, oracle, or provider does not execute this Pool-adopted assertion. A direct custody seizure is detected only if the same transaction also successfully touches the Pool, or at a later Pool-touching transaction when it is too late to block the causing transaction. Reverted Pool calls do not select the trigger. **Demonstrated by test.** + +The old `aave` branch test armed this assertion on the underlying collateral token and expected a direct seizure to trip. The later adopter-equality repair changed the valid adopter to the Pool, but the direct-movement NatSpec and trigger claim were not corrected and the behavioral V3 test was removed during example consolidation. **Verified from history.** + +#### Assumptions + +- The configured asset list exactly covers every live reserve and is updated on listings. +- Every asset is a conventional ERC-20 whose `balanceOf` and `totalSupply` are meaningful and non-reverting at fork snapshots. +- Underlying is not negative-rebasing and has no privileged balance rewrite that should be accepted. +- Transfers are not fee-on-transfer; token hooks/rebases cannot create an honest endpoint imbalance. +- Debt/aToken implementations and provider `MOCK_STABLE_DEBT` are trusted and ABI-compatible. +- A temporary intra-transaction deficit is not itself exploitable, because only `PostTx` is checked. +- `ε` is correctly chosen in each asset’s smallest units—an assumption contradicted by using one value for all assets. + +### B. Oracle envelope + +#### Implemented invariant + +Let `O_post` be the oracle address read from the provider at `PostTx`. For each successful matching Pool call and each discovered active/touched asset `a`: + +1. `source(O_post, a, PreTx) = source(O_post, a, PostTx)` +2. both endpoint prices are positive +3. the symmetric price ratio is within `ORACLE_DEVIATION_BPS` + +The six enumerated call groups are `borrow`, `withdraw`, collateral toggle, `finalizeTransfer`, `setUserEMode`, and `liquidationCall`. The implementation is at [`AaveV3HorizonOracleAssertion.sol:50`](examples/aave/src/AaveV3HorizonOracleAssertion.sol#L50). + +The account mapping is ABI-correct: + +- borrow → `onBehalfOf` +- withdraw/collateral/eMode → immediate Pool caller +- finalizeTransfer → `from` +- liquidation → liquidated `user` + +Touched assets are decoded correctly for every selector except eMode, which has no single touched asset and depends entirely on the user-position scan. `getAllCallInputs` returns argument tails without selectors; this assertion decodes the tails directly and is correct. The generic per-call base correctly prepends the selector before passing to its decoder. **Verified from source.** + +#### Trigger, calls, and forks + +- One Pool-adopted `TxEnd` trigger; provider/oracle/source-only transactions do not run it. +- PCL 1.6.0 returns only successful calls from `getAllCallInputs`. A trace with one successful and one caught/reverted `borrow` returned only the successful call. A transaction containing only a caught/reverted Pool call executed zero assertions. +- The reserve list and each reserve’s id come from `PostTx`, while both pre- and post-user bitmaps are interpreted with those post ids. Ordinary v3.3 never renumbers existing ids and only drops empty reserves, so this is safe under the current administrative logic. It is not robust against a buggy upgrade that changes ids—the class of failure an external assertion should ideally tolerate. +- All oracle reads use the single `O_post` address at both forks. + +**Demonstrated by test; verified from source.** + +#### Required assumptions + +- Provider oracle identity is constant, or the post oracle is the oracle whose pre-state should be used. +- The price that mattered to the Pool equals one of the transaction endpoints. +- No source, fallback, proxy implementation, aggregator implementation, or answer is maliciously changed and restored inside the transaction. +- Every affected account is reachable through the six selected external selectors. +- Reserve count is at most `MAX_RESERVES_TO_SCAN`. +- Work remains below the assertion budget for arbitrary successful call multiplicity. +- One global percentage tolerance is operationally appropriate for stablecoins, NAV-based RWAs, and volatile collateral. + +Several assumptions are false under the stated threat model. + +### C. Aave V3-like operation safety + +For each **successful** monitored Pool frame `c`: + +1. If the decoded operation increases debt, reduces effective collateral, or changes eMode, and the selected account is solvent at `PreCall(c)`, require it is solvent at `PostCall(c)`: + + `debt_post = 0 OR healthFactor_post >= 1e18` + +2. For withdrawal: + + `abi.decode(callOutput(c)) <= aToken.balanceOf(caller, PreCall(c))` + +3. For liquidation: + + `debtAsset transferred(liquidator → debt aToken, c) <= stableDebt(user, pre) + variableDebt(user, pre)` + + `collateral transferred(to liquidator, c) <= aToken.balanceOf(user, pre)` + +The base resolves the exact call through trigger context and call id, uses `PreCall`/`PostCall`, and subtracts cumulative ERC-20 transfer observations to isolate one call window. Multiple successful matching calls each get their own execution. **Verified from source; demonstrated by generic test.** + +Deployment assumptions fail: [`AaveV3PostOperationSolvency.sol:29`](src/protection/lending/examples/AaveV3PostOperationSolvency.sol#L29) creates `AaveV3HorizonProtectionSuite` in constructor initcode and stores its address. In the PCL assertion runtime that child has no code. The historical behavioral test explicitly documented this and substituted a flat fixture; the production wrapper itself had no positive E2E. **Verified from history; demonstrated by test.** + +## 6. Scorecard and dispositions + +Scores are 0–5. “Correctness” is shipping correctness, including operational reachability; it is not a score for the idea in isolation. + +| Assertion/check | Protected threat | Trigger and reachability | Correctness | Security interest | Redundancy with Aave | Principal false positive | Principal false negative/bypass | Evidence strength | Disposition | +|---|---|---|---:|---:|---|---|---|---|---| +| Reserve backing | External custody loss, bad accounting-token mint, upgrade/accounting bug | Pool-adopted TxEnd; only successful Pool-touching transactions | 2/5 | 4/5 | No single equivalent cross-contract postcondition | Old/unmodeled deficit, fee/rebase token behavior, raw tolerance mismatch | Direct token-only mutation; temporary deficit restored; pending treasury masks loss | Strong source + PCL mocks; no live fork | **Rework** | +| Oracle envelope | Oracle/source manipulation coupled to risk action | Pool-adopted TxEnd; six successful selector groups | 1/5 | 4/5 | Aave access control and Horizon DON bounds exist, but no same-tx cross-contract envelope | Honest price move over tolerance; new oracle not present at PreTx; OOG | Temporary manipulation/restore; permanent provider switch; omitted flash-loan debt path | Strong source + PCL mocks/traces; no live fork | **Replace with a better invariant** | +| Post-operation HF | Buggy Pool leaves healthy user liquidatable | Per-call on six selectors, but production child suite is unavailable | 1/5 | 2/5 | Borrow/withdraw/disable/transfer/eMode already validate HF/LTV | Current bundle reverts before evaluation; if flattened, oracle update can alter HF | aToken balance mutation precedes `finalizeTransfer` PreCall; flash-loan debt path omitted | Strong for operational failure/boundary; no real Aave fork | **Remove current wrapper; rework only if retained as defense in depth** | +| Withdrawal claim bound | Pool returns more underlying than caller claim | Same unusable wrapper; per-withdraw call if flattened | 1/5 | 1/5 | Exact `amount <= userBalance`, max sentinel clipping, then burn | Current bundle operational failure | A malicious transfer without standard event semantics; otherwise check is redundant | Source-strong, no Aave behavioral E2E | **Remove** | +| Liquidation debt bound | Liquidator repays more than user debt | Same unusable wrapper; per-liquidation call if flattened | 1/5 | 1/5 | v3.3 clips to user debt/close factor/collateral | Current bundle operational failure | Does not prove debt burned equals assets received or deficit created correctly | Source-strong, no Aave behavioral E2E | **Replace with settlement identity** | +| Liquidation collateral bound | Liquidator receives more than user collateral | Same unusable wrapper; per-liquidation call if flattened | 1/5 | 1/5 | v3.3 caps collateral by user balance | Current bundle operational failure | Counts only liquidator leg, omitting protocol-fee transfer from user | Source-strong, no Aave behavioral E2E | **Replace with settlement identity** | + +Dimension detail: + +| Surface | Model accuracy | ABI/accounting | Trigger reachability | Bypass resistance | FP safety | +|---|---:|---:|---:|---:|---:| +| Reserve backing | 2 | 4 | 1 | 1 | 2 | +| Oracle envelope | 2 | 4 | 3 | 0 | 1 | +| Operation HF | 4 | 4 | 0 | 1 | 0 | +| Withdraw bound | 4 | 4 | 0 | 2 | 0 | +| Liquidation debt bound | 3 | 4 | 0 | 2 | 0 | +| Liquidation collateral bound | 2 | 4 | 0 | 1 | 0 | + +## 7. Detailed findings ordered by severity + +### Critical — production operation wrapper cannot execute its suite + +**Demonstrated by test.** The production wrapper constructs and stores the child at [`AaveV3PostOperationSolvency.sol:29`](src/protection/lending/examples/AaveV3PostOperationSolvency.sol#L29). Its `_suite().getMonitoredSelectors()` and later suite calls target that address, which has no code in assertion execution. `pcl test -vvvv` shows the external child call stop with empty return data and the wrapper revert with empty data. The repository’s removed `AaveV3OperationSafetyBehavior.t.sol` already acknowledged the limitation and tested a flat substitute instead. + +Impact: the advertised bundle provides none of its six protections and can reject monitored Pool traffic operationally. Compilation and direct EVM deployment tests do not detect this execution-model failure. + +### High — backing assertion does not fire for its claimed threat + +**Demonstrated by test.** A direct `underlying.seize(aToken, recipient, amount)` after arming the assertion on the Pool executes zero assertions. Adding a successful Pool call to the same transaction causes the same deficit to trip. The contradictory NatSpec is at [`AaveV3HorizonReserveBackingAssertion.sol:12`](examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol#L12) and [`:31`](examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol#L31). + +This is not delayed detection equivalent to transaction blocking: the causing transfer commits, and the next unrelated Pool user may be the transaction rejected. + +### High — endpoint oracle comparison misses the price actually consumed + +**Demonstrated by test.** The assertion resolves only the post-state oracle at [`AaveV3HorizonOracleAssertion.sol:53`](examples/aave/src/AaveV3HorizonOracleAssertion.sol#L53), then supplies that one address to endpoint comparisons. The following sequences reach semantic completion without an oracle/source violation, then fail only because the assertion exceeds PCL’s gas budget: + +- price `P → malicious P' → Pool.borrow → P` +- source `S → malicious S' → Pool.borrow → S` +- provider oracle `O → malicious O' → Pool.borrow → O` +- permanent provider change `O → existing O'`, with `O'` stable at both endpoints + +The same construction applies to withdrawal, collateral disable, eMode, aToken transfer finalization, and liquidation. A healthy account can be made liquidatable only at the call’s manipulated price, liquidated, and the price restored before `PostTx`; endpoint equality does not reveal what `LiquidationLogic` consumed. Aave resolves the provider oracle inside each operation and reads asset prices during validation/liquidation; see pinned [`Pool.sol`](https://github.com/aave-dao/aave-v3-origin/blob/5431379f8beb4d7128c84a81ced3917d856efa84/src/contracts/protocol/pool/Pool.sol) and [`LiquidationLogic.sol`](https://github.com/aave-dao/aave-v3-origin/blob/5431379f8beb4d7128c84a81ced3917d856efa84/src/contracts/protocol/libraries/logic/LiquidationLogic.sol). **Verified from source.** + +Persistent source changes are caught. Persistent price changes outside the configured band are caught. These positive cases do not repair the intermediate-value bypass. + +### High — oracle cost is unbounded in calls and operationally over budget + +**Demonstrated by test.** + +- Two reserves, one stable borrow: 325,294 assertion gas; local limit 300,000. +- Two reserves, two stable borrows: 600,747. +- Persistent drift fails early at 260,079, so malicious tests can pass while the honest full-scan path OOGs. + +Cost scales with six trace queries at [`AaveV3HorizonOracleAssertion.sol:57`](examples/aave/src/AaveV3HorizonOracleAssertion.sol#L57), matching-call count at [`:71`](examples/aave/src/AaveV3HorizonOracleAssertion.sol#L71), reserve count per affected account, and active/touched assets. Users/assets are not deduplicated. `MAX_RESERVES_TO_SCAN` bounds reserve count but not calls. The July 2026 backing-history commit records a production sidecar default of 3 million gas; that larger limit postpones, but does not remove, the multi-call denial boundary. A realistic eleven-reserve market and attacker-controlled successful batching were not measured against a live sidecar. **Supported inference.** + +### High — reserve equation omits a real liability + +**Verified from source; demonstrated by test.** The implemented sum at [`AaveV3HorizonReserveBackingAssertion.sol:78`](examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol#L78) never reads `accruedToTreasury`. A mock state with `aTokenSupply = cash` and positive accrued treasury passes, although minting the pending treasury claim would create an immediate shortfall. Interest accrual creates exactly this pending claim: variable debt grows by total interest, user aToken supply grows by the depositor share, and the reserve-factor share accumulates scaled in `accruedToTreasury`. + +For current Horizon RWA aTokens the field should remain zero because `mintToTreasury` is blocked; stablecoin/debt reserves remain affected. + +### Medium — `finalizeTransfer` PreCall is after the risky balance mutation + +**Verified from source; demonstrated by test.** `AToken._transfer` executes `super._transfer` before `POOL.finalizeTransfer`. Consequently, `PreCall(finalizeTransfer)` already contains the reduced collateral balance. The base sees an already-insolvent account and intentionally returns at [`LendingBaseAssertion.sol:209`](src/protection/lending/LendingBaseAssertion.sol#L209). A control mutation performed *inside* `finalizeTransfer` is caught; the Aave-ordered mutation before it is skipped. + +Aave’s own `finalizeTransfer` validation still protects real v3.3. This is a false negative specifically as independent defense against a buggy/modified Aave validation path, including Horizon’s privileged `authorizedTransfer`. + +### Medium — operation safety mostly restates Aave v3.3 + +**Verified from source.** The local adapter’s six-selector list and HF selection are at [`AaveV3LikeHelpers.sol:63`](src/protection/lending/examples/AaveV3LikeHelpers.sol#L63) and [`:223`](src/protection/lending/examples/AaveV3LikeHelpers.sol#L223). + +- Borrow requires pre-borrow HF `> 1e18` and then verifies total collateral can cover existing plus new debt at current LTV. This is stricter than merely checking post-HF `>= 1e18`. +- Withdraw clips `type(uint256).max` to the normalized user balance, requires amount `<= userBalance`, burns that amount, then validates HF/LTV when collateral supports debt. +- Disabling collateral changes the flag then validates HF/LTV. +- aToken transfer mutates balances then `finalizeTransfer` validates HF/LTV. +- eMode stores the new category then validates HF. +- Liquidation validation requires HF below one and v3.3 settlement clips debt and collateral using close factor, user debt, collateral, bonus, protocol fee, and dust rules. + +Primary source: pinned [`ValidationLogic.sol`](https://github.com/aave-dao/aave-v3-origin/blob/5431379f8beb4d7128c84a81ced3917d856efa84/src/contracts/protocol/libraries/logic/ValidationLogic.sol), [`SupplyLogic.sol`](https://github.com/aave-dao/aave-v3-origin/blob/5431379f8beb4d7128c84a81ced3917d856efa84/src/contracts/protocol/libraries/logic/SupplyLogic.sol), and [`EModeLogic.sol`](https://github.com/aave-dao/aave-v3-origin/blob/5431379f8beb4d7128c84a81ced3917d856efa84/src/contracts/protocol/libraries/logic/EModeLogic.sol). + +The checks are defense in depth against an implementation upgrade or integration side effect, not new protections against ordinary Aave behavior. Because they trust `Pool.getUserAccountData`, a coherently buggy Pool risk view can also make both protocol validation and assertion agree on the same wrong result. + +### Medium — selector coverage omits debt opened through `flashLoan` + +**Verified from source.** Multi-asset `flashLoan` allows a non-`NONE` interest-rate mode. If funds are not returned, `FlashLoanLogic` calls `BorrowLogic.executeBorrow` internally and opens debt for `onBehalfOf`; there is no external Pool `borrow` selector frame. The local selector list at [`AaveV3LikeHelpers.sol:63`](src/protection/lending/examples/AaveV3LikeHelpers.sol#L63) and the oracle groups at [`AaveV3HorizonOracleAssertion.sol:57`](examples/aave/src/AaveV3HorizonOracleAssertion.sol#L57) both omit `flashLoan`. See pinned [`FlashLoanLogic.sol`](https://github.com/aave-dao/aave-v3-origin/blob/5431379f8beb4d7128c84a81ced3917d856efa84/src/contracts/protocol/libraries/logic/FlashLoanLogic.sol). + +Other omissions: + +- Supply and repay are safely omitted from account HF because they are risk-improving under standard token behavior; they still matter to reserve-wide accounting. +- `flashLoanSimple` must be repaid and does not open debt, so omitting it from user HF is reasonable; it can still affect treasury/backing. +- mint/back-unbacked are reserve-wide, not account-HF operations. +- Ethereum Horizon uses the normal Pool ABI. The generic “Aave-like” claim does not cover L2Pool compressed borrow/withdraw/collateral selectors. +- Administrative configuration, proxy upgrades, provider/oracle changes, and direct token operations are outside the six selectors. + +### Medium — one raw backing tolerance is not cross-asset safe + +**Verified from source.** `MAX_BACKING_DEFICIT` is added directly to every reserve’s raw token amount at [`AaveV3HorizonReserveBackingAssertion.sol:87`](examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol#L87). A value of `1_000_000` is one whole USDC but only `10^-12` tokens for an 18-decimal reserve. Equal raw values do not represent equal rounding budgets or economic risk. A zero value may create rounding false positives; a value suitable for an 18-decimal asset may authorize material loss for a 6-decimal asset. + +Use an immutable per-asset raw tolerance after deriving worst-case index/rounding error, or normalize into base value with a second trusted price source. The latter must not reuse the oracle being monitored without acknowledging circularity. + +### Medium — liquidation collateral bound omits the protocol-fee leg + +**Verified from source.** The local calculation at [`AaveV3LikeHelpers.sol:423`](src/protection/lending/examples/AaveV3LikeHelpers.sol#L423) counts only collateral sent to the liquidator: + +- receive-aToken: user aToken → liquidator +- receive-underlying: underlying aToken → liquidator + +v3.3 separately transfers `liquidationProtocolFeeAmount` in aTokens from the user to the treasury. Therefore the assertion does not bound the user’s total collateral debit. Horizon RWA liquidation requires the protocol fee to be configured to zero because `RwaAToken.transferOnLiquidation` reverts, but ordinary stable/crypto collateral can have a fee. The current inequality is also much weaker than an exact settlement reconciliation. + +### Medium — oracle false positives and configuration gaps + +**Demonstrated by test; verified from source.** The global endpoint comparison is implemented at [`AaveV3HorizonHelpers.sol:67`](examples/aave/src/AaveV3HorizonHelpers.sol#L67), and constructor configuration is accepted without a Pool/provider relationship check at [`AaveV3HorizonOracleAssertion.sol:25`](examples/aave/src/AaveV3HorizonOracleAssertion.sol#L25). + +- A legitimate 2% feed update bundled with a Pool borrow trips a 1% tolerance. +- Normal feed rounds can change while an unrelated account uses the Pool; the assertion does not establish causality between the update and malicious risk. +- Fallback-oracle identity is never compared. A fallback change is invisible whenever the primary remains positive, or whenever endpoint price remains within tolerance. +- Aggregator proxy implementation changes are invisible if the Aave source address and endpoint answer remain stable. +- A newly deployed post oracle may not exist at `PreTx`; reading the post address on the pre fork then fails. +- The provider passed to the constructor is not checked against `Pool.ADDRESSES_PROVIDER`, so a deployment can silently monitor the wrong provider. +- A static `MAX_RESERVES_TO_SCAN` becomes a market-wide denial if listings exceed it. + +### Low — reserve-list lifecycle is manual + +**Verified from source.** The backing assertion’s reserve array is constructor-supplied at [`AaveV3HorizonReserveBackingAssertion.sol:22`](examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol#L22). New listings are not covered; stale dropped entries cause `"reserve not listed"` on every later Pool-touching transaction. The oracle assertion dynamically reads the post list, but with a hard maximum. + +## 8. Operation-specific analysis + +### Withdrawals + +`type(uint256).max` is normalized by Aave to the current indexed user balance; `withdraw` returns the actual amount and the assertion decodes that output. `aToken.balanceOf` at `PreCall` calculates normalized income for the same block timestamp that `reserve.updateState` uses, so interest accrual should not create a meaningful mismatch beyond Aave’s own ray rounding. Partial/full paths are semantically handled. Multiple withdrawals are isolated by per-call forks. **Verified from source; generic per-call behavior demonstrated by test.** + +Conclusion: logically accurate but almost perfectly redundant with `validateWithdraw`, and unusable in the production wrapper. + +### Liquidations + +- `type(uint256).max`/oversized `debtToCover` is clipped to maximum liquidatable debt. +- Partial/full close-factor behavior, available collateral, liquidation bonus, v3.3 dust thresholds, and deficit realization are implemented upstream before transfers. +- Debt bound observes actual debt-asset transfer, not requested calldata, and compares to normalized pre-call stable+variable debt. +- receive-aToken mode and receive-underlying mode select the correct liquidator transfer token/sender. +- The separate treasury protocol-fee collateral transfer is omitted. +- Exact correctness would reconcile debt burned, debt asset received, collateral debited, liquidator proceeds, treasury fee, user flags, and any deficit created. The current pair of upper bounds cannot catch under-burning debt, wrong-recipient transfer, wrong deficit, or a user debit hidden in another transfer leg. + +**Verified from source.** + +## 9. Test and infrastructure results + +Toolchain: + +- `pcl 1.6.0`, commit `4a134645d475` +- Forge `1.5.1`, commit `b0a9dd9ceda36f63e2326ce530c10e6916f4b8a2` + +### Commands and exact outcomes + +```text +FOUNDRY_PROFILE=aave forge build +``` + +Pass. This proves compilation only. + +```text +FOUNDRY_PROFILE=aave forge build --sizes +``` + +Pass. Deployed/init sizes were: operation wrapper 8,473/27,660 bytes, child suite 17,593 deployed bytes, oracle 9,664 deployed bytes, and reserve backing 5,290 deployed bytes. This rules out EVM bytecode-size rejection as the child-suite cause; it does not prove PCL runtime availability or execution gas safety. + +```text +pcl test --match-contract AaveV3LikeOperationSafetyTest -vv +``` + +8/8 pass. They prove six selector values, caller/on-behalf decoding for unit fixtures, and suite deployment. They do not arm the production assertion, read Aave state, test bounds, or exercise trigger reachability. + +```text +pcl test --match-contract LendingSolvencyPerCallTest -vv +``` + +6/6 pass. The flat generic suite proves honest/broken/pre-insolvent behavior, per-call transient break-then-repair detection, selector prepending, and legacy entrypoint semantics. It does not prove the production Aave adapter or child wrapper. + +```text +forge test --offline --match-path 'test/protection/lending/*.t.sol' -vv +``` + +8 decoder/deployment tests pass; all 6 Credible E2Es fail on ordinary Forge’s unknown Credible cheatcode. PCL, not Forge, is the behavioral runner. + +```text +FOUNDRY_PROFILE=aave pcl test --match-contract AaveV3OperationBoundaryResearchTest -vvvv +``` + +2/2 pass: + +- mutation inside `finalizeTransfer` trips +- mutation before entry, matching aToken ordering, is skipped and leaves health `-1` + +```text +FOUNDRY_PROFILE=aave pcl test --match-contract AaveV3AdversarialResearchTest -vv +``` + +Final run: 17 tests, 9 pass/8 fail as evidence. The nonzero exit is intentional: several research cases encode the expected assertion execution or gas outcome, so a PCL framework failure is the demonstrated defect: + +- backing honest, deficit sign, same-tx Pool-touch seizure, treasury omission, and temporary restore behaviors reproduced +- direct token-only seizure fails because zero assertions execute +- persistent and legitimate oracle drift trip +- stable/full oracle paths exceed the 300,000 limit +- caught/reverted-only Pool call executes zero assertions +- production child suite reverts with empty data, as expected by the test +- with one successful and one caught/reverted borrow, `getAllCallInputs` returns exactly the successful call + +The cardinality result was also isolated with a focused trace: + +```text +FOUNDRY_PROFILE=aave pcl test \ + --match-test testGetAllCallInputsExcludesCaughtRevertedBorrow -vvvv +``` + +Pass. With one successful and one caught/reverted borrow, `getAllCallInputs` returns exactly the successful call. + +### Mutation matrix + +| Mutation | Intended result | Observed | +|---|---|---| +| Honest backing + Pool call | Pass | Pass, 169,918 assertion gas | +| Direct underlying seizure only | Trip | Zero assertion executions | +| Same seizure + Pool call | Trip | Trips | +| Add recognized v3.3 deficit equal to custody loss | Pass | Pass | +| Add pending treasury claim without backing | Trip under correct model | Pass | +| Seize, call Pool, restore before PostTx | Trip if intermediate forbidden | Pass | +| Persistent price/source drift | Trip | Trips before full scan | +| Temporary price/source/provider manipulation + restore | Trip | No semantic violation; then PCL OOG | +| Permanent provider switch to existing oracle | Trip | No semantic violation; then PCL OOG | +| Honest stable borrow | Pass | PCL OOG at 325,294 | +| Two honest borrows | Pass | PCL OOG at 600,747 | +| Ordinary 2% price update with 1% band | Operationally likely pass | Trips | +| Health break inside monitored call | Trip | Trips | +| aToken-ordered break before `finalizeTransfer` | Trip | Passes/skips | +| Production operation wrapper | Register and run | Empty child-suite revert | + +### Fork/backtest status + +No historical or live-fork results are claimed. + +- No RPC/fork URL was present in the environment. +- `pcl doctor` reported `api_health: error` and `auth_capabilities: error` for `https://ethereum.phylax.systems/`. +- The repository contains official deployment addresses but no local archive RPC or cached Horizon transaction corpus. + +Evidence that would resolve this gap: a pinned archive RPC, confirmed block ranges for Horizon Ethereum, the deployed assertion release/configuration and gas budget, and representative successful borrow/withdraw/liquidation/governance transactions. Required backtests should include oracle round-update bundles, every live reserve’s decimals/configuration, multi-call routers, deficit-creation liquidations, and authorized RWA transfers. + +## 10. Unsupported or overstated comments + +1. Reserve NatSpec says transaction-end runs “including direct reserve token movements outside the Pool call surface.” It does not when adopted by the Pool. **Demonstrated by test.** +2. Reserve notice says it protects against external underlying-token balance changes. It detects them only on a successful Pool-touching transaction, potentially later. **Demonstrated by test.** +3. Oracle NatSpec says it catches source swaps “earlier or later in the same transaction.” It catches only endpoint-persistent swaps, not swap/use/restore. **Demonstrated by test.** +4. Oracle notice says it protects the risk state consumed by the Pool. It never samples the oracle at the matching call boundary, so endpoint equality is not proof about the consumed value. **Verified from source; demonstrated by test.** +5. Operation wrapper says a revert means a risk-increasing Pool call violated a safety property. In the PCL runtime it can mean the constructor-created suite has no code. **Demonstrated by test.** +6. Operation helpers say the checks protect against over-liquidation. The collateral check omits the treasury fee leg and both checks are only upper bounds, not settlement correctness. **Verified from source.** +7. Horizon deficit interface says `getReserveDeficit` was added in v3.6. It was added in v3.3. **Verified from official release/source.** +8. The shared suite says it targets close forks generically. Its ABI omits L2 compressed entrypoints and assumes the v3 legacy reserve tuple/provider mock conventions. **Verified from source.** + +## 11. Three materially higher-value missing assertions + +### 1. Call-boundary oracle consumption/configuration invariant + +For every successful risk operation, compare the provider/oracle/source/fallback identities at `PreTx`, `PreCall`, `PostCall`, and `PostTx`; sample the prices at `PreCall` (the value the Pool is about to consume) and bind them to an independent reference or asset-specific policy. Add triggers on provider/oracle configuration writes so oracle-only changes are not invisible. Cover flash-loan debt conversion and liquidation explicitly. + +This is materially better because it detects temporary manipulation used by a real operation rather than merely endpoint drift. It must define governance allowlists and normal feed-update policy to avoid becoming a blanket oracle-change ban. + +### 2. Exact reserve liability/custody no-worsening invariant + +Per reserve: + +`liability = aTokenSupply + accruedToTreasury × normalizedIncome` + +`recognizedBacking = actualCustody + stableDebt + variableDebt + unbacked + deficit` + +Reject any increase in `max(liability - recognizedBacking, 0)` beyond per-asset rounding tolerance. Register ERC-20 change triggers for each underlying and appropriate a/debt-token or storage changes, rather than relying only on Pool TxEnd. Separately reconcile virtual underlying accounting where enabled. + +This preserves donation tolerance, includes treasury claims, tolerates pre-existing bad state without letting it worsen, and runs on the transaction that changes custody. + +### 3. Exact liquidation settlement and deficit reconciliation + +For each liquidation, reconcile: + +- variable/stable debt reduction +- debt asset actually received +- collateral aToken debit +- underlying or aToken delivered to liquidator +- protocol fee delivered to treasury +- collateral/borrowing bitmap transitions +- v3.3 bad debt burned and deficit created + +This detects wrong-recipient transfers, fee mistakes, under/over-burning, and bad deficit accounting that the current upper bounds and Aave’s local validation do not independently prove. + +## 12. Prioritized remediation plan + +1. **Quarantine the production operation wrapper immediately.** Do not advertise or deploy the child-suite form. Flatten suite and assertion into one runtime if any generic checks are retained, then require a real PCL E2E for every monitored selector. +2. **Fix trigger truth before equation work.** Redesign reserve triggers around underlying/aToken/debt-token changes and Pool accounting changes; explicitly test token-only transactions, reverted Pool calls, nested calls, and subsequent-detection behavior. +3. **Replace the reserve formula.** Include indexed `accruedToTreasury`, use per-asset tolerances, and prefer no-worsening deficit semantics. State unsupported token classes. +4. **Replace endpoint oracle logic with call-boundary logic.** Pin provider consistency, source/fallback/proxy identity policy, actual PreCall price, flash-loan debt path, and asset-specific tolerances. Deduplicate accounts/assets and strictly bound calls. +5. **Remove redundant bounds unless upgraded to exact reconciliation.** Withdrawal claim adds little beyond `validateWithdraw`. Replace liquidation upper bounds with full settlement/deficit identity. +6. **Add deployment validation.** Assert constructor provider equals `Pool.ADDRESSES_PROVIDER`, reserve list/count matches live configuration, every stable-debt compatibility address has the expected ABI/zero behavior, and configured gas budget covers worst-case measured paths. +7. **Restore behavioral V3 CI.** Keep the adversarial cases from the research harness, add a pinned Horizon fork, and fail CI if production assertion bytecode—not a flat substitute—cannot register and execute. +8. **Run historical backtests before disposition can improve to Keep.** Measure false positives over representative Horizon and ordinary Aave v3.3 transactions, including governance/source updates and realistic multi-call routers. + +## 13. Remaining uncertainties + +- **Unverified hypothesis:** whether the production sidecar’s actual assertion/precompile budgets and deployment compiler settings differ from the 3 million default recorded in repository history. +- **Unverified hypothesis:** whether any reviewed assertion is currently deployed or active on Horizon; no release manifest was found. +- **Unverified hypothesis:** live Horizon reserve count/configuration and `MOCK_STABLE_DEBT` behavior at a chosen historical block; no RPC was available. +- **Unverified hypothesis:** historical false-positive rates for normal RWA NAV updates, governance bundles, and oracle feed rounds. +- **Supported inference:** nonstandard RWA issuer token administration makes direct custody mutation a plausible high-impact failure, but the exact administrative capabilities differ by underlying asset and need token-by-token source review. +- **Supported inference:** an attacker can force oracle assertion OOG below a 3 million budget with enough successful calls; the exact minimum on the live eleven-reserve market needs a fork benchmark. + +Until those gaps are closed, the evidence supports **Rework/Replace/Remove**, not “Keep, but strengthen tests.” From c8f82c9c286badc8187d95b7a56d021c9eb1a223 Mon Sep 17 00:00:00 2001 From: makemake Date: Wed, 5 Aug 2026 15:15:58 +0200 Subject: [PATCH 8/9] fix(aave): fail closed on skipped oracle reads --- .../AAVE_V4_ORACLE_ASSERTION_DEPLOYMENT.md | 77 ++++++ ...ave-v4-flow-rate-calibration-2026-07-30.md | 72 +++++ ...racle-consumption-protection-2026-07-30.md | 250 ++++++++++++++++++ .../aave/src/AaveV3HorizonOracleAssertion.sol | 6 + .../src/AaveV4OracleConsumptionAssertion.sol | 4 +- .../src/AaveV4OracleConsumptionHelpers.sol | 22 +- .../test/AaveV3HorizonOracleAssertion.t.sol | 15 ++ .../AaveV4OracleConsumptionAssertion.t.sol | 59 +++++ 8 files changed, 501 insertions(+), 4 deletions(-) create mode 100644 examples/aave/AAVE_V4_ORACLE_ASSERTION_DEPLOYMENT.md create mode 100644 examples/aave/research/aave-v4-flow-rate-calibration-2026-07-30.md create mode 100644 examples/aave/research/aave-v4-oracle-consumption-protection-2026-07-30.md diff --git a/examples/aave/AAVE_V4_ORACLE_ASSERTION_DEPLOYMENT.md b/examples/aave/AAVE_V4_ORACLE_ASSERTION_DEPLOYMENT.md new file mode 100644 index 0000000..7f1593e --- /dev/null +++ b/examples/aave/AAVE_V4_ORACLE_ASSERTION_DEPLOYMENT.md @@ -0,0 +1,77 @@ +# Aave V4 consumed-oracle assertion deployment + +`AaveV4EthereumMainSpokeOracleAssertion` is pinned to Ethereum Main Spoke state +at block 25,646,732 and Aave V4 release `v0.5.11`. + +## Constructor + +```solidity +new AaveV4EthereumMainSpokeOracleAssertion( + maxTraceCalls, + deviationBpsByReserveId, + extraVerifiedConfigSlots +); +``` + +- `maxTraceCalls`: transaction-wide maximum for committed + `getReservePrice` calls and configured-source `latestAnswer` calls. `64` is a + practical initial value for the 14-reserve Main Spoke and two full price + sweeps. Lower values reduce worst-case work but reject larger multicalls. +- `deviationBpsByReserveId`: 14 tolerances ordered by reserve ID: + WETH, wstETH, weETH, WBTC, cbBTC, AAVE, LINK, USDC, USDT, EURC, RLUSD, USDG, + frxUSD, GHO. Zero means exact equality with the PreTx price. A value of 100 + permits `[99%, 101%]`, with conservative rounding at both bounds. +- `extraVerifiedConfigSlots`: additional `(target, slot)` guards. The wrapper + already includes all 22 verified mutable Chainlink/CAPO routing slots in the + pinned source graph. Do not add guesses; document the upstream layout and + deployed bytecode for every extra slot. + +The constructor rejects a zero target, zero trace bound, more than 64 policies, +more than 128 total config guards, duplicate guards, duplicate direct sources, +non-contiguous reserve IDs, zero assets/sources, and tolerances at or above +10,000 bps. + +## Adoption checklist + +Before activation: + +1. Confirm chain ID 1 and Main Spoke + `0x94e7A5dCbE816e498b89aB752661904E2F56c485`. +2. Re-read the ERC-1967 implementation slot and require + `0xABd0E26FE17BDe4F1f1187Ed8aA80C274E03D8b5`. +3. Require `ORACLE()` to equal + `0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127`, oracle `spoke()` to point back + to Main Spoke, and oracle decimals to equal 8. +4. Re-read all 14 reserves and direct sources and compare them with the + production wrapper. +5. Review the source graph and built-in config slots against verified deployed + source. Rebuild the wrapper after any upstream migration. +6. Choose asset-specific tolerances. Zero is appropriate only when legitimate + within-transaction source movement is impossible or should be blocked. +7. Run the focused PCL suite with `-vvvv`; confirm parent/child call IDs and + outputs are still exposed as documented. +8. Measure a worst-case multicall under the production executor. The current + 14-reserve/two-sweep fixture measures 2,881,782 gas. + +At runtime, the assertion validates policy completeness and exact PreTx +reserve/source identity before accepting a committed price read. A legitimate +reserve addition, source update, proxy rotation, CAPO parameter update, or +Spoke upgrade intentionally blocks risk-sensitive operations until a reviewed +replacement assertion is deployed. + +## Generic deployments + +`AaveV4OracleConsumptionAssertion` can protect another verified V4 Spoke, but +the caller must provide: + +- the exact Spoke adopter; +- the oracle selected by that implementation; +- the expected Spoke implementation; +- a complete reserve policy with unique direct sources; and +- verified storage guards for every mutable router, proxy, adapter, fallback, + or provider slot that can change a consumed price. + +Do not copy the Ethereum Main Spoke asset list, sources, implementation, or +storage guards to another Spoke or chain. See the accompanying +[research note](research/aave-v4-oracle-consumption-protection-2026-07-30.md) +for the exact source and trace evidence. diff --git a/examples/aave/research/aave-v4-flow-rate-calibration-2026-07-30.md b/examples/aave/research/aave-v4-flow-rate-calibration-2026-07-30.md new file mode 100644 index 0000000..62e1b10 --- /dev/null +++ b/examples/aave/research/aave-v4-flow-rate-calibration-2026-07-30.md @@ -0,0 +1,72 @@ +# Aave v4 flow-rate circuit-breaker calibration + +Snapshot date: 2026-07-30
+Chain: Ethereum mainnet
+Historical block range: 25,430,974–25,646,159
+Lookback: 30 days + +## Asset selection + +The three assets were selected from DefiLlama's aggregate Aave v4 token TVL: + +| Rank | Asset | Aggregate token TVL | +| --- | --- | ---: | +| 1 | WBTC | $52.35m | +| 2 | USDG | $30.84m | +| 3 | wstETH | $29.40m | + +DefiLlama derives Aave v4 TVL from ERC20 balances held by the Core, Plus, and +Prime Hubs. WBTC and wstETH are split between Core and Prime, so both Hubs need +their own adopter-scoped watchers. USDG is held only by Core. + +Sources: + +- +- +- + +## Methodology + +For each Hub/token pair: + +1. Query every ERC20 `Transfer` to and from the Hub during the 30-day range. +2. Reconstruct the Hub balance at the beginning of the range from the current + balance and the net transfer flow. +3. Calculate the maximum rolling 24-hour net directional flow as basis points + of the Hub balance immediately before the window's first transfer. +4. Bucket net flow into 10-second intervals and calculate the peak flow rate as + basis points of the Hub balance per second. +5. Set each production limit to `ceil(observed maximum × 1.20)`. + +This intentionally calibrates to the maximum observed window rather than the +30-day daily average. A breaker set from the average would have rejected +legitimate historical spikes. + +The Phylax cumulative watcher measures net flow: inflows offset outflows and +vice versa. It does not cap gross volume. The peak-rate signal is experimental +and comes from the same 10-second buckets. + +## Results + +| Hub | Asset | Current balance | 30d gross in | 30d gross out | Max 24h net in | In limit | Max 24h net out | Out limit | Peak in rate | In-rate limit | Peak out rate | Out-rate limit | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| Core | WBTC | 670.9402 | 129.0485 | 20.8481 | 986.44 bps | 1,184 bps | 91.41 bps | 110 bps | 39.32 bps/s | 48 bps/s | 7.22 bps/s | 9 bps/s | +| Core | USDG | 30.6733m | 68.6104m | 61.5900m | 4,329.38 bps | 5,196 bps | 5,364.94 bps | 6,438 bps | 438.64 bps/s | 527 bps/s | 128.27 bps/s | 154 bps/s | +| Core | wstETH | 8,490.4974 | 4,365.9474 | 1,416.9640 | 1,587.63 bps | 1,906 bps | 776.51 bps | 932 bps | 47.57 bps/s | 58 bps/s | 77.62 bps/s | 94 bps/s | +| Prime | WBTC | 136.7563 | 57.6882 | 47.4818 | 1,750.94 bps | 2,102 bps | 1,972.12 bps | 2,367 bps | 123.91 bps/s | 149 bps/s | 148.27 bps/s | 178 bps/s | +| Prime | wstETH | 3,840.5454 | 2,142.8947 | 1,233.7078 | 2,903.51 bps | 3,485 bps | 757.23 bps | 909 bps | 175.60 bps/s | 211 bps/s | 60.15 bps/s | 73 bps/s | + +## Operational notes + +- The assertion uses a 1 bps cumulative dispatch floor so the custom rate check + executes well before any calibrated limit. The policy trips if either the + cumulative limit or peak-rate limit is exceeded. +- Apply the Core assertion to the Core Hub and the Prime companion assertion to + the Prime Hub. Adopting either assertion on another address fails explicitly. +- Recalibrate before production rollout and after material cap, asset-mix, or + flow-regime changes. Thirty days is a useful initial sample, not a permanent + risk parameter. +- `inflowRate()` and `outflowRate()` require + `AssertionSpec.Experimental`; the public Phylax docs describe Experimental as + unrestricted and potentially untested. This draft should remain staged until + the runtime support and production policy are confirmed. diff --git a/examples/aave/research/aave-v4-oracle-consumption-protection-2026-07-30.md b/examples/aave/research/aave-v4-oracle-consumption-protection-2026-07-30.md new file mode 100644 index 0000000..9cb19b7 --- /dev/null +++ b/examples/aave/research/aave-v4-oracle-consumption-protection-2026-07-30.md @@ -0,0 +1,250 @@ +# Aave V4 consumed-oracle-price protection + +Research date: 2026-07-30
+Chain: Ethereum mainnet (chain ID 1)
+State pin: block 25,646,732
+Upstream release: `v0.5.11`
+Upstream commit: `cdacec509e4f848bff1a5556f503afa83eee3b79` + +## Deployment and source pin + +This implementation protects the Ethereum Main Spoke, not an inferred Aave V3 +deployment: + +| Component | Address | +| --- | --- | +| Main Spoke proxy | `0x94e7A5dCbE816e498b89aB752661904E2F56c485` | +| Main Spoke implementation | `0xABd0E26FE17BDe4F1f1187Ed8aA80C274E03D8b5` | +| Main Spoke `AaveOracle` | `0x99B2B6CEa9C3D2fd8F4d90f86741C44B212a6127` | + +The implementation was resolved from the ERC-1967 implementation slot at the +state pin. Sourcify reports a full runtime match. A local build of Aave V4 tag +`v0.5.11` used Solidity 0.8.28, optimizer 750, IR compilation, Cancun EVM, and +`bytecodeHash = none`; its compiler metadata tail matches the deployed +implementation and oracle. The official Aave address book was inspected at +commit `4ae19b95f84b077c28633ca1d0f9a6750a3ea1d4`, and its Aave V4 submodule +resolves the same release family. + +Primary references: + +- [Aave V4 `v0.5.11` source](https://github.com/aave/aave-v4/tree/cdacec509e4f848bff1a5556f503afa83eee3b79) +- [Pinned `AaveOracle.sol`](https://github.com/aave/aave-v4/blob/cdacec509e4f848bff1a5556f503afa83eee3b79/src/spoke/AaveOracle.sol) +- [Pinned `Spoke.sol`](https://github.com/aave/aave-v4/blob/cdacec509e4f848bff1a5556f503afa83eee3b79/src/spoke/Spoke.sol) +- [Pinned liquidation logic](https://github.com/aave/aave-v4/blob/cdacec509e4f848bff1a5556f503afa83eee3b79/src/spoke/libraries/LiquidationLogic.sol) +- [Official Ethereum address book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV4Ethereum.sol) +- [Aave V4 Ethereum activation](https://governance.aave.com/t/arfc-aave-v4-activation-on-ethereum-mainnet/24293) +- [Aave V4 live announcement](https://aave.com/blog/aave-v4-live-ethereum) + +## Exact oracle architecture + +The V4 Main Spoke has an immutable `ORACLE`; it does not discover a V3-style +provider at runtime. Its oracle is spoke-specific because reserve IDs, rather +than asset addresses, key the source mapping. + +`AaveOracle` has: + +- immutable `DECIMALS`, equal to 8; +- `address public spoke` in storage slot 0; +- `mapping(uint256 => IPriceFeed) _sources` in storage slot 1; +- `getReservePrice(uint256)`, which calls exactly + `IPriceFeed(source).latestAnswer()`, rejects a missing source and every + non-positive answer, and casts the positive `int256` to `uint256`; +- `getReservesPrices(uint256[])`, used here only at the PreTx fork for a + consistent baseline batch; and +- no fallback oracle, addresses provider, cached price, `latestRoundData` + timestamp check, or decimal conversion. + +Prices consumed by the Spoke are therefore positive, 8-decimal values in the +configured feed's output denomination. Every Main Spoke source at the pin +returns the deployment's normalized USD price. + +The verified oracle storage layout is the only hard-coded AaveOracle storage +assumption: `_sources[reserveId]` is +`keccak256(abi.encode(reserveId, uint256(1)))`. The Spoke implementation is +guarded through the standard ERC-1967 implementation slot. No V3 provider, +asset-keyed source mapping, fallback slot, or Chainlink round-data interface is +used. + +## Main Spoke reserve policy + +The PreTx Main Spoke has 14 contiguous reserve IDs: + +| ID | Asset | Underlying | Hub asset ID | Decimals | Direct source | Verified source type | +| ---: | --- | --- | ---: | ---: | --- | --- | +| 0 | WETH | `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` | 0 | 18 | `0x5424384B256154046E9667dDFaaa5e550145215e` | `EACAggregatorProxy` | +| 1 | wstETH | `0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0` | 1 | 18 | `0xe1D97bF61901B075E9626c8A2340a7De385861Ef` | `WstETHPriceCapAdapter` | +| 2 | weETH | `0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee` | 2 | 18 | `0x87625393534d5C102cADB66D37201dF24cc26d4C` | `WeETHPriceCapAdapter` | +| 3 | WBTC | `0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599` | 11 | 8 | `0xDaa4B74C6bAc4e25188e64ebc68DB5050b690cAc` | `CLSynchronicityPriceAdapterPegToBase` | +| 4 | cbBTC | `0xcBb7C0000aB88B473b1f5AFd9ef808440eED33BF` | 12 | 8 | `0xb41E773f507F7a7EA890b1afB7d2b660c30C8B0A` | `EACAggregatorProxy` | +| 5 | AAVE | `0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2dDAE9` | 15 | 18 | `0xF02C1e2A3B77c1cacC72f72B44f7d0a4c62e4a85` | `EACAggregatorProxy` | +| 6 | LINK | `0x514910771AF9Ca656af840dff83E8264EcF986CA` | 16 | 18 | `0xC7e9b623ed51F033b32AE7f1282b1AD62C28C183` | `EACAggregatorProxy` | +| 7 | USDC | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | 5 | 6 | `0x3f73F03aa83B2A48ed27E964eD0fDb590332095B` | `PriceCapAdapterStable` | +| 8 | USDT | `0xdAC17F958D2ee523a2206206994597C13D831ec7` | 4 | 6 | `0x260326c220E469358846b187eE53328303Efe19C` | `PriceCapAdapterStable` | +| 9 | EURC | `0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c` | 10 | 6 | `0xa6aB031A4d189B24628EC9Eb155F0a0f1A0E55a3` | `EURPriceCapAdapterStable` | +| 10 | RLUSD | `0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD` | 7 | 18 | `0xf0eaC18E908B34770FDEe46d069c846bDa866759` | `PriceCapAdapterStable` | +| 11 | USDG | `0xe343167631d89B6Ffc58B88d6b7fB0228795491D` | 8 | 6 | `0x83D20dEEdcd4aC1313496c8CBcAad0fa298c0CE4` | `PriceCapAdapterStable` | +| 12 | frxUSD | `0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29` | 9 | 18 | `0x25DEd2f9aE6ae9416693AB63Abe3aB25493861FD` | `PriceCapAdapterStable` | +| 13 | GHO | `0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f` | 6 | 18 | `0xD110cac5d8682A3b045D5524a9903E031d70FCCd` | `GhoOracle` | + +The assertion requires the live reserve count to equal the policy count, every +policy index to equal its reserve ID, the PreTx underlying and direct source to +match, and every direct source to be unique. Unique sources are required +because this PCL build does not expose the nested oracle calldata described +below. + +## Mutable source graph + +The production wrapper includes 22 write guards. These are not generic proxy +guesses: + +- Chainlink `EACAggregatorProxy` inherits `Owned`; its packed active + phase/aggregator is slot 2. Changing the active aggregator necessarily writes + this slot, including a change followed by restoration. +- `PriceCapAdapterBase` stores the packed snapshot ratio, timestamp, and growth + rate in slot 1 and the maximum yearly growth parameter in slot 2. +- `PriceCapAdapterStable` and `EURPriceCapAdapterStable` store their active cap + in slot 2. + +The guarded transitive EAC proxies are WETH/USD +`0x5424384B256154046E9667dDFaaa5e550145215e`, cbBTC/USD +`0xb41E773f507F7a7EA890b1afB7d2b660c30C8B0A`, AAVE/USD +`0xF02C1e2A3B77c1cacC72f72B44f7d0a4c62e4a85`, LINK/USD +`0xC7e9b623ed51F033b32AE7f1282b1AD62C28C183`, WBTC/BTC +`0xfdFD9C85aD200c506Cf9e21F1FD8dd01932FBB23`, and the underlying +USDC, USDT, RLUSD, USDG, frxUSD, EURC, and EUR feeds embedded in the cap +adapters. The cap adapters' immutable dependency getters were queried directly; +an immutable dependency cannot be replaced without replacing code. + +Direct source replacement is guarded separately by scanning writes to every +`AaveOracle._sources[reserveId]` mapping slot. Spoke implementation and beacon +writes are guarded through their ERC-1967 slots. Extra verified slots can be +supplied to the production wrapper, and are checked for duplicates. + +## Price-consuming operation coverage + +The exact ABI selectors at `v0.5.11` are: + +| Operation | Selector | Price behavior | +| --- | --- | --- | +| `borrow(uint256,uint256,address)` | `0xd6bda0c0` | Always refreshes and validates user account data; reads every active collateral/borrow reserve. | +| `withdraw(uint256,uint256,address)` | `0x0ad58d2f` | Reads prices only when the withdrawn reserve is currently collateral. | +| `liquidationCall(uint256,uint256,address,uint256,bool)` | `0xc2fa746c` | Reads active account prices, then explicit collateral and debt prices, and recalculates account data after a non-deficit liquidation. | +| `setUsingAsCollateral(uint256,bool,address)` | `0x9e35c533` | Disabling collateral recalculates prices; enabling only refreshes dynamic configuration. | +| `updateUserRiskPremium(address)` | `0x91c46d09` | Recalculates account data with current dynamic configuration. | +| `updateUserDynamicConfig(address)` | `0x826002e2` | Refreshes dynamic configuration and validates recalculated account data. | + +`multicall(bytes[])` is `0xac9650d8`. Its delegatecall legs remain separate +committed trace nodes, so all price reads from all legs are scanned once at +transaction end. + +Omissions are deliberate: + +- `supply` (`0x852a56a5`) and `repay` (`0xb1e8f8ef`) do not call the oracle. +- `addReserve` and `updateReservePriceSource` validate a candidate source by + calling `latestAnswer`, but do not use that answer to commit user risk, + borrowing, withdrawal, or liquidation state. A later risk operation is + protected, and source mapping writes in the same transaction are rejected. +- Hub accounting operations have no oracle dependency. +- Read-only account-data queries do not modify protocol state. If executed + inside a transaction they may cause a conservative extra check, but cannot + bypass one. +- The pinned Spoke has no native flash-loan callback. A surrounding protocol + callback can call the Spoke, and those nested Spoke calls are covered. + Reentrancy into an already executing Spoke operation is independently blocked + by V4's `nonReentrant` modifier. + +Only `getReservePrice(uint256)` (`0xd45c35ff`) is a committed consumption +surface. `getReservesPrices(uint256[])` (`0x7b5b8e9f`) is not called by the +Spoke's state-changing paths. + +## Trace evidence and exact mapping + +PCL 1.6.0 `-vvvv` proves all of the following: + +1. A Spoke operation, nested callback operation, and delegatecall multicall leg + are recorded with their real selector, success flag, parent, and depth. +2. Every Spoke-to-AaveOracle `getReservePrice` STATICCALL is recorded, but in + this compiler/executor path its `TriggerCall.input` is empty. The + implementation therefore does not claim that nested calldata is available. +3. Each price call has exactly one direct + AaveOracle-to-configured-source `latestAnswer()` (`0x50d25bcd`) child. + Source addresses are unique in the protected deployment, so the child target + maps the parent call to an exact reserve. +4. `callOutputAt` exposes both the child `int256` answer and the parent + `uint256` oracle return. The assertion requires both to be 32 bytes, the + source answer to be positive, and the values to be exactly equal before + applying the deviation envelope. +5. A reverted-and-caught borrow and its oracle subtree are absent from + `matchingCalls`, even if `successOnly` is disabled. Only committed calls + contribute evidence. + +An unmatched parent price call, zero/multiple matching source children, +malformed output, non-positive answer, source/parent output mismatch, unknown +source, or incomplete policy fails closed. + +The PreTx batch is called on the configured oracle at the PreTx fork. Source +identity is also read at PreTx. No PostTx-selected oracle or source is ever used +to construct a PreTx baseline. + +## Bounds and gas + +Both parent oracle calls and source-call scans request `maxTraceCalls + 1` and +fail if the configured maximum is exceeded. Total source matches across all +configured sources are also bounded. This can conservatively reject a +transaction that deliberately spams the same source outside Aave; it cannot +silently truncate an Aave read. + +Measured with PCL 1.6.0 and the Aave profile: + +| Scenario | Assertion gas | +| --- | ---: | +| Stable two-reserve borrow | 267,102 | +| Manipulated two-operation multicall, rejection path | 283,524 | +| Nested callback manipulation, rejection path | 267,451 | +| Stable one-reserve/two-operation multicall | 206,353 | +| Stable 14-reserve sweep plus 22 production-equivalent config guards | 1,792,565 | +| Two stable 14-reserve sweeps plus 22 guards | 2,881,782 | + +The realistic multicall stays below the requested 3,000,000 production budget +with 118,218 gas of measured margin. PCL 1.6.0's local CredibleTest runner still +enforces a legacy 300,000 assertion ceiling. The two realistic tests therefore +expect the runner's `Assertion exceeded gas limit` wrapper while `-vvvv` +reports the complete measured execution cost. The single-operation, callback, +rejection, and small multicall behavioral tests run normally under that local +ceiling. + +## Native checks and residual risk + +Aave V4 natively requires a configured source, 8 source decimals at +configuration time, and a positive `latestAnswer` at consumption time. Borrow, +withdraw, and configuration-refresh paths validate health factor using the +price they just read. Liquidation validates eligibility and calculates amounts +using current prices. None of those checks asks whether that current price was +temporarily changed after transaction start; health factor and liquidation +math can be internally consistent around an attacker-selected transient price. +The V4 functions also expose no general user-provided oracle-deviation bound. + +This invariant adds a transaction-start anchor and checks the exact price +returned to Aave at every committed consumption. Restoring the feed, source, +adapter cap, proxy aggregator, or Spoke implementation before transaction end +does not erase the trace output or write evidence. + +Residual limitations and false-positive risks: + +- A price already manipulated before PreTx is accepted as the baseline. An + independent reference-price/freshness assertion is complementary, not a + replacement. +- V4 intentionally calls legacy `latestAnswer`; no timestamp or round metadata + reaches AaveOracle. This assertion matches V4 semantics and cannot infer + staleness. A separate source-specific freshness assertion should use verified + downstream interfaces. +- Legitimate reserve additions, source migrations, Spoke upgrades, guarded + cap changes, or Chainlink aggregator rotations fail closed until the pinned + assertion is redeployed. +- The 118k realistic-multicall gas margin is narrow. Increase in reserve count, + guard count, or multicall price sweeps requires remeasurement; a lower + `maxTraceCalls` bounds cost but can conservatively reject large transactions. +- A future deployment that shares one direct source across reserve IDs cannot + use source-address mapping safely. This constructor rejects shared sources; + support requires nested oracle calldata from the executor or another verified + reserve-context signal. diff --git a/examples/aave/src/AaveV3HorizonOracleAssertion.sol b/examples/aave/src/AaveV3HorizonOracleAssertion.sol index 6e32db0..4c63886 100644 --- a/examples/aave/src/AaveV3HorizonOracleAssertion.sol +++ b/examples/aave/src/AaveV3HorizonOracleAssertion.sol @@ -166,15 +166,18 @@ contract AaveV3HorizonOracleAssertion is AaveV3HorizonHelpers { ph.getStaticCallInputs(ADDRESSES_PROVIDER, IAaveV3LikeAddressesProvider.getPriceOracle.selector); require(providerCalls.length <= MAX_TRACE_CALLS, "AaveV3Horizon: too many provider calls"); + bool poolProviderCallSeen; for (uint256 i; i < providerCalls.length; ++i) { if (providerCalls[i].caller != POOL) { continue; } + poolProviderCallSeen = true; bytes memory output = ph.callOutputAt(providerCalls[i].id); require(output.length == 32, "AaveV3Horizon: malformed provider output"); require(abi.decode(output, (address)) == expectedOracle, "AaveV3Horizon: Pool consumed a different oracle"); } + require(poolProviderCallSeen, "AaveV3Horizon: Pool skipped oracle provider"); } function _assertConsumedPrices(address oracle, address[] memory sources, uint256[] memory baselinePrices) @@ -219,11 +222,14 @@ contract AaveV3HorizonOracleAssertion is AaveV3HorizonHelpers { } } + bool poolPriceCallSeen; for (uint256 i; i < priceCalls.length; ++i) { if (priceCalls[i].caller == POOL) { + poolPriceCallSeen = true; require(mappedPriceCalls[i], "AaveV3Horizon: unrecognized Pool oracle price path"); } } + require(poolPriceCallSeen, "AaveV3Horizon: Pool skipped oracle prices"); } function _priceCallAsset(bytes memory priceCallInput) internal pure returns (bool available, address asset) { diff --git a/examples/aave/src/AaveV4OracleConsumptionAssertion.sol b/examples/aave/src/AaveV4OracleConsumptionAssertion.sol index 61cd36b..cb7328c 100644 --- a/examples/aave/src/AaveV4OracleConsumptionAssertion.sol +++ b/examples/aave/src/AaveV4OracleConsumptionAssertion.sol @@ -78,7 +78,7 @@ contract AaveV4OracleConsumptionAssertion is AaveV4OracleConsumptionHelpers { require(policy.reserveId == i, "AaveV4Oracle: policies not contiguous"); require(policy.asset != address(0), "AaveV4Oracle: policy asset zero"); require(policy.source != address(0), "AaveV4Oracle: policy source zero"); - require(policy.deviationBps <= BPS, "AaveV4Oracle: bad tolerance"); + require(policy.deviationBps < BPS, "AaveV4Oracle: bad tolerance"); for (uint256 j; j < i; ++j) { require(reservePolicies_[j].source != policy.source, "AaveV4Oracle: source must map one reserve"); } @@ -129,7 +129,7 @@ contract AaveV4OracleConsumptionAssertion is AaveV4OracleConsumptionHelpers { bool mandatoryPriceOperation; if (spokePriceCallCount == 0) { - mandatoryPriceOperation = _hasMandatoryPriceOperation(SPOKE); + mandatoryPriceOperation = _hasMandatoryPriceOperation(SPOKE, MAX_TRACE_CALLS); } if (spokePriceCallCount == 0 && !mandatoryPriceOperation) { return; diff --git a/examples/aave/src/AaveV4OracleConsumptionHelpers.sol b/examples/aave/src/AaveV4OracleConsumptionHelpers.sol index 284be9c..6e49579 100644 --- a/examples/aave/src/AaveV4OracleConsumptionHelpers.sol +++ b/examples/aave/src/AaveV4OracleConsumptionHelpers.sol @@ -73,8 +73,26 @@ abstract contract AaveV4OracleConsumptionHelpers is AaveV4Helpers { return _successfulStaticCalls(source, IAaveV4PriceFeed.latestAnswer.selector, limit); } - function _hasMandatoryPriceOperation(address spoke) internal view returns (bool) { + function _hasMandatoryPriceOperation(address spoke, uint256 maxTraceCalls) internal view returns (bool) { return _matchingCalls(spoke, IAaveV4Spoke.borrow.selector, 1).length != 0 - || _matchingCalls(spoke, IAaveV4Spoke.liquidationCall.selector, 1).length != 0; + || _matchingCalls(spoke, IAaveV4Spoke.withdraw.selector, 1).length != 0 + || _matchingCalls(spoke, IAaveV4Spoke.liquidationCall.selector, 1).length != 0 + || _hasCollateralDisable(spoke, maxTraceCalls) + || _matchingCalls(spoke, IAaveV4Spoke.updateUserRiskPremium.selector, 1).length != 0 + || _matchingCalls(spoke, IAaveV4Spoke.updateUserDynamicConfig.selector, 1).length != 0; + } + + function _hasCollateralDisable(address spoke, uint256 maxTraceCalls) private view returns (bool) { + PhEvm.TriggerCall[] memory calls = + _matchingCalls(spoke, IAaveV4Spoke.setUsingAsCollateral.selector, maxTraceCalls + 1); + require(calls.length <= maxTraceCalls, "AaveV4Oracle: trace limit exceeded"); + for (uint256 i; i < calls.length; ++i) { + require(calls[i].input.length == 32 * 3, "AaveV4Oracle: malformed collateral input"); + (, bool usingAsCollateral,) = abi.decode(calls[i].input, (uint256, bool, address)); + if (!usingAsCollateral) { + return true; + } + } + return false; } } diff --git a/examples/aave/test/AaveV3HorizonOracleAssertion.t.sol b/examples/aave/test/AaveV3HorizonOracleAssertion.t.sol index c7c1c32..c1777e0 100644 --- a/examples/aave/test/AaveV3HorizonOracleAssertion.t.sol +++ b/examples/aave/test/AaveV3HorizonOracleAssertion.t.sol @@ -97,6 +97,7 @@ interface IOracleGuardFlashReceiver { contract OracleGuardPool { OracleGuardProvider internal immutable provider; address[] internal assets; + bool internal consumePrices = true; constructor(OracleGuardProvider provider_, address[] memory assets_) { provider = provider_; @@ -107,6 +108,10 @@ contract OracleGuardPool { assets.push(asset); } + function setConsumePrices(bool enabled) external { + consumePrices = enabled; + } + function getReservesList() external view returns (address[] memory) { return assets; } @@ -152,6 +157,9 @@ contract OracleGuardPool { } function _consumePrices() internal view { + if (!consumePrices) { + return; + } OracleGuardOracle oracle = OracleGuardOracle(provider.getPriceOracle()); for (uint256 i; i < assets.length; ++i) { oracle.getAssetPrice(assets[i]); @@ -307,6 +315,13 @@ contract AaveV3HorizonOracleAssertionTest is Test, CredibleTest { pool.borrow(asset0, 1, 2, 0, address(this)); } + function testBorrowWithoutProviderOrPriceReadFailsClosed() public { + pool.setConsumePrices(false); + _arm(MAX_TRACE_CALLS); + vm.expectRevert(bytes("AaveV3Horizon: Pool skipped oracle provider")); + pool.borrow(asset0, 1, 2, 0, address(this)); + } + function testStableMulticallPassesBelowThreeMillionGas() public { _arm(MAX_TRACE_CALLS); bundle.twoBorrows(pool, asset0); diff --git a/examples/aave/test/AaveV4OracleConsumptionAssertion.t.sol b/examples/aave/test/AaveV4OracleConsumptionAssertion.t.sol index 60a8542..e1366ff 100644 --- a/examples/aave/test/AaveV4OracleConsumptionAssertion.t.sol +++ b/examples/aave/test/AaveV4OracleConsumptionAssertion.t.sol @@ -98,6 +98,7 @@ contract MockV4Spoke { address public immutable ORACLE; IAaveV4Spoke.Reserve[] internal reserves; bool internal readUnknownReserve; + bool internal consumePrices = true; constructor(address oracle_) { ORACLE = oracle_; @@ -121,6 +122,10 @@ contract MockV4Spoke { readUnknownReserve = enabled; } + function setConsumePrices(bool enabled) external { + consumePrices = enabled; + } + function getReserveCount() external view returns (uint256) { return reserves.length; } @@ -176,6 +181,9 @@ contract MockV4Spoke { } function _consumePrices() internal view { + if (!consumePrices) { + return; + } MockV4Oracle oracle = MockV4Oracle(ORACLE); for (uint256 i; i < reserves.length; ++i) { oracle.getReservePrice(i); @@ -389,6 +397,34 @@ contract AaveV4OracleConsumptionAssertionTest is Test, CredibleTest { spoke.borrow(0, 1, address(this)); } + function testBorrowWithoutPriceReadFailsClosed() public { + _expectMandatoryPathWithoutPriceFails(abi.encodeCall(MockV4Spoke.borrow, (0, 1, address(this)))); + } + + function testWithdrawWithoutPriceReadFailsClosed() public { + _expectMandatoryPathWithoutPriceFails(abi.encodeCall(MockV4Spoke.withdraw, (0, 1, address(this)))); + } + + function testLiquidationWithoutPriceReadFailsClosed() public { + _expectMandatoryPathWithoutPriceFails( + abi.encodeCall(MockV4Spoke.liquidationCall, (0, 1, address(this), 1, false)) + ); + } + + function testCollateralDisableWithoutPriceReadFailsClosed() public { + _expectMandatoryPathWithoutPriceFails( + abi.encodeCall(MockV4Spoke.setUsingAsCollateral, (0, false, address(this))) + ); + } + + function testRiskPremiumRefreshWithoutPriceReadFailsClosed() public { + _expectMandatoryPathWithoutPriceFails(abi.encodeCall(MockV4Spoke.updateUserRiskPremium, (address(this)))); + } + + function testDynamicConfigRefreshWithoutPriceReadFailsClosed() public { + _expectMandatoryPathWithoutPriceFails(abi.encodeCall(MockV4Spoke.updateUserDynamicConfig, (address(this)))); + } + function testHonestWithdrawPricePath() public { _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); spoke.withdraw(0, 1, address(this)); @@ -600,6 +636,17 @@ contract AaveV4OracleConsumptionAssertionTest is Test, CredibleTest { driver.priceBorrowRestore(source1, PRICE + 1, PRICE, spoke); } + function testFullRangeToleranceIsRejected() public { + AaveV4OracleConsumptionAssertion.ReservePolicy[] memory policies = _twoPolicies(10_000, address(source1)); + AaveV4OracleConsumptionAssertion.ConfigSlotGuard[] memory configGuards = + new AaveV4OracleConsumptionAssertion.ConfigSlotGuard[](0); + + vm.expectRevert(bytes("AaveV4Oracle: bad tolerance")); + new AaveV4OracleConsumptionAssertion( + address(spoke), address(oracle), address(0), MAX_TRACE_CALLS, policies, configGuards + ); + } + /// @dev Same-transaction protection cannot identify a baseline corrupted before PreTx. function testPreExistingManipulationIsDocumentedFalseNegative() public { source1.setAnswer(2 * PRICE); @@ -686,6 +733,18 @@ contract AaveV4OracleConsumptionAssertionTest is Test, CredibleTest { _armCustom(address(spoke), address(oracle), address(0), maxTraceCalls, policies, configGuards); } + function _expectMandatoryPathWithoutPriceFails(bytes memory callData) internal { + spoke.setConsumePrices(false); + _arm(MAX_TRACE_CALLS, DEVIATION_BPS, 2); + vm.expectRevert(bytes("AaveV4Oracle: unrecognized price path")); + (bool success, bytes memory result) = address(spoke).call(callData); + if (!success) { + assembly ("memory-safe") { + revert(add(result, 32), mload(result)) + } + } + } + function _twoPolicies(uint256 deviationBps, address secondSource) internal view From 1e08b44ccb0dc0b2200bce76dfdcdba971d09647 Mon Sep 17 00:00:00 2001 From: makemake Date: Wed, 5 Aug 2026 16:14:35 +0200 Subject: [PATCH 9/9] fix(aave): close remaining review gaps --- examples/aave/README.md | 10 +++--- ...ave-v4-flow-rate-calibration-2026-07-30.md | 13 ++++++-- .../aave-v4-flow-rate-observed-maxima.csv | 6 ++++ .../research/calculate-aave-v4-flow-limits.py | 13 ++++++++ examples/aave/src/AaveV3HorizonInterfaces.sol | 1 + .../aave/src/AaveV3HorizonOracleAssertion.sol | 2 +- .../AaveV3HorizonReserveBackingAssertion.sol | 19 ++++++----- .../src/AaveV4HubFlowRateCircuitBreaker.sol | 9 ++--- .../aave/test/AaveV3AdversarialResearch.t.sol | 33 ++++++++++++++++++- .../test/AaveV3HorizonOracleAssertion.t.sol | 9 +++++ .../AaveV4HubFlowRateCircuitBreaker.t.sol | 4 +-- 11 files changed, 96 insertions(+), 23 deletions(-) create mode 100644 examples/aave/research/aave-v4-flow-rate-observed-maxima.csv create mode 100644 examples/aave/research/calculate-aave-v4-flow-limits.py diff --git a/examples/aave/README.md b/examples/aave/README.md index 8062343..10031c4 100644 --- a/examples/aave/README.md +++ b/examples/aave/README.md @@ -13,10 +13,10 @@ FOUNDRY_PROFILE=aave forge build - AaveV3HorizonHelpers.sol - AaveV3HorizonInterfaces.sol - AaveV3HorizonOracleAssertion.sol -- AaveV3HorizonReserveBackingAssertion.sol +- AaveV3HorizonReserveBackingAssertion.sol (quarantined: token-only mutations do not dispatch a Pool-adopter trigger) - AaveV4Helpers.sol - AaveV4HubAccountingAssertion.sol -- AaveV4HubFlowRateCircuitBreaker.sol +- AaveV4HubFlowRateCircuitBreaker.sol (quarantined: net-flow dispatch can miss directional reversals) - AaveV4Interfaces.sol - AaveV4OracleConsumptionAssertion.sol - AaveV4OracleConsumptionHelpers.sol @@ -58,10 +58,12 @@ See the ## Aave v4 flow-rate calibration -The ready-to-adopt Core and Prime Hub circuit breakers cover WBTC, USDG, and +The staged Core and Prime Hub circuit breakers define policies for WBTC, USDG, and wstETH, the three highest-TVL Aave v4 assets in the 2026-07-30 DefiLlama snapshot. Their 24-hour cumulative-flow and 10-second peak-rate limits use a -20% buffer over the maximum observed values in the preceding 30 days. +20% buffer over the maximum observed values in the preceding 30 days. Their +triggers remain unarmed until absolute directional or rate-native dispatch can +cover reversals inside a net-flow window. See [aave-v4-flow-rate-calibration-2026-07-30.md](research/aave-v4-flow-rate-calibration-2026-07-30.md) diff --git a/examples/aave/research/aave-v4-flow-rate-calibration-2026-07-30.md b/examples/aave/research/aave-v4-flow-rate-calibration-2026-07-30.md index 62e1b10..fa3963c 100644 --- a/examples/aave/research/aave-v4-flow-rate-calibration-2026-07-30.md +++ b/examples/aave/research/aave-v4-flow-rate-calibration-2026-07-30.md @@ -51,16 +51,25 @@ and comes from the same 10-second buckets. | Hub | Asset | Current balance | 30d gross in | 30d gross out | Max 24h net in | In limit | Max 24h net out | Out limit | Peak in rate | In-rate limit | Peak out rate | Out-rate limit | | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | | Core | WBTC | 670.9402 | 129.0485 | 20.8481 | 986.44 bps | 1,184 bps | 91.41 bps | 110 bps | 39.32 bps/s | 48 bps/s | 7.22 bps/s | 9 bps/s | -| Core | USDG | 30.6733m | 68.6104m | 61.5900m | 4,329.38 bps | 5,196 bps | 5,364.94 bps | 6,438 bps | 438.64 bps/s | 527 bps/s | 128.27 bps/s | 154 bps/s | +| Core | USDG | 30.6733m | 68.6104m | 61.5900m | 4,329.38 bps | 5,196 bps | 4,472.00 bps | 5,367 bps | 438.64 bps/s | 527 bps/s | 128.27 bps/s | 154 bps/s | | Core | wstETH | 8,490.4974 | 4,365.9474 | 1,416.9640 | 1,587.63 bps | 1,906 bps | 776.51 bps | 932 bps | 47.57 bps/s | 58 bps/s | 77.62 bps/s | 94 bps/s | -| Prime | WBTC | 136.7563 | 57.6882 | 47.4818 | 1,750.94 bps | 2,102 bps | 1,972.12 bps | 2,367 bps | 123.91 bps/s | 149 bps/s | 148.27 bps/s | 178 bps/s | +| Prime | WBTC | 136.7563 | 57.6882 | 47.4818 | 1,750.94 bps | 2,102 bps | 2,026.00 bps | 2,432 bps | 123.91 bps/s | 149 bps/s | 148.27 bps/s | 178 bps/s | | Prime | wstETH | 3,840.5454 | 2,142.8947 | 1,233.7078 | 2,903.51 bps | 3,485 bps | 757.23 bps | 909 bps | 175.60 bps/s | 211 bps/s | 60.15 bps/s | 73 bps/s | +The observed maxima used for these constants are checked in as +`aave-v4-flow-rate-observed-maxima.csv`; `calculate-aave-v4-flow-limits.py` +recomputes every limit as `ceil(observed × 1.20)`. The table and chart were +reconciled to that artifact, notably for Core USDG and Prime WBTC outflow. + ## Operational notes - The assertion uses a 1 bps cumulative dispatch floor so the custom rate check executes well before any calibrated limit. The policy trips if either the cumulative limit or peak-rate limit is exceeded. +- The trigger is currently unarmed because net-flow dispatch can fail to select + the directional rate assertion after opposite-direction flow in the same + window. Keep this policy staged until absolute directional or rate-native + dispatch is available. - Apply the Core assertion to the Core Hub and the Prime companion assertion to the Prime Hub. Adopting either assertion on another address fails explicitly. - Recalibrate before production rollout and after material cap, asset-mix, or diff --git a/examples/aave/research/aave-v4-flow-rate-observed-maxima.csv b/examples/aave/research/aave-v4-flow-rate-observed-maxima.csv new file mode 100644 index 0000000..8bd9b07 --- /dev/null +++ b/examples/aave/research/aave-v4-flow-rate-observed-maxima.csv @@ -0,0 +1,6 @@ +hub,asset,in_window_bps,out_window_bps,in_peak_rate_bps,out_peak_rate_bps +Core,WBTC,986.44,91.41,39.32,7.22 +Core,USDG,4329.38,4472.00,438.64,128.27 +Core,wstETH,1587.63,776.51,47.57,77.62 +Prime,WBTC,1750.94,2026.00,123.91,148.27 +Prime,wstETH,2903.51,757.23,175.60,60.15 diff --git a/examples/aave/research/calculate-aave-v4-flow-limits.py b/examples/aave/research/calculate-aave-v4-flow-limits.py new file mode 100644 index 0000000..6f25377 --- /dev/null +++ b/examples/aave/research/calculate-aave-v4-flow-limits.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +import csv +import math +from pathlib import Path + +source = Path(__file__).with_name("aave-v4-flow-rate-observed-maxima.csv") +with source.open(newline="") as handle: + for row in csv.DictReader(handle): + limits = [ + math.ceil(float(row[column]) * 1.20) + for column in ("in_window_bps", "out_window_bps", "in_peak_rate_bps", "out_peak_rate_bps") + ] + print(f"{row['hub']} {row['asset']}: {limits}") diff --git a/examples/aave/src/AaveV3HorizonInterfaces.sol b/examples/aave/src/AaveV3HorizonInterfaces.sol index e4d6bf9..e436b0b 100644 --- a/examples/aave/src/AaveV3HorizonInterfaces.sol +++ b/examples/aave/src/AaveV3HorizonInterfaces.sol @@ -24,4 +24,5 @@ interface IAaveV3HorizonToken { /// @notice Horizon reserve-deficit view added to the Pool's v3.6 accounting surface. interface IAaveV3HorizonDeficitPool { function getReserveDeficit(address asset) external view returns (uint256); + function getReserveNormalizedIncome(address asset) external view returns (uint256); } diff --git a/examples/aave/src/AaveV3HorizonOracleAssertion.sol b/examples/aave/src/AaveV3HorizonOracleAssertion.sol index 4c63886..d017595 100644 --- a/examples/aave/src/AaveV3HorizonOracleAssertion.sol +++ b/examples/aave/src/AaveV3HorizonOracleAssertion.sol @@ -60,7 +60,7 @@ contract AaveV3HorizonOracleAssertion is AaveV3HorizonHelpers { for (uint256 i; i < assetPolicies_.length; ++i) { AssetPolicy memory policy = assetPolicies_[i]; require(policy.asset != address(0), "AaveV3Horizon: policy asset zero"); - require(policy.deviationBps <= BPS, "AaveV3Horizon: bad asset tolerance"); + require(policy.deviationBps < BPS, "AaveV3Horizon: bad asset tolerance"); for (uint256 j; j < i; ++j) { require(assetPolicies_[j].asset != policy.asset, "AaveV3Horizon: duplicate asset policy"); diff --git a/examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol b/examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol index 2b58b21..3e7d330 100644 --- a/examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol +++ b/examples/aave/src/AaveV3HorizonReserveBackingAssertion.sol @@ -31,13 +31,10 @@ contract AaveV3HorizonReserveBackingAssertion is AaveV3HorizonHelpers { RESERVE_ASSETS = reserveAssets_; } - /// @notice Registers a transaction-end backing check for configured Horizon reserve assets. - /// @dev The trigger intentionally runs after the whole transaction, including direct reserve - /// token movements outside the Pool call surface. That transaction envelope is not a place - /// where Horizon can add a Pool-level require. - function triggers() external view override { - registerTxEndTrigger(this.assertReserveBacking.selector); - } + /// @notice Quarantined until token-only transactions can select the Pool adopter's assertion. + /// @dev A Pool-adopter TxEnd trigger is not dispatched when an external token mutates aToken + /// custody without calling the Pool. `assertReserveBacking` remains available for research. + function triggers() external view virtual override {} /// @notice Checks all configured reserves remain backed at transaction end. /// @dev For each reserve, compares aToken supply plus the indexed treasury accrual with @@ -85,7 +82,10 @@ contract AaveV3HorizonReserveBackingAssertion is AaveV3HorizonHelpers { uint256 deficit = _readUintAt(POOL, abi.encodeCall(IAaveV3HorizonDeficitPool.getReserveDeficit, (asset)), fork); backing.aTokenSupply = _totalSupplyAt(reserveData.aTokenAddress, fork); - backing.accruedTreasuryLiability = _rayMul(reserveData.accruedToTreasury, reserveData.liquidityIndex); + uint256 normalizedIncome = _readUintAt( + POOL, abi.encodeCall(IAaveV3HorizonDeficitPool.getReserveNormalizedIncome, (asset)), fork + ); + backing.accruedTreasuryLiability = _rayMul(reserveData.accruedToTreasury, normalizedIncome); backing.backingClaims = availableLiquidity + stableDebt + variableDebt + reserveData.unbacked + deficit; } @@ -95,7 +95,8 @@ contract AaveV3HorizonReserveBackingAssertion is AaveV3HorizonHelpers { } /// @dev Aave stores `accruedToTreasury` in scaled aToken units. Mirror WadRayMath.rayMul's - /// half-up conversion so the liability is compared with token-denominated balances. + /// half-up conversion using current normalized income rather than the possibly stale + /// stored liquidity index. function _rayMul(uint256 scaledAmount, uint256 liquidityIndex) internal pure returns (uint256) { return (scaledAmount * liquidityIndex + HALF_RAY) / RAY; } diff --git a/examples/aave/src/AaveV4HubFlowRateCircuitBreaker.sol b/examples/aave/src/AaveV4HubFlowRateCircuitBreaker.sol index abaac83..8fe93f4 100644 --- a/examples/aave/src/AaveV4HubFlowRateCircuitBreaker.sol +++ b/examples/aave/src/AaveV4HubFlowRateCircuitBreaker.sol @@ -71,8 +71,9 @@ abstract contract AaveV4HubFlowRateCircuitBreaker is Assertion { } function _watchAsset(address token) internal view { - watchCumulativeInflow(token, DISPATCH_THRESHOLD_BPS, FLOW_WINDOW, this.assertInflowWithinRateLimits.selector); - watchCumulativeOutflow(token, DISPATCH_THRESHOLD_BPS, FLOW_WINDOW, this.assertOutflowWithinRateLimits.selector); + token; + // Quarantined: net cumulative dispatch can miss a directional reversal while prior + // opposite flow keeps the window net-positive or net-negative. } function _inflowTrips(address token, uint256 currentBps, uint256 peakRateBps) internal pure returns (bool) { @@ -119,7 +120,7 @@ contract AaveV4EthereumCoreHubFlowRateCircuitBreaker is AaveV4HubFlowRateCircuit } if (token == USDG) { return FlowLimits({ - inflowWindowBps: 5_196, outflowWindowBps: 6_438, inflowPeakRateBps: 527, outflowPeakRateBps: 154 + inflowWindowBps: 5_196, outflowWindowBps: 5_367, inflowPeakRateBps: 527, outflowPeakRateBps: 154 }); } if (token == WSTETH) { @@ -154,7 +155,7 @@ contract AaveV4EthereumPrimeHubFlowRateCircuitBreaker is AaveV4HubFlowRateCircui function _flowLimits(address token) internal pure override returns (FlowLimits memory limits) { if (token == WBTC) { return FlowLimits({ - inflowWindowBps: 2_102, outflowWindowBps: 2_367, inflowPeakRateBps: 149, outflowPeakRateBps: 178 + inflowWindowBps: 2_102, outflowWindowBps: 2_432, inflowPeakRateBps: 149, outflowPeakRateBps: 178 }); } if (token == WSTETH) { diff --git a/examples/aave/test/AaveV3AdversarialResearch.t.sol b/examples/aave/test/AaveV3AdversarialResearch.t.sol index 087fcdb..5ce2276 100644 --- a/examples/aave/test/AaveV3AdversarialResearch.t.sol +++ b/examples/aave/test/AaveV3AdversarialResearch.t.sol @@ -143,6 +143,7 @@ contract ResearchPool is IAaveV3LikePool { mapping(address => AaveV3LikeTypes.UserConfigurationMap) internal userConfig; mapping(address => AccountData) internal accounts; mapping(address => uint256) internal deficits; + mapping(address => uint256) internal normalizedIncome; constructor(address provider_) { ADDRESSES_PROVIDER = provider_; @@ -170,6 +171,7 @@ contract ResearchPool is IAaveV3LikePool { unbacked: 0, isolationModeTotalDebt: 0 }); + normalizedIncome[asset] = 1e27; } function setAccruedToTreasury(address asset, uint128 scaledAmount) external { @@ -188,6 +190,14 @@ contract ResearchPool is IAaveV3LikePool { return deficits[asset]; } + function setReserveNormalizedIncome(address asset, uint256 index) external { + normalizedIncome[asset] = index; + } + + function getReserveNormalizedIncome(address asset) external view returns (uint256) { + return normalizedIncome[asset]; + } + function setUserConfig(address user, uint256 data) external { userConfig[user].data = data; } @@ -415,6 +425,17 @@ contract ResearchPool is IAaveV3LikePool { } } + /// @dev Test-only harness arms the quarantined backing logic so its accounting remains covered. + contract ArmedReserveBackingAssertion is AaveV3HorizonReserveBackingAssertion { + constructor(address pool_, address[] memory assets_, uint256 deficit_) + AaveV3HorizonReserveBackingAssertion(pool_, assets_, deficit_) + {} + + function triggers() external view override { + registerTxEndTrigger(this.assertReserveBacking.selector); + } + } + contract AaveV3AdversarialResearchTest is Test, CredibleTest { uint256 internal constant MAX_RESERVES = 8; uint256 internal constant ORACLE_TOLERANCE_BPS = 100; @@ -489,6 +510,16 @@ contract ResearchPool is IAaveV3LikePool { pool.borrow(address(debtAsset), 1, 2, 0, alice); } + function testBackingUsesCurrentNormalizedIncomeWhenStoredIndexIsStale() public { + collateralAsset.mint(address(collateralAToken), 15 ether); + pool.setAccruedToTreasury(address(collateralAsset), uint128(10 ether)); + pool.setReserveNormalizedIncome(address(collateralAsset), 2e27); + + _armBacking(); + vm.expectRevert(bytes("AaveV3Horizon: reserve backing deficit")); + pool.borrow(address(debtAsset), 1, 2, 0, alice); + } + function testBackingDeficitHasCorrectPositiveSign() public { collateralAsset.seize(address(collateralAToken), recipient, 10 ether); pool.setReserveDeficit(address(collateralAsset), 10 ether); @@ -586,7 +617,7 @@ contract ResearchPool is IAaveV3LikePool { function _armBacking() internal { bytes memory createData = abi.encodePacked( - type(AaveV3HorizonReserveBackingAssertion).creationCode, abi.encode(address(pool), _assets(), 0) + type(ArmedReserveBackingAssertion).creationCode, abi.encode(address(pool), _assets(), 0) ); cl.assertion(address(pool), createData, AaveV3HorizonReserveBackingAssertion.assertReserveBacking.selector); } diff --git a/examples/aave/test/AaveV3HorizonOracleAssertion.t.sol b/examples/aave/test/AaveV3HorizonOracleAssertion.t.sol index c1777e0..7646e2f 100644 --- a/examples/aave/test/AaveV3HorizonOracleAssertion.t.sol +++ b/examples/aave/test/AaveV3HorizonOracleAssertion.t.sol @@ -404,6 +404,15 @@ contract AaveV3HorizonOracleAssertionTest is Test, CredibleTest { pool.borrow(asset0, 1, 2, 0, address(this)); } + function testFullRangeToleranceIsRejected() public { + AaveV3HorizonOracleAssertion.AssetPolicy[] memory policies = + new AaveV3HorizonOracleAssertion.AssetPolicy[](1); + policies[0] = AaveV3HorizonOracleAssertion.AssetPolicy({asset: asset0, deviationBps: 10_000}); + + vm.expectRevert(bytes("AaveV3Horizon: bad asset tolerance")); + new AaveV3HorizonOracleAssertion(address(pool), address(provider), MAX_TRACE_CALLS, policies); + } + function _arm(uint256 maxTraceCalls) internal { AaveV3HorizonOracleAssertion.AssetPolicy[] memory policies = new AaveV3HorizonOracleAssertion.AssetPolicy[](2); policies[0] = AaveV3HorizonOracleAssertion.AssetPolicy({asset: asset0, deviationBps: DEVIATION_BPS}); diff --git a/examples/aave/test/AaveV4HubFlowRateCircuitBreaker.t.sol b/examples/aave/test/AaveV4HubFlowRateCircuitBreaker.t.sol index 5d3a10f..feec1ee 100644 --- a/examples/aave/test/AaveV4HubFlowRateCircuitBreaker.t.sol +++ b/examples/aave/test/AaveV4HubFlowRateCircuitBreaker.t.sol @@ -120,7 +120,7 @@ contract AaveV4HubFlowRateCircuitBreakerTest is Test, CredibleTest { } function testCoreUsdgTripsOnWindowOutflow() public view { - assertTrue(core.outflowTrips(core.USDG(), 6_439, 154)); + assertTrue(core.outflowTrips(core.USDG(), 5_368, 154)); } function testCoreWstethTripsOnPeakOutflowRate() public view { @@ -128,7 +128,7 @@ contract AaveV4HubFlowRateCircuitBreakerTest is Test, CredibleTest { } function testPrimeWbtcAllowsExactOutflowLimits() public view { - assertFalse(prime.outflowTrips(prime.WBTC(), 2_367, 178)); + assertFalse(prime.outflowTrips(prime.WBTC(), 2_432, 178)); } function testPrimeWstethTripsOnWindowInflow() public view {