diff --git a/certora/basic/conf/NEW-pool-no-summarizations.conf b/certora/basic/conf/NEW-pool-no-summarizations.conf index 3c158029..65043ace 100644 --- a/certora/basic/conf/NEW-pool-no-summarizations.conf +++ b/certora/basic/conf/NEW-pool-no-summarizations.conf @@ -3,13 +3,13 @@ "certora/basic/harness/ATokenHarness.sol", "certora/basic/harness/PoolHarness.sol", "certora/basic/harness/SimpleERC20.sol", - "src/contracts/instances/VariableDebtTokenInstance.sol", - "src/contracts/helpers/AaveProtocolDataProvider.sol", - "src/contracts/misc/DefaultReserveInterestRateStrategyV2.sol", - "src/contracts/protocol/configuration/ACLManager.sol", - "src/contracts/misc/aave-upgradeability/InitializableImmutableAdminUpgradeabilityProxy.sol", - "src/contracts/misc/PriceOracleSentinel.sol", - "src/contracts/protocol/configuration/PoolAddressesProvider.sol", + "certora/basic/munged/contracts/instances/VariableDebtTokenInstance.sol", + "certora/basic/munged/contracts/helpers/AaveProtocolDataProvider.sol", + "certora/basic/munged/contracts/misc/DefaultReserveInterestRateStrategyV2.sol", + "certora/basic/munged/contracts/protocol/configuration/ACLManager.sol", + "certora/basic/munged/contracts/misc/aave-upgradeability/InitializableImmutableAdminUpgradeabilityProxy.sol", + "certora/basic/munged/contracts/misc/PriceOracleSentinel.sol", + "certora/basic/munged/contracts/protocol/configuration/PoolAddressesProvider.sol", ], "link": [ "ATokenHarness:POOL=PoolHarness", diff --git a/certora/basic/conf/NEW-pool-simple-properties.conf b/certora/basic/conf/NEW-pool-simple-properties.conf index e3492cfa..3ce69459 100644 --- a/certora/basic/conf/NEW-pool-simple-properties.conf +++ b/certora/basic/conf/NEW-pool-simple-properties.conf @@ -3,11 +3,11 @@ "certora/basic/harness/ATokenHarness.sol", "certora/basic/harness/PoolHarness.sol", "certora/basic/harness/SimpleERC20.sol", - "src/contracts/instances/VariableDebtTokenInstance.sol", - "src/contracts/helpers/AaveProtocolDataProvider.sol", - "src/contracts/misc/DefaultReserveInterestRateStrategyV2.sol", - "src/contracts/protocol/libraries/types/DataTypes.sol", - "src/contracts/protocol/configuration/PoolAddressesProvider.sol", + "certora/basic/munged/contracts/instances/VariableDebtTokenInstance.sol", + "certora/basic/munged/contracts/helpers/AaveProtocolDataProvider.sol", + "certora/basic/munged/contracts/misc/DefaultReserveInterestRateStrategyV2.sol", + "certora/basic/munged/contracts/protocol/libraries/types/DataTypes.sol", + "certora/basic/munged/contracts/protocol/configuration/PoolAddressesProvider.sol", ], "link": [ "ATokenHarness:POOL=PoolHarness", diff --git a/certora/basic/scripts/run-all.sh b/certora/basic/scripts/run-all.sh index 6d49ffef..d0a36377 100644 --- a/certora/basic/scripts/run-all.sh +++ b/certora/basic/scripts/run-all.sh @@ -33,9 +33,9 @@ certoraRun $CMN certora/basic/conf/stableRemoved.conf \ --msg "6: Stable fields are un-touched" echo -echo "******** Running: 6 EModeConfiguration ***************" +echo "******** Running: 7 EModeConfiguration ***************" certoraRun $CMN certora/basic/conf/EModeConfiguration.conf \ - --msg "6: EModeConfiguration" + --msg "7: EModeConfiguration" echo diff --git a/certora/basic/specs/EModeConfiguration.spec b/certora/basic/specs/EModeConfiguration.spec index c3823476..2e5f5960 100644 --- a/certora/basic/specs/EModeConfiguration.spec +++ b/certora/basic/specs/EModeConfiguration.spec @@ -56,5 +56,3 @@ rule independencyOfBorrowableSetters(uint256 reserveIndex, bool borrowable) { assert (reserveIndex != reserveIndex_other => before == after); } - - diff --git a/certora/basic/specs/NEW-pool-base.spec b/certora/basic/specs/NEW-pool-base.spec index cc91ac2e..72d9efa5 100644 --- a/certora/basic/specs/NEW-pool-base.spec +++ b/certora/basic/specs/NEW-pool-base.spec @@ -99,7 +99,7 @@ function calculateCompoundedInterestSummary(uint256 rate, uint40 t0, uint256 t1) } function isActiveReserve(env e, address asset) returns bool { - DataTypes.ReserveData data = getReserveDataExtended(e, asset); + DataTypes.ReserveDataLegacy data = getReserveData(e, asset); DataTypes.ReserveConfigurationMap configuration = data.configuration; bool isActive = getActive(e, configuration); @@ -107,7 +107,7 @@ function isActiveReserve(env e, address asset) returns bool { } function isFrozenReserve(env e, address asset) returns bool { - DataTypes.ReserveData data = getReserveDataExtended(e, asset); + DataTypes.ReserveDataLegacy data = getReserveData(e, asset); DataTypes.ReserveConfigurationMap configuration = data.configuration; bool isFrozen = getFrozen(e, configuration); @@ -115,7 +115,7 @@ function isFrozenReserve(env e, address asset) returns bool { } function isEnabledForBorrow(env e, address asset) returns bool { - DataTypes.ReserveData data = getReserveDataExtended(e, asset); + DataTypes.ReserveDataLegacy data = getReserveData(e, asset); DataTypes.ReserveConfigurationMap configuration = data.configuration; bool isBorrowEnabled = getBorrowingEnabled(e, configuration); @@ -123,12 +123,12 @@ function isEnabledForBorrow(env e, address asset) returns bool { } function getCurrentLiquidityRate(env e, address asset) returns mathint { - DataTypes.ReserveData data = getReserveDataExtended(e, asset); + DataTypes.ReserveDataLegacy data = getReserveData(e, asset); return data.currentLiquidityRate; } function getLiquidityIndex(env e, address asset) returns mathint { - DataTypes.ReserveData data = getReserveDataExtended(e, asset); + DataTypes.ReserveDataLegacy data = getReserveData(e, asset); return data.liquidityIndex; } diff --git a/certora/basic/specs/NEW-pool-no-summarizations.spec b/certora/basic/specs/NEW-pool-no-summarizations.spec index 826b0fb7..b5d9e9c6 100644 --- a/certora/basic/specs/NEW-pool-no-summarizations.spec +++ b/certora/basic/specs/NEW-pool-no-summarizations.spec @@ -81,7 +81,7 @@ rule liquidityIndexNonDecresingFor_cumulateToLiquidityIndex() { function get_AToken_of_asset(env e, address asset) returns address { - DataTypes.ReserveData data = getReserveDataExtended(e, asset); + DataTypes.ReserveDataLegacy data = getReserveData(e, asset); return data.aTokenAddress; } diff --git a/certora/basic/specs/stableRemoved.spec b/certora/basic/specs/stableRemoved.spec index bbd7f3ee..0ebd08d9 100644 --- a/certora/basic/specs/stableRemoved.spec +++ b/certora/basic/specs/stableRemoved.spec @@ -4,7 +4,7 @@ import "aux/aToken.spec"; //import "AddressProvider.spec"; methods { - function getReserveDataExtended(address) external returns (DataTypes.ReserveData memory) envfree; + // function getReserveDataExtended(address) external returns (DataTypes.ReserveData memory) envfree; function getReserveData(address) external returns (DataTypes.ReserveDataLegacy memory) envfree; function ValidationLogic.validateLiquidationCall( @@ -21,22 +21,22 @@ methods { DataTypes.CalculateUserAccountDataParams memory params ) internal returns (uint256, uint256, uint256, uint256, uint256, bool) => NONDET; - function LiquidationLogic._calculateDebt( + /* function LiquidationLogic._calculateDebt( DataTypes.ReserveCache memory debtReserveCache, DataTypes.ExecuteLiquidationCallParams memory params, uint256 healthFactor - ) internal returns (uint256, uint256) => NONDET; + ) internal returns (uint256, uint256) => NONDET;*/ function LiquidationLogic._calculateAvailableCollateralToLiquidate( - DataTypes.ReserveData storage collateralReserve, - DataTypes.ReserveCache memory debtReserveCache, - address collateralAsset, - address debtAsset, - uint256 debtToCover, - uint256 userCollateralBalance, - uint256 liquidationBonus, - address // IPriceOracleGetter - ) internal returns (uint256,uint256,uint256) => NONDET; + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal returns (uint256, uint256, uint256, uint256) => NONDET; } @@ -55,9 +55,9 @@ function init_state() { } -hook Sstore _reserves[KEY address a].__deprecatedStableBorrowRate uint128 rate (uint128 old_rate) { - assert false, "writing the field __deprecatedStableBorrowRate"; -} +//hook Sstore _reserves[KEY address a].__deprecatedStableBorrowRate uint128 rate (uint128 old_rate) { +// assert false, "writing the field __deprecatedStableBorrowRate"; +//} hook Sstore _reserves[KEY address a].__deprecatedStableDebtTokenAddress address stable (address old_stable) { assert false, "writing the field __deprecatedStableDebtTokenAddress"; @@ -83,27 +83,27 @@ rule stableFieldsUntouched(method f, env e, address _asset) && aTokenToUnderlying[currentContract._reserves[asset].variableDebtTokenAddress]==asset; - DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + //DataTypes.ReserveData reserve = getReserveDataExtended(_asset); DataTypes.ReserveDataLegacy reserveLegasy = getReserveData(_asset); - uint128 __deprecatedStableBorrowRate_BEFORE = reserve.__deprecatedStableBorrowRate; - address __deprecatedStableDebtTokenAddress_BEFORE = reserve.__deprecatedStableDebtTokenAddress; + //uint128 __deprecatedStableBorrowRate_BEFORE = reserve.__deprecatedStableBorrowRate; + //address __deprecatedStableDebtTokenAddress_BEFORE = reserve.__deprecatedStableDebtTokenAddress; uint128 currentStableBorrowRate_BEFORE = reserveLegasy.currentStableBorrowRate; // address stableDebtTokenAddress_BEFORE = reserveLegasy.stableDebtTokenAddress; calldataarg args; f(e,args); - DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + // DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); DataTypes.ReserveDataLegacy reserveLegasy2 = getReserveData(_asset); - uint128 __deprecatedStableBorrowRate_AFTER = reserve2.__deprecatedStableBorrowRate; - address __deprecatedStableDebtTokenAddress_AFTER = reserve2.__deprecatedStableDebtTokenAddress; + //uint128 __deprecatedStableBorrowRate_AFTER = reserve2.__deprecatedStableBorrowRate; + //address __deprecatedStableDebtTokenAddress_AFTER = reserve2.__deprecatedStableDebtTokenAddress; uint128 currentStableBorrowRate_AFTER = reserveLegasy2.currentStableBorrowRate; address stableDebtTokenAddress_AFTER = reserveLegasy2.stableDebtTokenAddress; - assert __deprecatedStableBorrowRate_BEFORE == __deprecatedStableBorrowRate_AFTER; - assert __deprecatedStableDebtTokenAddress_BEFORE == __deprecatedStableDebtTokenAddress_AFTER; + // assert __deprecatedStableBorrowRate_BEFORE == __deprecatedStableBorrowRate_AFTER; + //assert __deprecatedStableDebtTokenAddress_BEFORE == __deprecatedStableDebtTokenAddress_AFTER; assert currentStableBorrowRate_BEFORE == currentStableBorrowRate_AFTER; // assert stableDebtTokenAddress_BEFORE == stableDebtTokenAddress_AFTER; diff --git a/certora/experimental/BorrowLogic.sol b/certora/experimental/BorrowLogic.sol new file mode 100644 index 00000000..0eb38022 --- /dev/null +++ b/certora/experimental/BorrowLogic.sol @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {GPv2SafeERC20} from '../../../dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +import {SafeCast} from '../../../dependencies/openzeppelin/contracts/SafeCast.sol'; +import {IERC20} from '../../../dependencies/openzeppelin/contracts/IERC20.sol'; +import {IVariableDebtToken} from '../../../interfaces/IVariableDebtToken.sol'; +import {IAToken} from '../../../interfaces/IAToken.sol'; +import {UserConfiguration} from '../configuration/UserConfiguration.sol'; +import {ReserveConfiguration} from '../configuration/ReserveConfiguration.sol'; +import {DataTypes} from '../types/DataTypes.sol'; +import {ValidationLogic} from './ValidationLogic.sol'; +import {ReserveLogic} from './ReserveLogic.sol'; +import {IsolationModeLogic} from './IsolationModeLogic.sol'; + +/** + * @title BorrowLogic library + * @author Aave + * @notice Implements the base logic for all the actions related to borrowing + */ +library BorrowLogic { + using ReserveLogic for DataTypes.ReserveCache; + using ReserveLogic for DataTypes.ReserveData; + using GPv2SafeERC20 for IERC20; + using UserConfiguration for DataTypes.UserConfigurationMap; + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + using SafeCast for uint256; + + // See `IPool` for descriptions + event Borrow( + address indexed reserve, + address user, + address indexed onBehalfOf, + uint256 amount, + DataTypes.InterestRateMode interestRateMode, + uint256 borrowRate, + uint16 indexed referralCode + ); + event Repay( + address indexed reserve, + address indexed user, + address indexed repayer, + uint256 amount, + bool useATokens + ); + event IsolationModeTotalDebtUpdated(address indexed asset, uint256 totalDebt); + event ReserveUsedAsCollateralDisabled(address indexed reserve, address indexed user); + + /** + * @notice Implements the borrow feature. Borrowing allows users that provided collateral to draw liquidity from the + * Aave protocol proportionally to their collateralization power. For isolated positions, it also increases the + * isolated debt. + * @dev Emits the `Borrow()` event + * @param reservesData The state of all the reserves + * @param reservesList The addresses of all the active reserves + * @param eModeCategories The configuration of all the efficiency mode categories + * @param userConfig The user configuration mapping that tracks the supplied/borrowed assets + * @param params The additional parameters needed to execute the borrow function + */ + function executeBorrow( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteBorrowParams memory params + ) external { + DataTypes.ReserveData storage reserve = reservesData[params.asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + reserve.updateState(reserveCache); + + ( + bool isolationModeActive, + address isolationModeCollateralAddress, + uint256 isolationModeDebtCeiling + ) = userConfig.getIsolationModeState(reservesData, reservesList); + + ValidationLogic.validateBorrow( + reservesData, + reservesList, + eModeCategories, + DataTypes.ValidateBorrowParams({ + reserveCache: reserveCache, + userConfig: userConfig, + asset: params.asset, + userAddress: params.onBehalfOf, + amount: params.amount, + interestRateMode: params.interestRateMode, + reservesCount: params.reservesCount, + oracle: params.oracle, + userEModeCategory: params.userEModeCategory, + priceOracleSentinel: params.priceOracleSentinel, + isolationModeActive: isolationModeActive, + isolationModeCollateralAddress: isolationModeCollateralAddress, + isolationModeDebtCeiling: isolationModeDebtCeiling + }) + ); + + bool isFirstBorrowing = false; + + (isFirstBorrowing, reserveCache.nextScaledVariableDebt) = IVariableDebtToken( + reserveCache.variableDebtTokenAddress + ).mint(params.user, params.onBehalfOf, params.amount, reserveCache.nextVariableBorrowIndex); + + if (isFirstBorrowing) { + userConfig.setBorrowing(reserve.id, true); + } + + if (isolationModeActive) { + uint256 nextIsolationModeTotalDebt = reservesData[isolationModeCollateralAddress] + .isolationModeTotalDebt += (params.amount / + 10 ** + (reserveCache.reserveConfiguration.getDecimals() - + ReserveConfiguration.DEBT_CEILING_DECIMALS)).toUint128(); + emit IsolationModeTotalDebtUpdated( + isolationModeCollateralAddress, + nextIsolationModeTotalDebt + ); + } + + reserve.updateInterestRatesAndVirtualBalance( + reserveCache, + params.asset, + 0, + params.releaseUnderlying ? params.amount : 0 + ); + + if (params.releaseUnderlying) { + IAToken(reserveCache.aTokenAddress).transferUnderlyingTo(params.user, params.amount); + } + + emit Borrow( + params.asset, + params.user, + params.onBehalfOf, + params.amount, + DataTypes.InterestRateMode.VARIABLE, + reserve.currentVariableBorrowRate, + params.referralCode + ); + } + + /** + * @notice Implements the repay feature. Repaying transfers the underlying back to the aToken and clears the + * equivalent amount of debt for the user by burning the corresponding debt token. For isolated positions, it also + * reduces the isolated debt. + * @dev Emits the `Repay()` event + * @param reservesData The state of all the reserves + * @param reservesList The addresses of all the active reserves + * @param userConfig The user configuration mapping that tracks the supplied/borrowed assets + * @param params The additional parameters needed to execute the repay function + * @return The actual amount being repaid + */ + function executeRepay( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteRepayParams memory params + ) external returns (uint256) { + DataTypes.ReserveData storage reserve = reservesData[params.asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + reserve.updateState(reserveCache); + repay_hook_1(reserveCache); + uint256 variableDebt = IERC20(reserveCache.variableDebtTokenAddress).balanceOf( + params.onBehalfOf + ); + + ValidationLogic.validateRepay( + reserveCache, + params.amount, + params.interestRateMode, + params.onBehalfOf, + variableDebt + ); + + uint256 paybackAmount = variableDebt; + + // Allows a user to repay with aTokens without leaving dust from interest. + if (params.useATokens && params.amount == type(uint256).max) { + params.amount = IAToken(reserveCache.aTokenAddress).balanceOf(msg.sender); + } + + if (params.amount < paybackAmount) { + paybackAmount = params.amount; + } + repay_hook_2(reserveCache); + reserveCache.nextScaledVariableDebt = IVariableDebtToken(reserveCache.variableDebtTokenAddress) + .burn(params.onBehalfOf, paybackAmount, reserveCache.nextVariableBorrowIndex); + repay_hook_3(reserveCache, paybackAmount); + reserve.updateInterestRatesAndVirtualBalance( + reserveCache, + params.asset, + params.useATokens ? 0 : paybackAmount, + 0 + ); + + if (variableDebt - paybackAmount == 0) { + userConfig.setBorrowing(reserve.id, false); + } + + IsolationModeLogic.updateIsolatedDebtIfIsolated( + reservesData, + reservesList, + userConfig, + reserveCache, + paybackAmount + ); + + if (params.useATokens) { + IAToken(reserveCache.aTokenAddress).burn( + msg.sender, + reserveCache.aTokenAddress, + paybackAmount, + reserveCache.nextLiquidityIndex + ); + repay_hook_4(reserveCache); + bool isCollateral = userConfig.isUsingAsCollateral(reserve.id); + // in case of aToken repayment the msg.sender must always repay on behalf of itself + if (isCollateral && IAToken(reserveCache.aTokenAddress).scaledBalanceOf(msg.sender) == 0) { + userConfig.setUsingAsCollateral(reserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.asset, msg.sender); + } + } else { + IERC20(params.asset).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, paybackAmount); + IAToken(reserveCache.aTokenAddress).handleRepayment( + msg.sender, + params.onBehalfOf, + paybackAmount + ); + } + + emit Repay(params.asset, params.onBehalfOf, msg.sender, paybackAmount, params.useATokens); + + return paybackAmount; + } + function repay_hook_1(DataTypes.ReserveCache memory reserveCache) internal {} + function repay_hook_2(DataTypes.ReserveCache memory reserveCache) internal {} + function repay_hook_3(DataTypes.ReserveCache memory reserveCache, uint256 paybackAmount) internal {} + function repay_hook_4(DataTypes.ReserveCache memory reserveCache) internal {} +} diff --git a/certora/experimental/LiquidationLogic.sol b/certora/experimental/LiquidationLogic.sol new file mode 100644 index 00000000..adb22b62 --- /dev/null +++ b/certora/experimental/LiquidationLogic.sol @@ -0,0 +1,607 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IERC20} from '../../../dependencies/openzeppelin/contracts//IERC20.sol'; +import {GPv2SafeERC20} from '../../../dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +import {PercentageMath} from '../../libraries/math/PercentageMath.sol'; +import {WadRayMath} from '../../libraries/math/WadRayMath.sol'; +import {DataTypes} from '../../libraries/types/DataTypes.sol'; +import {ReserveLogic} from './ReserveLogic.sol'; +import {ValidationLogic} from './ValidationLogic.sol'; +import {GenericLogic} from './GenericLogic.sol'; +import {IsolationModeLogic} from './IsolationModeLogic.sol'; +import {EModeLogic} from './EModeLogic.sol'; +import {UserConfiguration} from '../../libraries/configuration/UserConfiguration.sol'; +import {ReserveConfiguration} from '../../libraries/configuration/ReserveConfiguration.sol'; +import {EModeConfiguration} from '../../libraries/configuration/EModeConfiguration.sol'; +import {IAToken} from '../../../interfaces/IAToken.sol'; +import {IVariableDebtToken} from '../../../interfaces/IVariableDebtToken.sol'; +import {IPriceOracleGetter} from '../../../interfaces/IPriceOracleGetter.sol'; +import {SafeCast} from '../../../dependencies/openzeppelin/contracts/SafeCast.sol'; +import {Errors} from '../helpers/Errors.sol'; + +/** + * @title LiquidationLogic library + * @author Aave + * @notice Implements actions involving management of collateral in the protocol, the main one being the liquidations + */ +library LiquidationLogic { + using WadRayMath for uint256; + using PercentageMath for uint256; + using ReserveLogic for DataTypes.ReserveCache; + using ReserveLogic for DataTypes.ReserveData; + using UserConfiguration for DataTypes.UserConfigurationMap; + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + using GPv2SafeERC20 for IERC20; + using SafeCast for uint256; + + // See `IPool` for descriptions + event ReserveUsedAsCollateralEnabled(address indexed reserve, address indexed user); + event ReserveUsedAsCollateralDisabled(address indexed reserve, address indexed user); + event DeficitCreated(address indexed user, address indexed debtAsset, uint256 amount); + event LiquidationCall( + address indexed collateralAsset, + address indexed debtAsset, + address indexed user, + uint256 debtToCover, + uint256 liquidatedCollateralAmount, + address liquidator, + bool receiveAToken + ); + + /** + * @dev Default percentage of borrower's debt to be repaid in a liquidation. + * @dev Percentage applied when the users health factor is above `CLOSE_FACTOR_HF_THRESHOLD` + * Expressed in bps, a value of 0.5e4 results in 50.00% + */ + uint256 internal constant DEFAULT_LIQUIDATION_CLOSE_FACTOR = 0.5e4; + + /** + * @dev This constant represents the upper bound on the health factor, below(inclusive) which the full amount of debt becomes liquidatable. + * A value of 0.95e18 results in 0.95 + */ + uint256 public constant CLOSE_FACTOR_HF_THRESHOLD = 0.95e18; + + /** + * @dev This constant represents a base value threshold. + * If the total collateral or debt on a position is below this threshold, the close factor is raised to 100%. + * @notice The default value assumes that the basePrice is usd denominated by 8 decimals and needs to be adjusted in a non USD-denominated pool. + */ + uint256 public constant MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD = 2000e8; + + /** + * @dev This constant represents the minimum amount of assets in base currency that need to be leftover after a liquidation, if not clearing a position completely. + * This parameter is inferred from MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD as the logic is dependent. + * Assuming a MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD of `n` a liquidation of `n+1` might result in `n/2` leftover which is assumed to be still economically liquidatable. + * This mechanic was introduced to ensure liquidators don't optimize gas by leaving some wei on the liquidation. + */ + uint256 public constant MIN_LEFTOVER_BASE = MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD / 2; + + struct LiquidationCallLocalVars { + uint256 userCollateralBalance; + uint256 userReserveDebt; + uint256 actualDebtToLiquidate; + uint256 actualCollateralToLiquidate; + uint256 liquidationBonus; + uint256 healthFactor; + uint256 liquidationProtocolFeeAmount; + uint256 totalCollateralInBaseCurrency; + uint256 totalDebtInBaseCurrency; + uint256 collateralToLiquidateInBaseCurrency; + uint256 userReserveDebtInBaseCurrency; + uint256 userReserveCollateralInBaseCurrency; + uint256 collateralAssetPrice; + uint256 debtAssetPrice; + uint256 collateralAssetUnit; + uint256 debtAssetUnit; + IAToken collateralAToken; + DataTypes.ReserveCache debtReserveCache; + } + + /** + * @notice Function to liquidate a position if its Health Factor drops below 1. The caller (liquidator) + * covers `debtToCover` amount of debt of the user getting liquidated, and receives + * a proportional amount of the `collateralAsset` plus a bonus to cover market risk + * @dev Emits the `LiquidationCall()` event, and the `DeficitCreated()` event if the liquidation results in bad debt + * @param reservesData The state of all the reserves + * @param reservesList The addresses of all the active reserves + * @param usersConfig The users configuration mapping that track the supplied/borrowed assets + * @param eModeCategories The configuration of all the efficiency mode categories + * @param params The additional parameters needed to execute the liquidation function + */ + function executeLiquidationCall( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(address => DataTypes.UserConfigurationMap) storage usersConfig, + mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.ExecuteLiquidationCallParams memory params + ) external { + LiquidationCallLocalVars memory vars; + + DataTypes.ReserveData storage collateralReserve = reservesData[params.collateralAsset]; + DataTypes.ReserveData storage debtReserve = reservesData[params.debtAsset]; + DataTypes.UserConfigurationMap storage userConfig = usersConfig[params.user]; + vars.debtReserveCache = debtReserve.cache(); + debtReserve.updateState(vars.debtReserveCache); HOOK_liquidation_after_updateState_DBT(); + + ( + vars.totalCollateralInBaseCurrency, + vars.totalDebtInBaseCurrency, + , + , + vars.healthFactor, + + ) = GenericLogic.calculateUserAccountData( + reservesData, + reservesList, + eModeCategories, + DataTypes.CalculateUserAccountDataParams({ + userConfig: userConfig, + reservesCount: params.reservesCount, + user: params.user, + oracle: params.priceOracle, + userEModeCategory: params.userEModeCategory + }) + ); HOOK_liquidation_after_calculateDebt(vars.userTotalDebt); + + vars.collateralAToken = IAToken(collateralReserve.aTokenAddress); + vars.userCollateralBalance = vars.collateralAToken.balanceOf(params.user); + vars.userReserveDebt = IERC20(vars.debtReserveCache.variableDebtTokenAddress).balanceOf( + params.user + ); + + ValidationLogic.validateLiquidationCall( + userConfig, + collateralReserve, + debtReserve, + DataTypes.ValidateLiquidationCallParams({ + debtReserveCache: vars.debtReserveCache, + totalDebt: vars.userReserveDebt, + healthFactor: vars.healthFactor, + priceOracleSentinel: params.priceOracleSentinel + }) + ); + + if ( + params.userEModeCategory != 0 && + EModeConfiguration.isReserveEnabledOnBitmap( + eModeCategories[params.userEModeCategory].collateralBitmap, + collateralReserve.id + ) + ) { + vars.liquidationBonus = eModeCategories[params.userEModeCategory].liquidationBonus; + } else { + vars.liquidationBonus = collateralReserve.configuration.getLiquidationBonus(); + } + vars.collateralAssetPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice( + params.collateralAsset + ); + vars.debtAssetPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice(params.debtAsset); + vars.collateralAssetUnit = 10 ** collateralReserve.configuration.getDecimals(); + vars.debtAssetUnit = 10 ** vars.debtReserveCache.reserveConfiguration.getDecimals(); + + vars.userReserveDebtInBaseCurrency = + (vars.userReserveDebt * vars.debtAssetPrice) / + vars.debtAssetUnit; + + vars.userReserveCollateralInBaseCurrency = + (vars.userCollateralBalance * vars.collateralAssetPrice) / + vars.collateralAssetUnit; + + // by default whole debt in the reserve could be liquidated + uint256 maxLiquidatableDebt = vars.userReserveDebt; + // but if debt and collateral is above or equal MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD + // and health factor is above CLOSE_FACTOR_HF_THRESHOLD this amount may be adjusted + if ( + vars.userReserveCollateralInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + vars.userReserveDebtInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + vars.healthFactor > CLOSE_FACTOR_HF_THRESHOLD + ) { + uint256 totalDefaultLiquidatableDebtInBaseCurrency = vars.totalDebtInBaseCurrency.percentMul( + DEFAULT_LIQUIDATION_CLOSE_FACTOR + ); + + // if the debt is more then DEFAULT_LIQUIDATION_CLOSE_FACTOR % of the whole, + // then we CAN liquidate only up to DEFAULT_LIQUIDATION_CLOSE_FACTOR % + if (vars.userReserveDebtInBaseCurrency > totalDefaultLiquidatableDebtInBaseCurrency) { + maxLiquidatableDebt = + (totalDefaultLiquidatableDebtInBaseCurrency * vars.debtAssetUnit) / + vars.debtAssetPrice; + } + } + + vars.actualDebtToLiquidate = params.debtToCover > maxLiquidatableDebt + ? maxLiquidatableDebt + : params.debtToCover; + + ( + vars.actualCollateralToLiquidate, + vars.actualDebtToLiquidate, + vars.liquidationProtocolFeeAmount, + vars.collateralToLiquidateInBaseCurrency + ) = _calculateAvailableCollateralToLiquidate( + collateralReserve.configuration, + vars.collateralAssetPrice, + vars.collateralAssetUnit, + vars.debtAssetPrice, + vars.debtAssetUnit, + vars.actualDebtToLiquidate, + vars.userCollateralBalance, + vars.liquidationBonus + ); + + // to prevent accumulation of dust on the protocol, it is enforced that you either + // 1. liquidate all debt + // 2. liquidate all collateral + // 3. leave more than MIN_LEFTOVER_BASE of collateral & debt + if ( + vars.actualDebtToLiquidate < vars.userReserveDebt && + vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount < + vars.userCollateralBalance + ) { + bool isDebtMoreThanLeftoverThreshold = ((vars.userReserveDebt - vars.actualDebtToLiquidate) * + vars.debtAssetPrice) / + vars.debtAssetUnit >= + MIN_LEFTOVER_BASE; + + bool isCollateralMoreThanLeftoverThreshold = ((vars.userCollateralBalance - + vars.actualCollateralToLiquidate - + vars.liquidationProtocolFeeAmount) * vars.collateralAssetPrice) / + vars.collateralAssetUnit >= + MIN_LEFTOVER_BASE; + + require( + isDebtMoreThanLeftoverThreshold && isCollateralMoreThanLeftoverThreshold, + Errors.MUST_NOT_LEAVE_DUST + ); + } + + // If the collateral being liquidated is equal to the user balance, + // we set the currency as not being used as collateral anymore + if ( + vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount == + vars.userCollateralBalance + ) { + userConfig.setUsingAsCollateral(collateralReserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.collateralAsset, params.user); + } + + bool hasNoCollateralLeft = vars.totalCollateralInBaseCurrency == + vars.collateralToLiquidateInBaseCurrency; + _burnDebtTokens( + vars.debtReserveCache, + debtReserve, + userConfig, + params.user, + params.debtAsset, + vars.userReserveDebt, + vars.actualDebtToLiquidate, + hasNoCollateralLeft + ); + + // IsolationModeTotalDebt only discounts `actualDebtToLiquidate`, not the fully burned amount in case of deficit creation. + // This is by design as otherwise debt debt ceiling would render ineffective if a collateral asset faces bad debt events. + // The governance can decide the raise the ceiling to discount manifested deficit. + IsolationModeLogic.updateIsolatedDebtIfIsolated( + reservesData, + reservesList, + userConfig, + vars.debtReserveCache, + vars.actualDebtToLiquidate + ); + + if (params.receiveAToken) { + _liquidateATokens(reservesData, reservesList, usersConfig, collateralReserve, params, vars); + } else { + _burnCollateralATokens(collateralReserve, params, vars); HOOK_liquidation_after_updateState_COL(); + } + + // Transfer fee to treasury if it is non-zero + if (vars.liquidationProtocolFeeAmount != 0) { + uint256 liquidityIndex = collateralReserve.getNormalizedIncome(); + uint256 scaledDownLiquidationProtocolFee = vars.liquidationProtocolFeeAmount.rayDiv( + liquidityIndex + ); + uint256 scaledDownUserBalance = vars.collateralAToken.scaledBalanceOf(params.user); + // To avoid trying to send more aTokens than available on balance, due to 1 wei imprecision + if (scaledDownLiquidationProtocolFee > scaledDownUserBalance) { + vars.liquidationProtocolFeeAmount = scaledDownUserBalance.rayMul(liquidityIndex); + } + vars.collateralAToken.transferOnLiquidation( + params.user, + vars.collateralAToken.RESERVE_TREASURY_ADDRESS(), + vars.liquidationProtocolFeeAmount + ); + } + + // burn bad debt if necessary + // Each additional debt asset already adds around ~75k gas to the liquidation. + // To keep the liquidation gas under control, 0 usd collateral positions are not touched, as there is no immediate benefit in burning or transfering to treasury. + if (hasNoCollateralLeft && userConfig.isBorrowingAny()) { + _burnBadDebt(reservesData, reservesList, userConfig, params.reservesCount, params.user); + } + + // Transfers the debt asset being repaid to the aToken, where the liquidity is kept + IERC20(params.debtAsset).safeTransferFrom( + msg.sender, + vars.debtReserveCache.aTokenAddress, + vars.actualDebtToLiquidate + ); + + IAToken(vars.debtReserveCache.aTokenAddress).handleRepayment( + msg.sender, + params.user, + vars.actualDebtToLiquidate + ); + + emit LiquidationCall( + params.collateralAsset, + params.debtAsset, + params.user, + vars.actualDebtToLiquidate, + vars.actualCollateralToLiquidate, + msg.sender, + params.receiveAToken + ); + } + + /** + * @notice Burns the collateral aTokens and transfers the underlying to the liquidator. + * @dev The function also updates the state and the interest rate of the collateral reserve. + * @param collateralReserve The data of the collateral reserve + * @param params The additional parameters needed to execute the liquidation function + * @param vars The executeLiquidationCall() function local vars + */ + function _burnCollateralATokens( + DataTypes.ReserveData storage collateralReserve, + DataTypes.ExecuteLiquidationCallParams memory params, + LiquidationCallLocalVars memory vars + ) internal { + DataTypes.ReserveCache memory collateralReserveCache = collateralReserve.cache(); + collateralReserve.updateState(collateralReserveCache); + collateralReserve.updateInterestRatesAndVirtualBalance( + collateralReserveCache, + params.collateralAsset, + 0, + vars.actualCollateralToLiquidate + ); + + // Burn the equivalent amount of aToken, sending the underlying to the liquidator + vars.collateralAToken.burn( + params.user, + msg.sender, + vars.actualCollateralToLiquidate, + collateralReserveCache.nextLiquidityIndex + ); + } + + /** + * @notice Liquidates the user aTokens by transferring them to the liquidator. + * @dev The function also checks the state of the liquidator and activates the aToken as collateral + * as in standard transfers if the isolation mode constraints are respected. + * @param reservesData The state of all the reserves + * @param reservesList The addresses of all the active reserves + * @param usersConfig The users configuration mapping that track the supplied/borrowed assets + * @param collateralReserve The data of the collateral reserve + * @param params The additional parameters needed to execute the liquidation function + * @param vars The executeLiquidationCall() function local vars + */ + function _liquidateATokens( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(address => DataTypes.UserConfigurationMap) storage usersConfig, + DataTypes.ReserveData storage collateralReserve, + DataTypes.ExecuteLiquidationCallParams memory params, + LiquidationCallLocalVars memory vars + ) internal { + uint256 liquidatorPreviousATokenBalance = IERC20(vars.collateralAToken).balanceOf(msg.sender); + vars.collateralAToken.transferOnLiquidation( + params.user, + msg.sender, + vars.actualCollateralToLiquidate + ); + + if (liquidatorPreviousATokenBalance == 0) { + DataTypes.UserConfigurationMap storage liquidatorConfig = usersConfig[msg.sender]; + if ( + ValidationLogic.validateAutomaticUseAsCollateral( + reservesData, + reservesList, + liquidatorConfig, + collateralReserve.configuration, + collateralReserve.aTokenAddress + ) + ) { + liquidatorConfig.setUsingAsCollateral(collateralReserve.id, true); + emit ReserveUsedAsCollateralEnabled(params.collateralAsset, msg.sender); + } + } + } + + /** + * @notice Burns the debt tokens of the user up to the amount being repaid by the liquidator + * or the entire debt if the user is in a bad debt scenario. + * @dev The function alters the `debtReserveCache` state in `vars` to update the debt related data. + * @param debtReserveCache The cached debt reserve parameters + * @param debtReserve The storage pointer of the debt reserve parameters + * @param userConfig The pointer of the user configuration + * @param user The user address + * @param debtAsset The debt asset address + * @param actualDebtToLiquidate The actual debt to liquidate + * @param hasNoCollateralLeft The flag representing, will user will have no collateral left after liquidation + */ + function _burnDebtTokens( + DataTypes.ReserveCache memory debtReserveCache, + DataTypes.ReserveData storage debtReserve, + DataTypes.UserConfigurationMap storage userConfig, + address user, + address debtAsset, + uint256 userReserveDebt, + uint256 actualDebtToLiquidate, + bool hasNoCollateralLeft + ) internal { + // Prior v3.1, there were cases where, after liquidation, the `isBorrowing` flag was left on + // even after the user debt was fully repaid, so to avoid this function reverting in the `_burnScaled` + // (see ScaledBalanceTokenBase contract), we check for any debt remaining. + if (userReserveDebt != 0) { + debtReserveCache.nextScaledVariableDebt = IVariableDebtToken( + debtReserveCache.variableDebtTokenAddress + ).burn( + user, + hasNoCollateralLeft ? userReserveDebt : actualDebtToLiquidate, + debtReserveCache.nextVariableBorrowIndex + ); + } + + uint256 outstandingDebt = userReserveDebt - actualDebtToLiquidate; + if (hasNoCollateralLeft && outstandingDebt != 0) { + debtReserve.deficit += outstandingDebt.toUint128(); + emit DeficitCreated(user, debtAsset, outstandingDebt); + + outstandingDebt = 0; + } + + if (outstandingDebt == 0) { + userConfig.setBorrowing(debtReserve.id, false); + } + + debtReserve.updateInterestRatesAndVirtualBalance( + debtReserveCache, + debtAsset, + actualDebtToLiquidate, + 0 + ); + } + + struct AvailableCollateralToLiquidateLocalVars { + uint256 maxCollateralToLiquidate; + uint256 baseCollateral; + uint256 bonusCollateral; + uint256 collateralAmount; + uint256 debtAmountNeeded; + uint256 liquidationProtocolFeePercentage; + uint256 liquidationProtocolFee; + uint256 collateralToLiquidateInBaseCurrency; + uint256 collateralAssetPrice; + } + + /** + * @notice Calculates how much of a specific collateral can be liquidated, given + * a certain amount of debt asset. + * @dev This function needs to be called after all the checks to validate the liquidation have been performed, + * otherwise it might fail. + * @param collateralReserveConfiguration The data of the collateral reserve + * @param collateralAssetPrice The price of the underlying asset used as collateral + * @param collateralAssetUnit The asset units of the collateral + * @param debtAssetPrice The price of the underlying borrowed asset to be repaid with the liquidation + * @param debtAssetUnit The asset units of the debt + * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover + * @param userCollateralBalance The collateral balance for the specific `collateralAsset` of the user being liquidated + * @param liquidationBonus The collateral bonus percentage to receive as result of the liquidation + * @return The maximum amount that is possible to liquidate given all the liquidation constraints (user balance, close factor) + * @return The amount to repay with the liquidation + * @return The fee taken from the liquidation bonus amount to be paid to the protocol + * @return The collateral amount to liquidate in the base currency used by the price feed + */ + function _calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal pure returns (uint256, uint256, uint256, uint256) { + AvailableCollateralToLiquidateLocalVars memory vars; + vars.collateralAssetPrice = collateralAssetPrice; + vars.liquidationProtocolFeePercentage = collateralReserveConfiguration + .getLiquidationProtocolFee(); + + // This is the base collateral to liquidate based on the given debt to cover + vars.baseCollateral = + ((debtAssetPrice * debtToCover * collateralAssetUnit)) / + (vars.collateralAssetPrice * debtAssetUnit); + + vars.maxCollateralToLiquidate = vars.baseCollateral.percentMul(liquidationBonus); + + if (vars.maxCollateralToLiquidate > userCollateralBalance) { + vars.collateralAmount = userCollateralBalance; + vars.debtAmountNeeded = ((vars.collateralAssetPrice * vars.collateralAmount * debtAssetUnit) / + (debtAssetPrice * collateralAssetUnit)).percentDiv(liquidationBonus); + } else { + vars.collateralAmount = vars.maxCollateralToLiquidate; + vars.debtAmountNeeded = debtToCover; + } + + vars.collateralToLiquidateInBaseCurrency = + (vars.collateralAmount * vars.collateralAssetPrice) / + collateralAssetUnit; + + if (vars.liquidationProtocolFeePercentage != 0) { + vars.bonusCollateral = + vars.collateralAmount - + vars.collateralAmount.percentDiv(liquidationBonus); + + vars.liquidationProtocolFee = vars.bonusCollateral.percentMul( + vars.liquidationProtocolFeePercentage + ); + vars.collateralAmount -= vars.liquidationProtocolFee; + } + return ( + vars.collateralAmount, + vars.debtAmountNeeded, + vars.liquidationProtocolFee, + vars.collateralToLiquidateInBaseCurrency + ); + } + + /** + * @notice Remove a user's bad debt by burning debt tokens. + * @dev This function iterates through all active reserves where the user has a debt position, + * updates their state, and performs the necessary burn. + * @param reservesData The state of all the reserves + * @param reservesList The addresses of all the active reserves + * @param userConfig The user configuration + * @param reservesCount The total number of valid reserves + * @param user The user from which the debt will be burned. + */ + function _burnBadDebt( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + uint256 reservesCount, + address user + ) internal { + for (uint256 i; i < reservesCount; i++) { + if (!userConfig.isBorrowing(i)) { + continue; + } + + address reserveAddress = reservesList[i]; + if (reserveAddress == address(0)) { + continue; + } + + DataTypes.ReserveData storage currentReserve = reservesData[reserveAddress]; + DataTypes.ReserveCache memory reserveCache = currentReserve.cache(); + if (!reserveCache.reserveConfiguration.getActive()) continue; + + currentReserve.updateState(reserveCache); + + _burnDebtTokens( + reserveCache, + currentReserve, + userConfig, + user, + reserveAddress, + IERC20(reserveCache.variableDebtTokenAddress).balanceOf(user), + 0, + true + ); + } + } + function HOOK_liquidation_after_updateState_DBT() internal {} + function HOOK_liquidation_after_updateState_COL() internal {} + function HOOK_liquidation_after_calculateDebt(uint256 userTotalDebt) internal {} +} diff --git a/certora/experimental/Makefile b/certora/experimental/Makefile new file mode 100644 index 00000000..611d7a80 --- /dev/null +++ b/certora/experimental/Makefile @@ -0,0 +1,28 @@ +default: help + +PATCH = applyHarness.patch +CONTRACTS_DIR = ../../src +MUNGED_DIR = munged + +help: + @echo "usage:" + @echo " make clean: remove all generated files (those ignored by git)" + @echo " make $(MUNGED_DIR): create $(MUNGED_DIR) directory by applying the patch file to $(CONTRACTS_DIR)" + @echo " make record: record a new patch file capturing the differences between $(CONTRACTS_DIR) and $(MUNGED_DIR)" + +munged: $(wildcard $(CONTRACTS_DIR)/*.sol) $(PATCH) + rm -rf $@ + mkdir $@ + cp -r ../../src $@ + patch -p0 -d $@ < $(PATCH) + +record: + mkdir tmp + cp -r ../../src tmp + diff -ruN tmp $(MUNGED_DIR) | sed 's+tmp/++g' | sed 's+$(MUNGED_DIR)/++g' > $(PATCH) + rm -rf tmp + +clean: + git clean -fdX + touch $(PATCH) + diff --git a/certora/experimental/ReserveLogic.sol b/certora/experimental/ReserveLogic.sol new file mode 100644 index 00000000..69fc7cbb --- /dev/null +++ b/certora/experimental/ReserveLogic.sol @@ -0,0 +1,396 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IERC20} from '../../../dependencies/openzeppelin/contracts/IERC20.sol'; +import {GPv2SafeERC20} from '../../../dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +import {IVariableDebtToken} from '../../../interfaces/IVariableDebtToken.sol'; +import {IReserveInterestRateStrategy} from '../../../interfaces/IReserveInterestRateStrategy.sol'; +import {ReserveConfiguration} from '../configuration/ReserveConfiguration.sol'; +import {MathUtils} from '../math/MathUtils.sol'; +import {WadRayMath} from '../math/WadRayMath.sol'; +import {PercentageMath} from '../math/PercentageMath.sol'; +import {Errors} from '../helpers/Errors.sol'; +import {DataTypes} from '../types/DataTypes.sol'; +import {SafeCast} from '../../../dependencies/openzeppelin/contracts/SafeCast.sol'; +import {IAToken} from '../../../interfaces/IAToken.sol'; +import {UserConfiguration} from '../configuration/UserConfiguration.sol'; + +/** + * @title ReserveLogic library + * @author Aave + * @notice Implements the logic to update the reserves state + */ +library ReserveLogic { + using WadRayMath for uint256; + using PercentageMath for uint256; + using SafeCast for uint256; + using GPv2SafeERC20 for IERC20; + using ReserveLogic for DataTypes.ReserveData; + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + using UserConfiguration for DataTypes.UserConfigurationMap; + + // See `IPool` for descriptions + event ReserveDataUpdated( + address indexed reserve, + uint256 liquidityRate, + uint256 stableBorrowRate, + uint256 variableBorrowRate, + uint256 liquidityIndex, + uint256 variableBorrowIndex + ); + + event ReserveUsedAsCollateralDisabled(address indexed reserve, address indexed user); + event DeficitCovered(address indexed reserve, address caller, uint256 amountDecreased); + + /** + * @notice Returns the ongoing normalized income for the reserve. + * @dev A value of 1e27 means there is no income. As time passes, the income is accrued + * @dev A value of 2*1e27 means for each unit of asset one unit of income has been accrued + * @param reserve The reserve object + * @return The normalized income, expressed in ray + */ + function getNormalizedIncome( + DataTypes.ReserveData storage reserve + ) internal view returns (uint256) { + uint40 timestamp = reserve.lastUpdateTimestamp; + uint256 ret_val; + //solium-disable-next-line + if (timestamp == block.timestamp) { + //if the index was updated in the same block, no need to perform any calculation + ret_val = reserve.liquidityIndex; + } else { + ret_val = + MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp).rayMul( + reserve.liquidityIndex + ); + } + getNormalizedIncome_hook(ret_val,reserve.aTokenAddress); + return ret_val; + } + + /** + * @notice Returns the ongoing normalized variable debt for the reserve. + * @dev A value of 1e27 means there is no debt. As time passes, the debt is accrued + * @dev A value of 2*1e27 means that for each unit of debt, one unit worth of interest has been accumulated + * @param reserve The reserve object + * @return The normalized variable debt, expressed in ray + */ + function getNormalizedDebt( + DataTypes.ReserveData storage reserve + ) internal view returns (uint256) { + uint40 timestamp = reserve.lastUpdateTimestamp; + uint256 ret_val; + //solium-disable-next-line + if (timestamp == block.timestamp) { + //if the index was updated in the same block, no need to perform any calculation + ret_val = reserve.variableBorrowIndex; + } else { + ret_val = + MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp).rayMul( + reserve.variableBorrowIndex + ); + } + getNormalizedDebt_hook(ret_val,reserve.aTokenAddress); + return ret_val; + } + + /** + * @notice Updates the liquidity cumulative index and the variable borrow index. + * @param reserve The reserve object + * @param reserveCache The caching layer for the reserve data + */ + function updateState( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache + ) internal { + // If time didn't pass since last stored timestamp, skip state update + //solium-disable-next-line + if (reserveCache.reserveLastUpdateTimestamp == uint40(block.timestamp)) { + return; + } + + _updateIndexes(reserve, reserveCache); + _accrueToTreasury(reserve, reserveCache); + + //solium-disable-next-line + reserve.lastUpdateTimestamp = uint40(block.timestamp); + } + + /** + * @notice Accumulates a predefined amount of asset to the reserve as a fixed, instantaneous income. Used for example + * to accumulate the flashloan fee to the reserve, and spread it between all the suppliers. + * @param reserve The reserve object + * @param totalLiquidity The total liquidity available in the reserve + * @param amount The amount to accumulate + * @return The next liquidity index of the reserve + */ + function cumulateToLiquidityIndex( + DataTypes.ReserveData storage reserve, + uint256 totalLiquidity, + uint256 amount + ) internal returns (uint256) { + //next liquidity index is calculated this way: `((amount / totalLiquidity) + 1) * liquidityIndex` + //division `amount / totalLiquidity` done in ray for precision + uint256 result = (amount.wadToRay().rayDiv(totalLiquidity.wadToRay()) + WadRayMath.RAY).rayMul( + reserve.liquidityIndex + ); + reserve.liquidityIndex = result.toUint128(); + return result; + } + + /** + * @notice Initializes a reserve. + * @param reserve The reserve object + * @param aTokenAddress The address of the overlying atoken contract + * @param variableDebtTokenAddress The address of the overlying variable debt token contract + * @param interestRateStrategyAddress The address of the interest rate strategy contract + */ + function init( + DataTypes.ReserveData storage reserve, + address aTokenAddress, + address variableDebtTokenAddress, + address interestRateStrategyAddress + ) internal { + require(reserve.aTokenAddress == address(0), Errors.RESERVE_ALREADY_INITIALIZED); + + reserve.liquidityIndex = uint128(WadRayMath.RAY); + reserve.variableBorrowIndex = uint128(WadRayMath.RAY); + reserve.aTokenAddress = aTokenAddress; + reserve.variableDebtTokenAddress = variableDebtTokenAddress; + reserve.interestRateStrategyAddress = interestRateStrategyAddress; + } + + /** + * @notice Updates the reserve current variable borrow rate and the current liquidity rate. + * @param reserve The reserve reserve to be updated + * @param reserveCache The caching layer for the reserve data + * @param reserveAddress The address of the reserve to be updated + * @param liquidityAdded The amount of liquidity added to the protocol (supply or repay) in the previous action + * @param liquidityTaken The amount of liquidity taken from the protocol (redeem or borrow) + */ + function updateInterestRatesAndVirtualBalance( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache, + address reserveAddress, + uint256 liquidityAdded, + uint256 liquidityTaken + ) internal { + uint256 totalVariableDebt = reserveCache.nextScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex + ); + + (uint256 nextLiquidityRate, uint256 nextVariableRate) = IReserveInterestRateStrategy( + reserve.interestRateStrategyAddress + ).calculateInterestRates( + DataTypes.CalculateInterestRatesParams({ + unbacked: reserve.unbacked, + liquidityAdded: liquidityAdded, + liquidityTaken: liquidityTaken, + totalDebt: totalVariableDebt, + reserveFactor: reserveCache.reserveFactor, + reserve: reserveAddress, + usingVirtualBalance: reserveCache.reserveConfiguration.getIsVirtualAccActive(), + virtualUnderlyingBalance: reserve.virtualUnderlyingBalance + }) + ); + + reserve.currentLiquidityRate = nextLiquidityRate.toUint128(); + reserve.currentVariableBorrowRate = nextVariableRate.toUint128(); + + // Only affect virtual balance if the reserve uses it + if (reserveCache.reserveConfiguration.getIsVirtualAccActive()) { + if (liquidityAdded > 0) { + reserve.virtualUnderlyingBalance += liquidityAdded.toUint128(); + } + if (liquidityTaken > 0) { + reserve.virtualUnderlyingBalance -= liquidityTaken.toUint128(); + } + } + + emit ReserveDataUpdated( + reserveAddress, + nextLiquidityRate, + 0, + nextVariableRate, + reserveCache.nextLiquidityIndex, + reserveCache.nextVariableBorrowIndex + ); + } + + /** + * @notice Mints part of the repaid interest to the reserve treasury as a function of the reserve factor for the + * specific asset. + * @param reserve The reserve to be updated + * @param reserveCache The caching layer for the reserve data + */ + function _accrueToTreasury( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache + ) internal { + if (reserveCache.reserveFactor == 0) { + return; + } + + //calculate the total variable debt at moment of the last interaction + uint256 prevTotalVariableDebt = reserveCache.currScaledVariableDebt.rayMul( + reserveCache.currVariableBorrowIndex + ); + + //calculate the new total variable debt after accumulation of the interest on the index + uint256 currTotalVariableDebt = reserveCache.currScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex + ); + + //debt accrued is the sum of the current debt minus the sum of the debt at the last update + uint256 totalDebtAccrued = currTotalVariableDebt - prevTotalVariableDebt; + + uint256 amountToMint = totalDebtAccrued.percentMul(reserveCache.reserveFactor); + + if (amountToMint != 0) { + reserve.accruedToTreasury += amountToMint.rayDiv(reserveCache.nextLiquidityIndex).toUint128(); + } + } + + /** + * @notice Updates the reserve indexes and the timestamp of the update. + * @param reserve The reserve reserve to be updated + * @param reserveCache The cache layer holding the cached protocol data + */ + function _updateIndexes( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache + ) internal { + // Only cumulating on the supply side if there is any income being produced + // The case of Reserve Factor 100% is not a problem (currentLiquidityRate == 0), + // as liquidity index should not be updated + if (reserveCache.currLiquidityRate != 0) { + uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest( + reserveCache.currLiquidityRate, + reserveCache.reserveLastUpdateTimestamp + ); + reserveCache.nextLiquidityIndex = cumulatedLiquidityInterest.rayMul( + reserveCache.currLiquidityIndex + ); + reserve.liquidityIndex = reserveCache.nextLiquidityIndex.toUint128(); + } + + // Variable borrow index only gets updated if there is any variable debt. + // reserveCache.currVariableBorrowRate != 0 is not a correct validation, + // because a positive base variable rate can be stored on + // reserveCache.currVariableBorrowRate, but the index should not increase + if (reserveCache.currScaledVariableDebt != 0) { + uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest( + reserveCache.currVariableBorrowRate, + reserveCache.reserveLastUpdateTimestamp + ); + reserveCache.nextVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul( + reserveCache.currVariableBorrowIndex + ); + reserve.variableBorrowIndex = reserveCache.nextVariableBorrowIndex.toUint128(); + } + _updateIndexes_hook(reserve,reserveCache); + } + + /** + * @notice Creates a cache object to avoid repeated storage reads and external contract calls when updating state and + * interest rates. + * @param reserve The reserve object for which the cache will be filled + * @return The cache object + */ + function cache( + DataTypes.ReserveData storage reserve + ) internal view returns (DataTypes.ReserveCache memory) { + DataTypes.ReserveCache memory reserveCache; + + reserveCache.reserveConfiguration = reserve.configuration; + reserveCache.reserveFactor = reserveCache.reserveConfiguration.getReserveFactor(); + reserveCache.currLiquidityIndex = reserveCache.nextLiquidityIndex = reserve.liquidityIndex; + reserveCache.currVariableBorrowIndex = reserveCache.nextVariableBorrowIndex = reserve + .variableBorrowIndex; + reserveCache.currLiquidityRate = reserve.currentLiquidityRate; + reserveCache.currVariableBorrowRate = reserve.currentVariableBorrowRate; + + reserveCache.aTokenAddress = reserve.aTokenAddress; + reserveCache.variableDebtTokenAddress = reserve.variableDebtTokenAddress; + + reserveCache.reserveLastUpdateTimestamp = reserve.lastUpdateTimestamp; + + reserveCache.currScaledVariableDebt = reserveCache.nextScaledVariableDebt = IVariableDebtToken( + reserveCache.variableDebtTokenAddress + ).scaledTotalSupply(); + + return reserveCache; + } + + /** + * @notice Reduces a portion or all of the deficit of a specified reserve by burning the equivalent aToken `amount`. + * The caller of this method MUST always be the Umbrella contract and the Umbrella contract is assumed to never have debt. + * @dev Emits the `DeficitCovered() event`. + * @dev If the coverage admin covers its entire balance, `ReserveUsedAsCollateralDisabled()` is emitted. + * @param reservesData The state of all the reserves + * @param userConfig The user configuration mapping that tracks the supplied/borrowed assets + * @param params The additional parameters needed to execute the eliminateDeficit function + */ + function executeEliminateDeficit( + mapping(address => DataTypes.ReserveData) storage reservesData, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteEliminateDeficitParams memory params + ) external { + require(params.amount != 0, Errors.INVALID_AMOUNT); + + DataTypes.ReserveData storage reserve = reservesData[params.asset]; + uint256 currentDeficit = reserve.deficit; + + require(currentDeficit != 0, Errors.RESERVE_NOT_IN_DEFICIT); + require(!userConfig.isBorrowingAny(), Errors.USER_CANNOT_HAVE_DEBT); + + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + reserve.updateState(reserveCache); + bool isActive = reserveCache.reserveConfiguration.getActive(); + require(isActive, Errors.RESERVE_INACTIVE); + + uint256 balanceWriteOff = params.amount; + + if (params.amount > currentDeficit) { + balanceWriteOff = currentDeficit; + } + + if (reserveCache.reserveConfiguration.getIsVirtualAccActive()) { + IAToken(reserveCache.aTokenAddress).burn( + msg.sender, + reserveCache.aTokenAddress, + balanceWriteOff, + reserveCache.nextLiquidityIndex + ); + // update ir due to updateState + reserve.updateInterestRatesAndVirtualBalance(reserveCache, params.asset, 0, 0); + } else { + // This is a special case to allow mintable assets (ex. GHO), which by definition cannot be supplied + // and thus do not use virtual underlying balances. + // In that case, the procedure is 1) sending the underlying asset to the aToken and + // 2) trigger the handleRepayment() for the aToken to dispose of those assets + IERC20(params.asset).safeTransferFrom( + msg.sender, + reserveCache.aTokenAddress, + balanceWriteOff + ); + IAToken(reserveCache.aTokenAddress).handleRepayment( + msg.sender, + // In the context of GHO it's only relevant that the address has no debt. + // Passing the pool is fitting as it's handeling the repayment on behalf of the protocol. + address(this), + balanceWriteOff + ); + // updating the IR is not needed in this case, as the IR is constant for assets without virtual accounting. + } + + reserve.deficit -= balanceWriteOff.toUint128(); + + emit DeficitCovered(params.asset, msg.sender, balanceWriteOff); + } + + function getNormalizedIncome_hook(uint256 ret_val, address aTokenAddress) internal view {} + function getNormalizedDebt_hook(uint256 ret_val, address aTokenAddress) internal view {} + function _updateIndexes_hook(DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache) internal view {} +} diff --git a/certora/experimental/applyHarness.patch b/certora/experimental/applyHarness.patch new file mode 100644 index 00000000..0de63e18 --- /dev/null +++ b/certora/experimental/applyHarness.patch @@ -0,0 +1,231 @@ +diff -ruN .gitignore .gitignore +--- .gitignore 1970-01-01 02:00:00.000000000 +0200 ++++ .gitignore 2024-11-28 09:37:40.688170748 +0200 +@@ -0,0 +1,2 @@ ++* ++!.gitignore +\ No newline at end of file +diff -ruN src/contracts/protocol/libraries/logic/BorrowLogic.sol src/contracts/protocol/libraries/logic/BorrowLogic.sol +--- src/contracts/protocol/libraries/logic/BorrowLogic.sol 2024-11-28 19:17:13.680142695 +0200 ++++ src/contracts/protocol/libraries/logic/BorrowLogic.sol 2024-11-28 09:37:40.688170748 +0200 +@@ -160,7 +160,7 @@ + DataTypes.ReserveData storage reserve = reservesData[params.asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + reserve.updateState(reserveCache); +- ++ repay_hook_1(reserveCache); + uint256 variableDebt = IERC20(reserveCache.variableDebtTokenAddress).balanceOf( + params.onBehalfOf + ); +@@ -183,10 +183,10 @@ + if (params.amount < paybackAmount) { + paybackAmount = params.amount; + } +- ++ repay_hook_2(reserveCache); + reserveCache.nextScaledVariableDebt = IVariableDebtToken(reserveCache.variableDebtTokenAddress) + .burn(params.onBehalfOf, paybackAmount, reserveCache.nextVariableBorrowIndex); +- ++ repay_hook_3(reserveCache, paybackAmount); + reserve.updateInterestRatesAndVirtualBalance( + reserveCache, + params.asset, +@@ -214,6 +214,7 @@ + paybackAmount, + reserveCache.nextLiquidityIndex + ); ++ repay_hook_4(reserveCache); + bool isCollateral = userConfig.isUsingAsCollateral(reserve.id); + if (isCollateral && IAToken(reserveCache.aTokenAddress).scaledBalanceOf(msg.sender) == 0) { + userConfig.setUsingAsCollateral(reserve.id, false); +@@ -232,4 +233,8 @@ + + return paybackAmount; + } ++ function repay_hook_1(DataTypes.ReserveCache memory reserveCache) internal {} ++ function repay_hook_2(DataTypes.ReserveCache memory reserveCache) internal {} ++ function repay_hook_3(DataTypes.ReserveCache memory reserveCache, uint256 paybackAmount) internal {} ++ function repay_hook_4(DataTypes.ReserveCache memory reserveCache) internal {} + } +diff -ruN src/contracts/protocol/libraries/logic/LiquidationLogic.sol src/contracts/protocol/libraries/logic/LiquidationLogic.sol +--- src/contracts/protocol/libraries/logic/LiquidationLogic.sol 2024-11-28 19:17:13.685142696 +0200 ++++ src/contracts/protocol/libraries/logic/LiquidationLogic.sol 2024-11-28 19:09:01.430978734 +0200 +@@ -126,7 +126,9 @@ + DataTypes.ReserveData storage debtReserve = reservesData[params.debtAsset]; + DataTypes.UserConfigurationMap storage userConfig = usersConfig[params.user]; + vars.debtReserveCache = debtReserve.cache(); ++ /*HOOK !!*/ HOOK_liquidation_before_updateState_DBT(); + debtReserve.updateState(vars.debtReserveCache); ++ /*HOOK !!*/ HOOK_liquidation_after_updateState_DBT(); + + ( + vars.totalCollateralInBaseCurrency, +@@ -149,11 +151,13 @@ + ); + + vars.collateralAToken = IAToken(collateralReserve.aTokenAddress); +- vars.userCollateralBalance = vars.collateralAToken.balanceOf(params.user); ++ //vars.userCollateralBalance = vars.collateralAToken.balanceOf(params.user); ++ vars.userCollateralBalance = get_userCollateralBalance(); + vars.userReserveDebt = IERC20(vars.debtReserveCache.variableDebtTokenAddress).balanceOf( + params.user + ); + ++ /*HOOK !!*/ HOOK_liquidation_before_validateLiquidationCall(vars.userReserveDebt); + ValidationLogic.validateLiquidationCall( + userConfig, + collateralReserve, +@@ -272,6 +276,7 @@ + + bool hasNoCollateralLeft = vars.totalCollateralInBaseCurrency == + vars.collateralToLiquidateInBaseCurrency; ++ /*HOOK !!*/ HOOK_liquidation_before_burnDebtTokens(hasNoCollateralLeft); + _burnDebtTokens( + vars.debtReserveCache, + debtReserve, +@@ -282,7 +287,8 @@ + vars.actualDebtToLiquidate, + hasNoCollateralLeft + ); +- ++ /*HOOK !!*/ HOOK_liquidation_after_burnDebtTokens(hasNoCollateralLeft, vars.actualDebtToLiquidate, vars.userReserveDebt); ++ + // IsolationModeTotalDebt only discounts `actualDebtToLiquidate`, not the fully burned amount in case of deficit creation. + // This is by design as otherwise debt debt ceiling would render ineffective if a collateral asset faces bad debt events. + // The governance can decide the raise the ceiling to discount manifested deficit. +@@ -294,10 +300,12 @@ + vars.actualDebtToLiquidate + ); + ++ /*HOOK !!*/ HOOK_liquidation_before_burnCollateralATokens(vars.actualCollateralToLiquidate); + if (params.receiveAToken) { + _liquidateATokens(reservesData, reservesList, usersConfig, collateralReserve, params, vars); + } else { + _burnCollateralATokens(collateralReserve, params, vars); ++ /*HOOK !!*/ HOOK_liquidation_after_burnCollateralATokens(vars.actualCollateralToLiquidate); + } + + // Transfer fee to treasury if it is non-zero +@@ -321,9 +329,11 @@ + // burn bad debt if necessary + // Each additional debt asset already adds around ~75k gas to the liquidation. + // To keep the liquidation gas under control, 0 usd collateral positions are not touched, as there is no immediate benefit in burning or transfering to treasury. ++ /*HOOK !!*/ HOOK_liquidation_before_burnBadDebt(); + if (hasNoCollateralLeft && userConfig.isBorrowingAny()) { + _burnBadDebt(reservesData, reservesList, userConfig, params.reservesCount, params.user); + } ++ /*HOOK !!*/ HOOK_liquidation_after_burnBadDebt(); + + // Transfers the debt asset being repaid to the aToken, where the liquidity is kept + IERC20(params.debtAsset).safeTransferFrom( +@@ -363,6 +373,8 @@ + ) internal { + DataTypes.ReserveCache memory collateralReserveCache = collateralReserve.cache(); + collateralReserve.updateState(collateralReserveCache); ++ /*HOOK !!*/ HOOK_burnCollateralATokens_after_updateState(); ++ + collateralReserve.updateInterestRatesAndVirtualBalance( + collateralReserveCache, + params.collateralAsset, +@@ -624,9 +636,9 @@ + DataTypes.ReserveData storage currentReserve = reservesData[reserveAddress]; + DataTypes.ReserveCache memory reserveCache = currentReserve.cache(); + if (!reserveCache.reserveConfiguration.getActive()) continue; +- ++ /*HOOK !!*/ HOOK_burnBadDebt_inside_loop(reserveAddress); + currentReserve.updateState(reserveCache); +- ++ /*HOOK !!*/ HOOK_burnBadDebt_before_burnDebtTokens(reserveAddress, IERC20(reserveCache.variableDebtTokenAddress).balanceOf(user)); + _burnDebtTokens( + reserveCache, + currentReserve, +@@ -637,6 +649,22 @@ + 0, + true + ); ++ /*HOOK !!*/ HOOK_burnBadDebt_after_burnDebtTokens(reserveAddress); + } + } ++ function HOOK_liquidation_before_updateState_DBT() internal {} ++ function HOOK_liquidation_after_updateState_DBT() internal {} ++ function HOOK_burnCollateralATokens_after_updateState() internal {} ++ function HOOK_liquidation_before_validateLiquidationCall(uint256 userTotalDebt) internal {} ++ function HOOK_liquidation_before_burnDebtTokens(bool hasNoCollateralLeft) internal {} ++ function HOOK_liquidation_after_burnDebtTokens(bool hasNoCollateralLeft, uint256 actualDebtToLiquidate, uint256 userReserveDebt) internal {} ++ function HOOK_liquidation_before_burnCollateralATokens(uint256 actualCollateralToLiquidate) internal {} ++ function HOOK_liquidation_after_burnCollateralATokens(uint256 actualCollateralToLiquidate) internal {} ++ function HOOK_liquidation_before_burnBadDebt() internal {} ++ function HOOK_burnBadDebt_inside_loop(address reserveAddress) internal {} ++ function HOOK_burnBadDebt_before_burnDebtTokens(address reserveAddress, uint256 amount) internal {} ++ function HOOK_burnBadDebt_after_burnDebtTokens(address reserveAddress) internal {} ++ function HOOK_liquidation_after_burnBadDebt() internal {} ++ ++ function get_userCollateralBalance() internal returns(uint256) {return 0;} + } +diff -ruN src/contracts/protocol/libraries/logic/ReserveLogic.sol src/contracts/protocol/libraries/logic/ReserveLogic.sol +--- src/contracts/protocol/libraries/logic/ReserveLogic.sol 2024-11-28 19:17:13.680142695 +0200 ++++ src/contracts/protocol/libraries/logic/ReserveLogic.sol 2024-11-28 09:37:40.691170771 +0200 +@@ -53,17 +53,19 @@ + DataTypes.ReserveData storage reserve + ) internal view returns (uint256) { + uint40 timestamp = reserve.lastUpdateTimestamp; +- ++ uint256 ret_val; + //solium-disable-next-line + if (timestamp == block.timestamp) { + //if the index was updated in the same block, no need to perform any calculation +- return reserve.liquidityIndex; ++ ret_val = reserve.liquidityIndex; + } else { +- return ++ ret_val = + MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp).rayMul( + reserve.liquidityIndex + ); + } ++ getNormalizedIncome_hook(ret_val,reserve.aTokenAddress); ++ return ret_val; + } + + /** +@@ -77,17 +79,19 @@ + DataTypes.ReserveData storage reserve + ) internal view returns (uint256) { + uint40 timestamp = reserve.lastUpdateTimestamp; +- ++ uint256 ret_val; + //solium-disable-next-line + if (timestamp == block.timestamp) { + //if the index was updated in the same block, no need to perform any calculation +- return reserve.variableBorrowIndex; ++ ret_val = reserve.variableBorrowIndex; + } else { +- return ++ ret_val = + MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp).rayMul( + reserve.variableBorrowIndex + ); + } ++ getNormalizedDebt_hook(ret_val,reserve.aTokenAddress); ++ return ret_val; + } + + /** +@@ -285,6 +289,7 @@ + ); + reserve.variableBorrowIndex = reserveCache.nextVariableBorrowIndex.toUint128(); + } ++ _updateIndexes_hook(reserve,reserveCache); + } + + /** +@@ -401,4 +406,9 @@ + + emit DeficitCovered(params.asset, msg.sender, balanceWriteOff); + } ++ ++ function getNormalizedIncome_hook(uint256 ret_val, address aTokenAddress) internal view {} ++ function getNormalizedDebt_hook(uint256 ret_val, address aTokenAddress) internal view {} ++ function _updateIndexes_hook(DataTypes.ReserveData storage reserve, ++ DataTypes.ReserveCache memory reserveCache) internal view {} + } diff --git a/certora/experimental/confs/ATokenEqCheck.conf b/certora/experimental/confs/ATokenEqCheck.conf new file mode 100644 index 00000000..a175b36a --- /dev/null +++ b/certora/experimental/confs/ATokenEqCheck.conf @@ -0,0 +1,22 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "src/core/instances/ATokenInstance.sol", + "certora/harnesses/Aave/PoolInstanceForAToken.sol:PoolInstance" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "aToken check", + "optimistic_loop": true, // for name, symbol + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": true, // required for autofinders in ATokenInstance to compile + "verify": "ATokenInstance:certora/specs/Aave/aTokenCheck.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/AaveMathCheck.conf b/certora/experimental/confs/AaveMathCheck.conf new file mode 100644 index 00000000..aaf79721 --- /dev/null +++ b/certora/experimental/confs/AaveMathCheck.conf @@ -0,0 +1,18 @@ +{ + "assert_autofinder_success": true, + // including dummy file + "files": [ + "certora/harnesses/Utilities.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "generic Aave math checks", + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "verify": "Utilities:certora/specs/Aave/AaveMathCheck.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/DefaultReserveInterestRateStrategyV2_check.conf b/certora/experimental/confs/DefaultReserveInterestRateStrategyV2_check.conf new file mode 100644 index 00000000..763dc263 --- /dev/null +++ b/certora/experimental/confs/DefaultReserveInterestRateStrategyV2_check.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "certora/munged/src/core/contracts/protocol/pool/DefaultReserveInterestRateStrategyV2.sol" + ], + "process": "emv", + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "disable_solc_optimizers": ["cse","peephole"], + // "optimistic_loop": true, + /*"prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ],*/ + "smt_timeout": "3000", + "prover_version": "master", + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": false, + "verify": "DefaultReserveInterestRateStrategyV2:certora/specs/Aave/ReserveInterestRateStrategyCheck.spec", + "msg": "Aave V3 DefaultReserveInterestRateStrategyV2 - check summaries", +} \ No newline at end of file diff --git a/certora/experimental/confs/PoolInstance.conf b/certora/experimental/confs/PoolInstance.conf new file mode 100644 index 00000000..fb7c10a6 --- /dev/null +++ b/certora/experimental/confs/PoolInstance.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "src/core/instances/PoolInstance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "Aave V3 Pool", + "optimistic_loop": true, + "process": "emv", + /*"prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ],*/ + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": false, + "verify": "PoolInstance:certora/specs/Aave/Pool.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/VariableDebtTokenEqCheck.conf b/certora/experimental/confs/VariableDebtTokenEqCheck.conf new file mode 100644 index 00000000..3b11cae4 --- /dev/null +++ b/certora/experimental/confs/VariableDebtTokenEqCheck.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "src/core/instances/VariableDebtTokenInstance.sol", + "certora/harnesses/Aave/PoolInstanceForAToken.sol:PoolInstance" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "VariableDebtToken check", + "optimistic_loop": true, // for name, symbol + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "verify": "VariableDebtTokenInstance:certora/specs/Aave/VariableDebtTokenCheck.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/indexes_rates.conf b/certora/experimental/confs/indexes_rates.conf new file mode 100644 index 00000000..95a21fc0 --- /dev/null +++ b/certora/experimental/confs/indexes_rates.conf @@ -0,0 +1,29 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "certora/munged/src/core/instances/PoolInstance.sol" + ], + "disable_solc_optimizers": [ + "cse", + "peephole" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "Indexes and Rates", + "optimistic_loop": true, + "process": "emv", +// "prover_args": ["-numOfUnsatCores 1"], + "prover_version": "master", + "server": "prover", + "smt_timeout": "3000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", +// "coverage_info": "basic", + "verify": "PoolInstance:certora/specs/Aave/indexes_rates.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/irrelevant/PoolInstance_builtin_assertions.conf b/certora/experimental/confs/irrelevant/PoolInstance_builtin_assertions.conf new file mode 100644 index 00000000..3e588c77 --- /dev/null +++ b/certora/experimental/confs/irrelevant/PoolInstance_builtin_assertions.conf @@ -0,0 +1,20 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "src/core/instances/PoolInstance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": false, + "verify": "PoolInstance:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/irrelevant/PoolInstance_sanity.conf b/certora/experimental/confs/irrelevant/PoolInstance_sanity.conf new file mode 100644 index 00000000..86df8b0f --- /dev/null +++ b/certora/experimental/confs/irrelevant/PoolInstance_sanity.conf @@ -0,0 +1,18 @@ +{ + "assert_autofinder_success": true, + "files": [ + "src/core/instances/PoolInstance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": false, + "verify": "PoolInstance:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/irrelevant/PoolInstance_sanity_with_erc20cvl.conf b/certora/experimental/confs/irrelevant/PoolInstance_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..53e9a954 --- /dev/null +++ b/certora/experimental/confs/irrelevant/PoolInstance_sanity_with_erc20cvl.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "src/core/instances/PoolInstance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": false, + "verify": "PoolInstance:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/irrelevant/PoolInstance_sanity_with_erc20dispatched.conf b/certora/experimental/confs/irrelevant/PoolInstance_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..6063317c --- /dev/null +++ b/certora/experimental/confs/irrelevant/PoolInstance_sanity_with_erc20dispatched.conf @@ -0,0 +1,22 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "src/core/instances/PoolInstance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": false, + "verify": "PoolInstance:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/borrow.conf b/certora/experimental/confs/solvency/borrow.conf new file mode 100644 index 00000000..1c51bd71 --- /dev/null +++ b/certora/experimental/confs/solvency/borrow.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - BORROW", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/borrow.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/cumulateToLiquidityIndexCVL-check.conf b/certora/experimental/confs/solvency/cumulateToLiquidityIndexCVL-check.conf new file mode 100644 index 00000000..d00318ce --- /dev/null +++ b/certora/experimental/confs/solvency/cumulateToLiquidityIndexCVL-check.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "cumulateToLiquidityIndex check", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/cumulateToLiquidityIndexCVL-check.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/flashloan.conf b/certora/experimental/confs/solvency/flashloan.conf new file mode 100644 index 00000000..3b3f7f81 --- /dev/null +++ b/certora/experimental/confs/solvency/flashloan.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY-FLASHLOAN RULE", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/flashloan.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/liquidationCall-COLasset-lemma.conf b/certora/experimental/confs/solvency/liquidationCall-COLasset-lemma.conf new file mode 100644 index 00000000..944c5fac --- /dev/null +++ b/certora/experimental/confs/solvency/liquidationCall-COLasset-lemma.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - COLasset lemma", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, +// "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-lemma.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/liquidationCall-COLasset-main.conf b/certora/experimental/confs/solvency/liquidationCall-COLasset-main.conf new file mode 100644 index 00000000..2a4c7ec8 --- /dev/null +++ b/certora/experimental/confs/solvency/liquidationCall-COLasset-main.conf @@ -0,0 +1,22 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - COLasset main", + "optimistic_loop": true, + "process": "emv", +// "prover_args": [" -split false" ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, +// "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-main.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/liquidationCall-COLasset-totSUP0-lemma.conf b/certora/experimental/confs/solvency/liquidationCall-COLasset-totSUP0-lemma.conf new file mode 100644 index 00000000..04e2d10b --- /dev/null +++ b/certora/experimental/confs/solvency/liquidationCall-COLasset-totSUP0-lemma.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - COLasset totSUP==0 lemma", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-totSUP0-lemma.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/liquidationCall-COLasset-totSUP0.conf b/certora/experimental/confs/solvency/liquidationCall-COLasset-totSUP0.conf new file mode 100644 index 00000000..2ea79d64 --- /dev/null +++ b/certora/experimental/confs/solvency/liquidationCall-COLasset-totSUP0.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - COLasset case totDebt==0", + "optimistic_loop": true, + "process": "emv", +// "prover_args": [ " -split false" ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-totSUP0.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/liquidationCall-DBTasset-lemma.conf b/certora/experimental/confs/solvency/liquidationCall-DBTasset-lemma.conf new file mode 100644 index 00000000..9758293a --- /dev/null +++ b/certora/experimental/confs/solvency/liquidationCall-DBTasset-lemma.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - DBTasset lemma", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, +// "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-lemma.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/liquidationCall-DBTasset-main.conf b/certora/experimental/confs/solvency/liquidationCall-DBTasset-main.conf new file mode 100644 index 00000000..4aec24d9 --- /dev/null +++ b/certora/experimental/confs/solvency/liquidationCall-DBTasset-main.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - DBTasset main", + "optimistic_loop": true, + "process": "emv", +// "prover_args": [ " -split false" ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, +// "precise_bitwise_ops": true, +// "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-main.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/liquidationCall-DBTasset-totSUP0.conf b/certora/experimental/confs/solvency/liquidationCall-DBTasset-totSUP0.conf new file mode 100644 index 00000000..0c881a51 --- /dev/null +++ b/certora/experimental/confs/solvency/liquidationCall-DBTasset-totSUP0.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - DBTasset must REVERT if totDebt==0", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-totSUP0.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/liquidationCall-burnBadDebt-assetINloop.conf b/certora/experimental/confs/solvency/liquidationCall-burnBadDebt-assetINloop.conf new file mode 100644 index 00000000..0c1c3a16 --- /dev/null +++ b/certora/experimental/confs/solvency/liquidationCall-burnBadDebt-assetINloop.conf @@ -0,0 +1,22 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - _burnBadDebt - asset==loopAsset", + "optimistic_loop": true, + "process": "emv", +// "prover_args": [" -split false" ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", +// "rule_sanity": "basic", + "multi_assert_check" : true, + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/liquidationCall/burnBadDebt-assetINloop.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/liquidationCall-burnBadDebt-assetNOTINloop.conf b/certora/experimental/confs/solvency/liquidationCall-burnBadDebt-assetNOTINloop.conf new file mode 100644 index 00000000..fa2304bc --- /dev/null +++ b/certora/experimental/confs/solvency/liquidationCall-burnBadDebt-assetNOTINloop.conf @@ -0,0 +1,22 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - _burnBadDebt - asset NEQ loopAsset", + "optimistic_loop": true, + "process": "emv", +// "prover_args": [" -split false" ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", +// "rule_sanity": "basic", + "multi_assert_check" : true, + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/liquidationCall/burnBadDebt-assetNOTINloop.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/liquidationCall-sanity.conf b/certora/experimental/confs/solvency/liquidationCall-sanity.conf new file mode 100644 index 00000000..b5986de8 --- /dev/null +++ b/certora/experimental/confs/solvency/liquidationCall-sanity.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - SANITY", + "optimistic_loop": true, + "process": "emv", +// "prover_args": [ " -split false" ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, +// "precise_bitwise_ops": true, +// "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/liquidationCall/sanity.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/repay-NONindexSUMM.conf b/certora/experimental/confs/solvency/repay-NONindexSUMM.conf new file mode 100644 index 00000000..a91fff9f --- /dev/null +++ b/certora/experimental/confs/solvency/repay-NONindexSUMM.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - REPAY - NON INDEX SUMMARIZATION", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/repay/repay-NONindexSUMM.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/repay-indexSUMM.conf b/certora/experimental/confs/solvency/repay-indexSUMM.conf new file mode 100644 index 00000000..fe6f535d --- /dev/null +++ b/certora/experimental/confs/solvency/repay-indexSUMM.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - REPAY - INDEX SUMMARIZATION", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/repay/repay-indexSUMM.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/repayWithATokens-HOOKS.conf b/certora/experimental/confs/solvency/repayWithATokens-HOOKS.conf new file mode 100644 index 00000000..065655b1 --- /dev/null +++ b/certora/experimental/confs/solvency/repayWithATokens-HOOKS.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/ERC20Like/DummyWeth.sol", + "certora/experimental/harnesses/Utilities.sol", + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - REPAY WITH ATOKENS - HOOKS", + "optimistic_loop": true, + "process": "emv", + "prover_args": ["-prettifyCEX false", +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/repayWithATokens/repayWithATokens-HOOKS.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/repayWithATokens-noHOOKS.conf b/certora/experimental/confs/solvency/repayWithATokens-noHOOKS.conf new file mode 100644 index 00000000..773fddde --- /dev/null +++ b/certora/experimental/confs/solvency/repayWithATokens-noHOOKS.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - REPAY-WITH-ATOKENS - no HOOKS", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/repayWithATokens/repayWithATokens-noHOOKS.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/supply.conf b/certora/experimental/confs/solvency/supply.conf new file mode 100644 index 00000000..52ca5e2d --- /dev/null +++ b/certora/experimental/confs/solvency/supply.conf @@ -0,0 +1,20 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": ["certora/experimental/harnesses/Aave/PoolInstanceHarness.sol"], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - SUPPLY", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/supply.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/time_passing.conf b/certora/experimental/confs/solvency/time_passing.conf new file mode 100644 index 00000000..a2c95ea2 --- /dev/null +++ b/certora/experimental/confs/solvency/time_passing.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/ERC20Like/DummyWeth.sol", + "certora/experimental/harnesses/Utilities.sol", + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY : TIME PASSING", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/time_passing.spec" +} \ No newline at end of file diff --git a/certora/experimental/confs/solvency/withdraw.conf b/certora/experimental/confs/solvency/withdraw.conf new file mode 100644 index 00000000..db8d43be --- /dev/null +++ b/certora/experimental/confs/solvency/withdraw.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/experimental/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - WITHDRAW", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/experimental/specs/Aave/solvency/withdraw.spec" +} \ No newline at end of file diff --git a/certora/experimental/harnesses/Aave/DummyContract.sol b/certora/experimental/harnesses/Aave/DummyContract.sol new file mode 100644 index 00000000..bc1dc298 --- /dev/null +++ b/certora/experimental/harnesses/Aave/DummyContract.sol @@ -0,0 +1,8 @@ + +contract DummyContract { + function havoc_all_dummy() external { + // havoc_all_dummy_internal(); + } + + //function havoc_all_dummy_internal() internal {} +} diff --git a/certora/experimental/harnesses/Aave/PoolInstanceForAToken.sol b/certora/experimental/harnesses/Aave/PoolInstanceForAToken.sol new file mode 100644 index 00000000..1f5a4730 --- /dev/null +++ b/certora/experimental/harnesses/Aave/PoolInstanceForAToken.sol @@ -0,0 +1,34 @@ +import "../Utilities.sol"; + +contract PoolInstance is Utilities { + uint h; + + function getReserveNormalizedIncomeInt( + address asset + ) internal returns (uint256) { + // should be summarized, if not, h will be nondet and other 'bad' things + this.havocAll(); + return h; + } + + function getReserveNormalizedIncome( + address asset + ) external returns (uint256) { + return getReserveNormalizedIncomeInt(asset); + } + + + function getReserveNormalizedVariableDebtInt( + address asset + ) internal returns (uint256) { + // should be summarized, if not, h will be nondet and other 'bad' things + this.havocAll(); + return h; + } + + function getReserveNormalizedVariableDebt( + address asset + ) external returns (uint256) { + return getReserveNormalizedVariableDebtInt(asset); + } +} \ No newline at end of file diff --git a/certora/experimental/harnesses/Aave/PoolInstanceHarness.sol b/certora/experimental/harnesses/Aave/PoolInstanceHarness.sol new file mode 100644 index 00000000..38a27c09 --- /dev/null +++ b/certora/experimental/harnesses/Aave/PoolInstanceHarness.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Pool} from '../../munged/src/contracts/protocol/pool/Pool.sol'; +import {IPoolAddressesProvider} from '../../munged/src/contracts/interfaces/IPoolAddressesProvider.sol'; +import {PoolInstance} from '../../munged/src/contracts/instances/PoolInstance.sol'; +import {DataTypes} from '../../munged/src/contracts/protocol/libraries/types/DataTypes.sol'; +import {ReserveLogic} from '../../munged/src/contracts/protocol/libraries/logic/ReserveLogic.sol'; +import {WadRayMath} from '../../munged/src/contracts/protocol/libraries/math/WadRayMath.sol'; +import {LiquidationLogic} from '../../munged/src/contracts/protocol/libraries/logic/LiquidationLogic.sol'; + +import {DummyContract} from "./DummyContract.sol"; + + +contract PoolInstanceHarness is PoolInstance { + DummyContract DUMMY; + + constructor(IPoolAddressesProvider provider) PoolInstance(provider) {} + + function cumulateToLiquidityIndex(address asset, + uint256 totalLiquidity, + uint256 amount + ) external returns (uint256) { + return ReserveLogic.cumulateToLiquidityIndex(_reserves[asset], totalLiquidity, amount); + } + + function getNormalizedIncome(address asset) external returns (uint256) { + return ReserveLogic.getNormalizedIncome(_reserves[asset]); + } + + function getNormalizedDebt(address asset) external returns (uint256) { + return ReserveLogic.getNormalizedDebt(_reserves[asset]); + } + + function havoc_all() public { + DUMMY.havoc_all_dummy(); + } + + function rayMul(uint256 a, uint256 b) external returns (uint256) { + return WadRayMath.rayMul(a,b); + } + + function rayDiv(uint256 a, uint256 b) external returns (uint256) { + return WadRayMath.rayDiv(a,b); + } + + function getReserveDataExtended(address asset) + external view returns (DataTypes.ReserveData memory) { + return _reserves[asset]; + } + + function _burnBadDebt_WRP(address user) external { + LiquidationLogic._burnBadDebt(_reserves, _reservesList, _usersConfig[user], _reservesCount, user); + } +} diff --git a/certora/experimental/harnesses/ERC20Like/DummyERC20A.sol b/certora/experimental/harnesses/ERC20Like/DummyERC20A.sol new file mode 100644 index 00000000..de4d8a41 --- /dev/null +++ b/certora/experimental/harnesses/ERC20Like/DummyERC20A.sol @@ -0,0 +1,55 @@ +// Represents a symbolic/dummy ERC20 token + +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +contract DummyERC20A { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + + return true; + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + + return true; + } +} diff --git a/certora/experimental/harnesses/ERC20Like/DummyERC20B.sol b/certora/experimental/harnesses/ERC20Like/DummyERC20B.sol new file mode 100644 index 00000000..2dc4ffdc --- /dev/null +++ b/certora/experimental/harnesses/ERC20Like/DummyERC20B.sol @@ -0,0 +1,55 @@ +// Represents a symbolic/dummy ERC20 token + +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +contract DummyERC20B { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + + return true; + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + + return true; + } +} diff --git a/certora/experimental/harnesses/ERC20Like/DummyWeth.sol b/certora/experimental/harnesses/ERC20Like/DummyWeth.sol new file mode 100644 index 00000000..426378d6 --- /dev/null +++ b/certora/experimental/harnesses/ERC20Like/DummyWeth.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity >=0.8.0; + +/** + * Dummy Weth token. + */ +contract DummyWeth { + uint256 t; + + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + return true; + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + return true; + } + + // WETH + function deposit() external payable { + b[msg.sender] += msg.value; + } + + function withdraw(uint256 amt) external { + b[msg.sender] -= amt; + payable(msg.sender).transfer(amt); // use optimistic_fallback here + } +} diff --git a/certora/experimental/harnesses/Utilities.sol b/certora/experimental/harnesses/Utilities.sol new file mode 100644 index 00000000..81d264cc --- /dev/null +++ b/certora/experimental/harnesses/Utilities.sol @@ -0,0 +1,22 @@ + +import {MathUtils} from '../munged/src/contracts/protocol/libraries/math/MathUtils.sol'; +import {FlashLoanLogic} from '../munged/src/contracts/protocol/libraries/logic/FlashLoanLogic.sol'; +import {DataTypes} from '../munged/src/contracts/protocol/libraries/types/DataTypes.sol'; + + +contract Utilities { + function havocAll() external { + (bool success, ) = address(0xdeadbeef).call(abi.encodeWithSelector(0x12345678)); + require(success); + } + + function justRevert() external { + revert(); + } + + function nop() external {} + + function SECONDS_PER_YEAR() external view returns (uint256) { + return MathUtils.SECONDS_PER_YEAR; + } +} diff --git a/certora/experimental/munged/.gitignore b/certora/experimental/munged/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/certora/experimental/munged/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/certora/experimental/scripts/run-all.sh b/certora/experimental/scripts/run-all.sh new file mode 100644 index 00000000..3243b8b6 --- /dev/null +++ b/certora/experimental/scripts/run-all.sh @@ -0,0 +1,79 @@ +#CMN="--compilation_steps_only" + + + +#echo "******** Running: 1 ***************" +#certoraRun $CMN certora/experimental/confs/solvency/time_passing.conf \ +# --msg "1: time_passing.conf" + +echo "******** Running: 2 ***************" +certoraRun $CMN certora/experimental/confs/solvency/borrow.conf \ + --msg "2: borrow.conf" + +echo "******** Running: 3 ***************" +certoraRun $CMN certora/experimental/confs/solvency/supply.conf \ + --msg "3: supply.conf" + +echo "******** Running: 4 ***************" +certoraRun $CMN certora/experimental/confs/solvency/withdraw.conf \ + --msg "4: withdraw.conf" + +echo "******** Running: 5a ***************" +certoraRun $CMN certora/experimental/confs/solvency/repay-indexSUMM.conf \ + --msg "5a: repay-indexSUMM.conf" + +echo "******** Running: 5b ***************" +certoraRun $CMN certora/experimental/confs/solvency/repay-NONindexSUMM.conf \ + --msg "5b: repay-NONindexSUMM.conf" + +echo "******** Running: 6a ***************" +certoraRun $CMN certora/experimental/confs/solvency/repayWithATokens-HOOKS.conf \ + --multi_assert_check \ + --msg "6a: repayWithATokens-HOOKS.conf" + +echo "******** Running: 6b ***************" +certoraRun $CMN certora/experimental/confs/solvency/repayWithATokens-noHOOKS.conf \ + --msg "6b: repayWithATokens-noHOOKS.conf" + +echo "******** Running: 7 ***************" +certoraRun $CMN certora/experimental/confs/solvency/flashloan.conf --rule solvency__flashLoanSimple \ + --multi_assert_check \ + --msg "7: flashloan.conf" + +echo "******** Running: 8 ***************" +certoraRun $CMN certora/experimental/confs/solvency/cumulateToLiquidityIndexCVL-check.conf --rule check_cumulateToLiquidityIndexCVL \ + --msg "8: cumulateToLiquidityIndexCVL-check.conf :: check_cumulateToLiquidityIndexCVL" + + +echo "******** Running: 9a ***************" +certoraRun $CMN certora/experimental/confs/solvency/liquidationCall-DBTasset-main.conf \ + --msg "9a: liquidationCall-DBTasset-main.conf" + +echo "******** Running: 9b ***************" +certoraRun $CMN certora/experimental/confs/solvency/liquidationCall-DBTasset-lemma.conf \ + --msg "9b: liquidationCall-DBTasset-lemma.conf" + +echo "******** Running: 9c ***************" +certoraRun $CMN certora/experimental/confs/solvency/liquidationCall-DBTasset-totSUP0.conf \ + --msg "9c: liquidationCall-DBTasset-totSUP0.conf" + +echo "******** Running: 9d ***************" +certoraRun $CMN certora/experimental/confs/solvency/liquidationCall-COLasset-main.conf \ + --msg "9d: liquidationCall-COLasset-main.conf" + +echo "******** Running: 9e ***************" +certoraRun $CMN certora/experimental/confs/solvency/liquidationCall-COLasset-lemma.conf \ + --msg "9e: liquidationCall-COLasset-lemma.conf" + +echo "******** Running: 9f ***************" +certoraRun $CMN certora/experimental/confs/solvency/liquidationCall-COLasset-totSUP0.conf \ + --msg "9f: liquidationCall-COLasset-totSUP0.conf" + +echo "******** Running: 9g ***************" +certoraRun $CMN certora/experimental/confs/solvency/liquidationCall-COLasset-totSUP0-lemma.conf \ + --msg "9g: liquidationCall-COLasset-totSUP0.conf" + + + + + diff --git a/certora/experimental/specs/Aave/ACLManager.spec b/certora/experimental/specs/Aave/ACLManager.spec new file mode 100644 index 00000000..6777414f --- /dev/null +++ b/certora/experimental/specs/Aave/ACLManager.spec @@ -0,0 +1,10 @@ +methods { + function _.isPoolAdmin(address admin) external => NONDET; // expect bool; + function _.isEmergencyAdmin(address admin) external => NONDET; // expect bool; + function _.isRiskAdmin(address admin) external => NONDET; // expect bool; + function _.isFlashBorrower(address borrower) external => NONDET; // expect bool; + function _.isBridge(address bridge) external => NONDET; // expect bool; + function _.isAssetListingAdmin(address admin) external => NONDET; // expect bool; + + function _.hasRole(bytes32 role, address account) external => NONDET; // expect bool; +} \ No newline at end of file diff --git a/certora/experimental/specs/Aave/AaveMath.spec b/certora/experimental/specs/Aave/AaveMath.spec new file mode 100644 index 00000000..7d8a122e --- /dev/null +++ b/certora/experimental/specs/Aave/AaveMath.spec @@ -0,0 +1,10 @@ +import "../Math/CVLMath.spec"; + +/* Math utils */ +function rayMulCVL(uint x, uint y) returns uint256 { + return mulDivUpAbstractPlus(x, y, RAY()); +} + +function rayDivCVL(uint x, uint y) returns uint256 { + return mulDivUpAbstractPlus(x, RAY(), y); +} diff --git a/certora/experimental/specs/Aave/AaveMathCheck.spec b/certora/experimental/specs/Aave/AaveMathCheck.spec new file mode 100644 index 00000000..84aefc1e --- /dev/null +++ b/certora/experimental/specs/Aave/AaveMathCheck.spec @@ -0,0 +1,15 @@ +import "./AaveMath.spec"; + +rule rayMulCVLCorrectness(uint x, uint y) { + /* simluate rayMul */ + uint solRes = rayMulCVLPrecise(x, y); + uint cvlRes = rayMulCVL(x, y); + assert solRes == cvlRes; +} + +rule rayDivCVLCorrectness(uint x, uint y) { + /* simluate rayDiv */ + uint solRes = rayDivCVLPrecise(x, y); + uint cvlRes = rayDivCVL(x, y); + assert solRes == cvlRes; +} \ No newline at end of file diff --git a/certora/experimental/specs/Aave/AddressProvider.spec b/certora/experimental/specs/Aave/AddressProvider.spec new file mode 100644 index 00000000..34535bc7 --- /dev/null +++ b/certora/experimental/specs/Aave/AddressProvider.spec @@ -0,0 +1,11 @@ +methods { + function _.getMarketId() external => NONDET; // expect string; + function _.getAddress(bytes32 id) external => NONDET; // expect address; + function _.getPool() external => NONDET; // expect address; + function _.getPoolConfigurator() external => NONDET; // expect address; + function _.getPriceOracle() external => NONDET; // expect address; + function _.getACLManager() external => NONDET; // expect address; + function _.getACLAdmin() external => NONDET; // expect address; + function _.getPriceOracleSentinel() external => NONDET; // expect address; + function _.getPoolDataProvider() external => NONDET; // expect address; +} \ No newline at end of file diff --git a/certora/experimental/specs/Aave/FlashLoanReceiver.spec b/certora/experimental/specs/Aave/FlashLoanReceiver.spec new file mode 100644 index 00000000..f85049f4 --- /dev/null +++ b/certora/experimental/specs/Aave/FlashLoanReceiver.spec @@ -0,0 +1,18 @@ +methods { + function _.executeOperation( + address[] assets, + uint256[] amounts, + uint256[] premiums, + address initiator, + bytes params + ) external => NONDET; // expect bool; + + // simple receiver + function _.executeOperation( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes params + ) external => NONDET; // expect bool; +} \ No newline at end of file diff --git a/certora/experimental/specs/Aave/IncentivesControllerForTokens.spec b/certora/experimental/specs/Aave/IncentivesControllerForTokens.spec new file mode 100644 index 00000000..2a5d6026 --- /dev/null +++ b/certora/experimental/specs/Aave/IncentivesControllerForTokens.spec @@ -0,0 +1,4 @@ +methods { + // these summaries require additional rules to be safe + function _.handleAction(address user, uint256 totalSupply, uint256 userBalance) external => NONDET; // it is actually non-view, check no interaction with tokens XXX +} \ No newline at end of file diff --git a/certora/experimental/specs/Aave/Pool.spec b/certora/experimental/specs/Aave/Pool.spec new file mode 100644 index 00000000..87c43567 --- /dev/null +++ b/certora/experimental/specs/Aave/Pool.spec @@ -0,0 +1,466 @@ +import "../ERC20/WETHcvl.spec"; +import "../ERC721/erc721.spec"; +import "../ERC1967/erc1967.spec"; +import "../PriceAggregators/chainlink.spec"; +import "../PriceAggregators/tellor.spec"; + +// aave imports +import "./aToken.spec"; +import "./AddressProvider.spec"; +import "./PriceOracleSentinel.spec"; +import "./PriceOracle.spec"; +import "./ACLManager.spec"; +import "./ReserveInterestRateStrategy.spec"; +import "./FlashLoanReceiver.spec"; + +// standard +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +import "../generic.spec"; // pick additional rules from here + + +//using DummyWeth as weth; + +methods { + function _.ADDRESSES_PROVIDER() external => NONDET; // expect address + + function ReserveLogic.getNormalizedIncome(DataTypes.ReserveData storage reserve) internal returns (uint256); + + function _.calculateInterestRates(DataTypes.CalculateInterestRatesParams params) external + => NONDET; + + // function getIsVirtualAccActive(DataTypes.ReserveConfigurationMap memory self) internal returns (bool) envfree; + + // envfree + // function getReserveDataExtended(address) external returns (DataTypes.ReserveData memory) envfree; // use direct storage access instead +} + +use builtin rule sanity filtered { f -> f.contract == currentContract } + +// Simple rule - depends on better summaries for `IReserveInterestRateStrategy` OR linking it (with the risk of longer running time) +// See in https://prover.certora.com/output/60724/b7043622211340e98e602bd4e46c9aff/?anonymousKey=ff16ebf727d2aa6631a187c862a845a624bf64a5 +// by filtering for `virtualUnderlyingBalance` accesses in the trace +rule user_withdraw_cannot_surpass_VA() { + env e; + uint256 amount; + address to; + address asset; + uint128 virtual_bal_before = currentContract._reserves[asset].virtualUnderlyingBalance; + withdraw(e,asset, amount, to); + assert amount <= assert_uint256(virtual_bal_before); +} + +function isVirtualAccActive(uint256 data) returns bool { + uint mask = 0xEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + return (data & ~mask) != 0; +} + +// for an asset in the reserves, it's aToken's totalSupply is not bigger than the virtualUnderlyingBalance +invariant virtualUnderlyingBalanceLessThanATokenTotalSupply(env e, address asset) + to_mathint(currentContract._reserves[asset].virtualUnderlyingBalance) + <= to_mathint(aTokenTotalSupplyCVL(currentContract._reserves[asset].aTokenAddress, e)) +filtered { f -> f.contract == currentContract } + { + preserved with (env eF) { + init_state(); + require eF.block.timestamp == e.block.timestamp; + require eF.block.number == e.block.number; + // here's the weird one - we initialize the currentLiquidityRate to be liquidityRateModel[0], + // so if the function we call increases liquidity, the currentLiquidityRate will decrease, + // and if the function we call decreases liquidity, the currentLiquidityRate will increase + require require_uint256(currentContract._reserves[asset].currentLiquidityRate) == liquidityRateModel[0]; + // let's also require that we use the virtual balance + require isVirtualAccActive(currentContract._reserves[asset].configuration.data); + requireInvariant reserveAssetsAreATokenUnderlyingsFORALL(); + require asset != 0; // there's no asset in address 0 (we could also define that the aTokenAddress of 0 is 0 and prove it - it should verify xxx) + } + } + +/** + This pair of rules is really the same and should be merged to one definition xxx. + The idea is to use the forall version in requireInvariant, + and the non-forall version for easier debugging of the check (it's a stronger version which is 'pointwise', so proving it also proves the forall). + It currently fails on: + 1. `initReserve` - because initReserve does not check that the provided aToken's underlying is the asset we want. To check if that's an issue or not. + 2. `flashLoan` because I'm not dealing with the havocs it has currently. xxx + */ +invariant reserveAssetsAreATokenUnderlyingsFORALL() + // if `asset` is initialized as a reserve: + forall address asset. currentContract._reserves[asset].aTokenAddress != 0 => + aTokenToUnderlying[currentContract._reserves[asset].aTokenAddress] == asset +filtered { f -> f.contract == currentContract } + +invariant reserveAssetsAreATokenUnderlyings(address asset) + // if `asset` is initialized as a reserve: + currentContract._reserves[asset].aTokenAddress != 0 => + aTokenToUnderlying[currentContract._reserves[asset].aTokenAddress] == asset +filtered { f -> f.contract == currentContract } + +// a legitimate hard rule +invariant supply_gte_debt(env e, address asset) + to_mathint(aTokenTotalSupplyCVL(currentContract._reserves[asset].aTokenAddress, e)) + >= ( + aTokenTotalSupplyCVL(currentContract._reserves[asset].variableDebtTokenAddress, e) // totalVariable + + aTokenTotalSupplyCVL(currentContract._reserves[asset].stableDebtTokenAddress, e) // totalStable + ) +filtered { f -> f.contract == currentContract } +{ + preserved { + init_state(); + } +} + +// exploratory rules +rule changes_virtual_underlying_balance(method f) +filtered { f -> f.contract == currentContract } +{ + address asset; + uint _virtualUnderlyingBalance = currentContract._reserves[asset].virtualUnderlyingBalance; + + env e; + calldataarg arg; + f(e, arg); + + uint virtualUnderlyingBalance_ = currentContract._reserves[asset].virtualUnderlyingBalance; + + satisfy _virtualUnderlyingBalance != virtualUnderlyingBalance_, "Does not update virtual underlying balance"; +} + +rule changes_aToken_supply(method f) +filtered { f -> f.contract == currentContract } +{ + env e; + address asset; + address aToken = currentContract._reserves[asset].aTokenAddress; + uint _aTokenTotalSupply = aTokenTotalSupplyCVL(aToken, e); + + env eF; + require eF.block.timestamp == e.block.timestamp; + require eF.block.number == e.block.number; + calldataarg arg; + f(e, arg); + + uint aTokenTotalSupply_ = aTokenTotalSupplyCVL(aToken, e); + + satisfy _aTokenTotalSupply != aTokenTotalSupply_, "Does not update an aToken's total supply"; +} + +function init_state() { + // based on aTokensAreNotUnderlyings + require forall address a. + a == 0 // nothing-token + || aTokenToUnderlying[a] == 0 // underlying + || aTokenToUnderlying[aTokenToUnderlying[a]] == 0 // aTokens map to underlyings which map to 0 + ; + // aTokens have the AToken sort, VariableDebtTokens have the VariableDebt sort, etc... + require forall address a. tokenToSort[currentContract._reserves[a].aTokenAddress] == AToken_token(); + require forall address a. tokenToSort[currentContract._reserves[a].variableDebtTokenAddress] == VariableDebtToken_token(); + require forall address a. tokenToSort[currentContract._reserves[a].stableDebtTokenAddress] == StableDebtToken_token(); +} + + + + + +rule supply_gte_debt__deposit(env e, address asset) { + init_state(); + + mathint total_supply_Atoken; + mathint total_supply_var; + mathint total_supply_stb; + + address atoken = currentContract._reserves[asset].aTokenAddress; + address debt = currentContract._reserves[asset].variableDebtTokenAddress; + address stb = currentContract._reserves[asset].stableDebtTokenAddress; + + require atoken==10; + require debt==11; + require stb==12; + + require asset==100; + + DataTypes.ReserveData reserves = getReserveDataExtended(e, asset); + // require reserves.lastUpdateTimestamp == require_uint40(e.block.timestamp); + + total_supply_Atoken = to_mathint(aTokenTotalSupplyCVL(atoken, e)); + total_supply_var = to_mathint(aTokenTotalSupplyCVL(debt, e)); + // total_supply_stb = to_mathint(aTokenTotalSupplyCVL(stb, e)); + + require aTokenToUnderlying[atoken]==asset; + require aTokenToUnderlying[debt]==asset; + // require aTokenToUnderlying[stb]==asset; + + require total_supply_Atoken >= total_supply_var //+ total_supply_stb; + ; + // address asset; + uint256 amount; + address onBehalfOf; + uint16 referralCode; + + deposit(e, asset, amount, onBehalfOf, referralCode); + + assert + //to_mathint( + aTokenTotalSupplyCVL(currentContract._reserves[asset].aTokenAddress, e) + //) + >= ( + aTokenTotalSupplyCVL(currentContract._reserves[asset].variableDebtTokenAddress, e) // totalVariable + // + aTokenTotalSupplyCVL(currentContract._reserves[asset].stableDebtTokenAddress, e) // totalStable + ); + +} + + + +rule supply_gte_debt__borrow(env e, address _asset) { + init_state(); + + mathint _total_supply_Atoken; mathint _total_supply_var;//, _total_supply_stb; + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + + require _atoken==10; require _debt==11; require _stb==12; require _asset==100; + require weth!=10 && weth!=11 && weth!=12; + + + DataTypes.ReserveData reserves = getReserveDataExtended(e, _asset); + require reserves.lastUpdateTimestamp == require_uint40(e.block.timestamp); + require reserves.liquidityIndex == reserves.variableBorrowIndex; + require reserves.liquidityIndex == 10^27; + + _total_supply_Atoken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + _total_supply_var = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + // _total_supply_stb = to_mathint(aTokenTotalSupplyCVL(_stb, e)); + + require aTokenToUnderlying[_atoken]==_asset; + require aTokenToUnderlying[_debt]==_asset; + // require aTokenToUnderlying[_stb]==_asset; + + require _total_supply_Atoken >= _total_supply_var; //+ _total_supply_stb; + require _total_supply_Atoken <= 100; + uint256 _amount; + uint256 _interestRateMode; + + require forall address a. balanceByToken[_debt][a] <= totalSupplyByToken[_debt]; + require forall address a. balanceByToken[_atoken][a] <= totalSupplyByToken[_atoken]; + + address onBehalfOf; + uint16 referralCode; + require _interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + bool _is_VA_active = reserves.configuration.data &~0xEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF != 0; + require _is_VA_active == true; + uint128 _virtual_bal_before = getReserveDataExtended(e,_asset).virtualUnderlyingBalance; + require to_mathint(_virtual_bal_before) <= _total_supply_Atoken - _total_supply_var; + + borrow(e, _asset, _amount, _interestRateMode, referralCode, onBehalfOf); + + assert + //to_mathint( + aTokenTotalSupplyCVL(currentContract._reserves[_asset].aTokenAddress, e) + //) + >= ( + aTokenTotalSupplyCVL(currentContract._reserves[_asset].variableDebtTokenAddress, e) // totalVariable + // + aTokenTotalSupplyCVL(currentContract._reserves[_asset].stableDebtTokenAddress, e) // totalStable + ); +} + + + + + + + + +rule virtual_plus_debt_LEQ_supply__borrow(env e, address _asset) { + init_state(); + + mathint __totSUP_aToken; mathint __totSUP_debt; + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + + require forall address a. balanceByToken[_debt][a] <= totalSupplyByToken[_debt]; + require forall address a. balanceByToken[_atoken][a] <= totalSupplyByToken[_atoken]; + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; //require aTokenToUnderlying[_stb]==_asset; + + require _atoken==10; require _debt==11; require _stb==12; require _asset==100; + require weth!=10 && weth!=11 && weth!=12; + + + DataTypes.ReserveData reserve = getReserveDataExtended(e, _asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + require reserve.liquidityIndex == reserve.variableBorrowIndex; + require 10^27 <= reserve.liquidityIndex && reserve.liquidityIndex <= 2*10^27; + // require reserve.currentVariableBorrowRate==reserve.currentLiquidityRate; + + + __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + // __total_supply_stb = to_mathint(aTokenTotalSupplyCVL(_stb, e)); + uint128 __virtual_bal = getReserveDataExtended(e,_asset).virtualUnderlyingBalance; + //THE MAIN REQUIREMENT + require __virtual_bal + __totSUP_debt <= to_mathint(__totSUP_aToken); + + require 40 <= __totSUP_aToken && __totSUP_aToken <= 100; + uint256 _amount; uint256 _interestRateMode; address onBehalfOf; uint16 referralCode; + require _interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + bool _is_VA_active = reserve.configuration.data &~0xEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF != 0; + require _is_VA_active == true; + + // FUNCTION CALL + borrow(e, _asset, _amount, _interestRateMode, referralCode, onBehalfOf); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(e, _asset); + require reserve2.liquidityIndex == reserve2.variableBorrowIndex; + require 10^27 <= reserve2.liquidityIndex && reserve2.liquidityIndex <= 2*10^27; + + + mathint __totSUP_aToken__; mathint __totSUP_debt__; + __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(e,_asset).virtualUnderlyingBalance; + + assert __virtual_bal__ + __totSUP_debt__ <= to_mathint(__totSUP_aToken__)+1; +} + + + + +rule borrowIndex_GEQ_liquidityIndex__borrow(env e, address _asset) { + init_state(); + + mathint __totSUP_aToken; mathint __totSUP_debt; + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + + + require forall address a. balanceByToken[_debt][a] <= totalSupplyByToken[_debt]; + require forall address a. balanceByToken[_atoken][a] <= totalSupplyByToken[_atoken]; + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; //require aTokenToUnderlying[_stb]==_asset; + + require _atoken==10; require _debt==11; require _asset==100; + require weth!=10 && weth!=11; + + + DataTypes.ReserveData reserve = getReserveDataExtended(e, _asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + //THE MAIN REQUIREMENT + require reserve.liquidityIndex <= reserve.variableBorrowIndex; + require 10^27 <= reserve.liquidityIndex && reserve.variableBorrowIndex <= 2*10^27; + // require reserve.currentVariableBorrowRate==reserve.currentLiquidityRate; + + + __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + // __total_supply_stb = to_mathint(aTokenTotalSupplyCVL(_stb, e)); + uint128 __virtual_bal = getReserveDataExtended(e,_asset).virtualUnderlyingBalance; + // require __virtual_bal + __totSUP_debt <= to_mathint(__totSUP_aToken); + + require 40 <= __totSUP_aToken && __totSUP_aToken <= 100; + uint256 _amount; uint256 _interestRateMode; address onBehalfOf; uint16 referralCode; + require _interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + bool _is_VA_active = reserve.configuration.data &~0xEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF != 0; + require _is_VA_active == true; + + // FUNCTION CALL + borrow(e, _asset, _amount, _interestRateMode, referralCode, onBehalfOf); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(e, _asset); + // require reserve2.liquidityIndex == reserve2.variableBorrowIndex; + require 10^27 <= reserve2.liquidityIndex && reserve2.liquidityIndex <= 2*10^27; + + /* + mathint __totSUP_aToken__; mathint __totSUP_debt__; + __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(e,_asset).virtualUnderlyingBalance; + assert __virtual_bal__ + __totSUP_debt__ <= to_mathint(__totSUP_aToken__)+1;*/ + + assert reserve2.liquidityIndex <= reserve2.variableBorrowIndex; +} + + + + + + +rule virtual_plus_debt_EQ_supply_plus_accued__borrow(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + require _atoken==10; require _debt==11; require _stb==12; require _asset==100; + require weth!=10 && weth!=11 && weth!=12; + + require forall address a. balanceByToken[_debt][a] <= totalSupplyByToken[_debt]; + require forall address a. balanceByToken[_atoken][a] <= totalSupplyByToken[_atoken]; + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + + DataTypes.ReserveData reserve = getReserveDataExtended(e, _asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint128 __liqInd_beforeOLD = reserve.liquidityIndex; + uint128 __dbtInd_beforeOLD = reserve.variableBorrowIndex; + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + + require RAY() <= __liqInd_before && __liqInd_before <= __dbtInd_before ; + require assert_uint128(RAY()) <= __liqInd_beforeOLD && __liqInd_beforeOLD <= __dbtInd_beforeOLD; + //require 10^27 <= reserve.liquidityIndex && reserve.liquidityIndex <= 2*10^27; + // require __liqInd_beforeOLD ==assert_uint128(RAY()); require __dbtInd_beforeOLD == assert_uint128(RAY() + RAY()/10); + //require __liqInd_before ==RAY(); require __dbtInd_before == assert_uint256(RAY() + RAY()/10); + + require reserve.currentLiquidityRate <= reserve.currentVariableBorrowRate; + + mathint __totSUP_aToken; mathint __totSUP_debt; mathint __totSUP_stb; + __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + __totSUP_stb = to_mathint(aTokenTotalSupplyCVL(_stb, e)); + require scaledTotalSupplyCVL(_stb)==0; + uint128 __virtual_bal = getReserveDataExtended(e,_asset).virtualUnderlyingBalance; + uint128 __accrued = reserve.accruedToTreasury; + //THE MAIN REQUIREMENT + //require __virtual_bal + __totSUP_debt == + // to_mathint(__totSUP_aToken) + rayMulCVLPrecise(__accrued,reserve.liquidityIndex); + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt; + + require 40 <= __totSUP_aToken && __totSUP_aToken <= 100; + uint256 _amount; uint256 _interestRateMode; address onBehalfOf; uint16 referralCode; + require _interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + bool _is_VA_active = reserve.configuration.data &~0xEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF != 0; + require _is_VA_active == true; + + // FUNCTION CALL + borrow(e, _asset, _amount, _interestRateMode, referralCode, onBehalfOf); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(e, _asset); + //require reserve2.liquidityIndex == reserve2.variableBorrowIndex; + // require 10^27 <= reserve2.liquidityIndex && reserve2.liquidityIndex <= 2*10^27; + uint128 __liqInd_afterOLD = reserve2.liquidityIndex; + uint128 __dbtInd_afterOLD = reserve2.variableBorrowIndex; + uint256 __liqInd_after = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_after = getReserveNormalizedVariableDebt(e, _asset); + + + mathint __totSUP_aToken__; mathint __totSUP_debt__; + __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(e,_asset).virtualUnderlyingBalance; + + //assert __virtual_bal__ + __totSUP_debt__ <= + //to_mathint(__totSUP_aToken__) + rayMulCVLPrecise(__accrued,reserve2.liquidityIndex) + 1; + //assert to_mathint(__totSUP_aToken__) + rayMulCVLPrecise(__accrued,reserve2.liquidityIndex) + // <= __virtual_bal__ + __totSUP_debt__ + 1; + + //PURE SOLVENCY + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + rayDivCVLPrecise(__dbtInd_after,RAY()) ; +} diff --git a/certora/experimental/specs/Aave/PoolSummarizationForTokens.spec b/certora/experimental/specs/Aave/PoolSummarizationForTokens.spec new file mode 100644 index 00000000..acbef625 --- /dev/null +++ b/certora/experimental/specs/Aave/PoolSummarizationForTokens.spec @@ -0,0 +1,44 @@ +// This is to be used in tandem with `PoolInstanceForAToken` and similar, +// as we build them. +methods { + function _.ADDRESSES_PROVIDER() external => NONDET; // expect address + + // Folding an internal function inside `getReserveNormalizedIncome` + // So that we could summarize it in both spec and contract calls + function _.getReserveNormalizedIncomeInt(address asset) internal with (env e) + => computeReserveNormalizedIncome(asset, e) expect uint256; + + function _.getReserveNormalizedIncome(address asset) external with (env e) + => computeReserveNormalizedIncome(asset, e) expect uint256; + + // Same trick for `getReserveNormalizedVariableDebt` + function _.getReserveNormalizedVariableDebtInt(address asset) internal with (env e) + => computeReserveNormalizedVariableDebt(asset, e) expect uint256; + + function _.getReserveNormalizedVariableDebt(address asset) external with (env e) + => computeReserveNormalizedVariableDebt(asset, e) expect uint256; + + // these summaries require additional rules to be safe + function _.finalizeTransfer( + address asset, + address from, + address to, + uint256 amount, + uint256 balanceFromBefore, + uint256 balanceToBefore + ) external => NONDET; // it is actually non-view, check no interaction with tokens XXX +} + +// Probably needs to model also last and current timestamps +ghost mapping(address => uint256) thePoolReservedNormalizedIncome; +function computeReserveNormalizedIncome(address underlyingAsset, env e) returns uint256 { + // xxx need to take timestamp into account + return thePoolReservedNormalizedIncome[underlyingAsset]; +} + +ghost mapping(address => uint256) thePoolReservedNormalizedVariableDebt; +function computeReserveNormalizedVariableDebt(address underlyingAsset, env e) returns uint256 { + // xxx need to take timestamp into account + return thePoolReservedNormalizedVariableDebt[underlyingAsset]; +} + diff --git a/certora/experimental/specs/Aave/PriceOracle.spec b/certora/experimental/specs/Aave/PriceOracle.spec new file mode 100644 index 00000000..b5658537 --- /dev/null +++ b/certora/experimental/specs/Aave/PriceOracle.spec @@ -0,0 +1,3 @@ +methods { + function _.getAssetPrice(address asset) external => NONDET; // expect uint256; +} \ No newline at end of file diff --git a/certora/experimental/specs/Aave/PriceOracleSentinel.spec b/certora/experimental/specs/Aave/PriceOracleSentinel.spec new file mode 100644 index 00000000..47b0a175 --- /dev/null +++ b/certora/experimental/specs/Aave/PriceOracleSentinel.spec @@ -0,0 +1,5 @@ +methods { + // no side effects + function _.isBorrowAllowed() external => NONDET; // expect bool; + function _.isLiquidationAllowed() external => NONDET; // expect bool; +} \ No newline at end of file diff --git a/certora/experimental/specs/Aave/ReserveInterestRateStrategy.spec b/certora/experimental/specs/Aave/ReserveInterestRateStrategy.spec new file mode 100644 index 00000000..5284cc33 --- /dev/null +++ b/certora/experimental/specs/Aave/ReserveInterestRateStrategy.spec @@ -0,0 +1,24 @@ +methods { + // function _.calculateInterestRates( + // DataTypes.CalculateInterestRatesParams params + // ) external => calculateInterestRatesCVL(calledContract, params) expect (uint256, uint256, uint256); // marked view +} + +ghost mapping(mathint /* liquidityDelta */ => uint256) liquidityRateModel { + // monotone-decreasing, see [checkNextLiquidityRateChangeWhenLiquidityAddedOrTakenChangesLe] + axiom forall mathint n. forall mathint m. n >= m => liquidityRateModel[n] <= liquidityRateModel[m]; +} + +function calculateInterestRatesCVL( + address interestRateStrategy, // redundancy + DataTypes.CalculateInterestRatesParams params +) returns (uint256, uint256, uint256) { + uint256 liquidityRate = liquidityRateModel[params.liquidityAdded - params.liquidityTaken]; + uint256 stableBorrowRate; + uint256 variableBorrowRate; + + require (params.usingVirtualBalance && params.totalStableDebt + params.totalVariableDebt != 0) + => params.liquidityTaken <= require_uint256(params.virtualUnderlyingBalance + params.liquidityAdded); + + return (liquidityRate, stableBorrowRate, variableBorrowRate); +} diff --git a/certora/experimental/specs/Aave/ReserveInterestRateStrategyCheck.spec b/certora/experimental/specs/Aave/ReserveInterestRateStrategyCheck.spec new file mode 100644 index 00000000..157c77f9 --- /dev/null +++ b/certora/experimental/specs/Aave/ReserveInterestRateStrategyCheck.spec @@ -0,0 +1,268 @@ +import "../Math/CVLMath.spec"; + +// current contract is assumed to be an IReserveInterestRateStrategy +methods { + function calculateInterestRates( + DataTypes.CalculateInterestRatesParams params + ) external; // view function + + // optimizations + function _.wadToRay(uint256 a) internal => wadToRayCVL(a) expect uint256; // this is optimized well actually + function _.rayMul(uint256 a, uint256 b) internal => rayMulCVLPrecise(a, b) expect uint256; // not optimized well by Prover + function _.rayDiv(uint256 a, uint256 b) internal => rayDivCVLPrecise(a, b) expect uint256; // seems to be optimized well by Prover + function _.percentMul(uint256 value, uint256 percentage) internal => percentMulPrecise(value, percentage) expect uint256; // to use require_uint256 instead of inline assembly +} + +definition WAD_RAY_RATIO() returns uint256 = /* 1e9 */ 1000000000; + +function wadToRayCVL(uint256 a) returns uint256 { + return require_uint256(a * WAD_RAY_RATIO()); +} + + +definition PERCENTAGE_FACTOR() returns uint256 = /* 1e4 */ 10000; +definition HALF_PERCENTAGE_FACTOR() returns uint256 = /* 0.5e4 */ 5000; + +function percentMulPrecise(uint256 value, uint256 percentage) returns uint256 { + return require_uint256((value*percentage + HALF_PERCENTAGE_FACTOR())/ PERCENTAGE_FACTOR()); +} + + +rule checkCalculateInterestRatesCVLSummary { + env e; + DataTypes.CalculateInterestRatesParams params; + calculateInterestRates(e, params); + assert (params.usingVirtualBalance && params.totalStableDebt + params.totalVariableDebt != 0) + => params.liquidityTaken <= assert_uint256(params.virtualUnderlyingBalance + params.liquidityAdded); +} + +// exploratory rule section +function checkNextLiquidityRateEffects( + DataTypes.CalculateInterestRatesParams params1, + DataTypes.CalculateInterestRatesParams params2 +) returns (uint256, uint256) { + env e; + + uint256 nextLiquidityRate1; + uint256 nextStableRate1; + uint256 nextVariableRate1; + nextLiquidityRate1, nextStableRate1, nextVariableRate1 = calculateInterestRates(e, params1); + uint256 nextLiquidityRate2; + uint256 nextStableRate2; + uint256 nextVariableRate2; + nextLiquidityRate2, nextStableRate2, nextVariableRate2 = calculateInterestRates(e, params2); + + return (nextLiquidityRate1, nextLiquidityRate2); +} + +function LeTotalVariableDebt( + DataTypes.CalculateInterestRatesParams params1, + DataTypes.CalculateInterestRatesParams params2 +) { + require + params1.unbacked == params2.unbacked + && params1.liquidityAdded == params2.liquidityAdded + && params1.liquidityTaken == params2.liquidityTaken + && params1.totalStableDebt == params2.totalStableDebt + && params1.totalVariableDebt <= params2.totalVariableDebt + && params1.averageStableBorrowRate == params2.averageStableBorrowRate + && params1.reserveFactor == params2.reserveFactor + && params1.reserve == params2.reserve + && params1.usingVirtualBalance == params2.usingVirtualBalance + && params1.virtualUnderlyingBalance == params2.virtualUnderlyingBalance + ; +} + +function GeTotalVariableDebt( + DataTypes.CalculateInterestRatesParams params1, + DataTypes.CalculateInterestRatesParams params2 +) { + require + params1.unbacked == params2.unbacked + && params1.liquidityAdded == params2.liquidityAdded + && params1.liquidityTaken == params2.liquidityTaken + && params1.totalStableDebt == params2.totalStableDebt + && params1.totalVariableDebt >= params2.totalVariableDebt + && params1.averageStableBorrowRate == params2.averageStableBorrowRate + && params1.reserveFactor == params2.reserveFactor + && params1.reserve == params2.reserve + && params1.usingVirtualBalance == params2.usingVirtualBalance + && params1.virtualUnderlyingBalance == params2.virtualUnderlyingBalance + ; +} + +function GeLiquidity( + DataTypes.CalculateInterestRatesParams params1, + DataTypes.CalculateInterestRatesParams params2 +) { + // if liquidity added is increased and liquidity taken is decreased then there's _more_ available liquidity + // which decreases the borrow rate, thus decreasing the liquidity rate + require + params1.unbacked == params2.unbacked + && params1.liquidityAdded >= params2.liquidityAdded + && params1.liquidityTaken <= params2.liquidityTaken + && params1.totalStableDebt == params2.totalStableDebt + && params1.totalVariableDebt == params2.totalVariableDebt + && params1.averageStableBorrowRate == params2.averageStableBorrowRate + && params1.reserveFactor == params2.reserveFactor + && params1.reserve == params2.reserve + && params1.usingVirtualBalance == params2.usingVirtualBalance + && params1.virtualUnderlyingBalance == params2.virtualUnderlyingBalance + ; +} + +// wrong +rule checkNextLiquidityRateChangeWhenTotalVariableDebtDecreasesGe() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + LeTotalVariableDebt(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 >= nextLiquidityRate2, "params1 resulted in >= nextLiquidityRate"; +} + +// wrong +rule checkNextLiquidityRateChangeWhenTotalVariableDebtDecreasesLe() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + LeTotalVariableDebt(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 <= nextLiquidityRate2, "params1 resulted in >= nextLiquidityRate"; +} + +// wrong +rule checkNextLiquidityRateChangeWhenTotalVariableDebtDecreasesEq() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + LeTotalVariableDebt(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 == nextLiquidityRate2, "params1 resulted in >= nextLiquidityRate"; +} + +// wrong +rule checkNextLiquidityRateChangeWhenTotalVariableDebtIncreasesGe() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + GeTotalVariableDebt(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 >= nextLiquidityRate2, "params1 resulted in >= nextLiquidityRate"; +} + +// wrong +rule checkNextLiquidityRateChangeWhenTotalVariableDebtIncreasesLe() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + GeTotalVariableDebt(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 <= nextLiquidityRate2, "params1 resulted in <= nextLiquidityRate"; +} + +// wrong +rule checkNextLiquidityRateChangeWhenLiquidityAddedOrTakenChangesGe() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + GeLiquidity(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 >= nextLiquidityRate2, "params1 resulted in >= nextLiquidityRate"; +} + +// see [GeLiquidity] - I'd expect this to be true - liquidity increased in params1 -> nextLiquidityRate1 is smaller +rule checkNextLiquidityRateChangeWhenLiquidityAddedOrTakenChangesLe() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + GeLiquidity(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 <= nextLiquidityRate2, "params1 resulted in <= nextLiquidityRate"; +} + +// wrong +rule checkNextLiquidityRateChangeWhenLiquidityAddedOrTakenChangesEq() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + GeLiquidity(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 == nextLiquidityRate2, "params1 resulted in == nextLiquidityRate"; +} + +// sanity +//use builtin rule sanity filtered { f -> f.contract == currentContract } + + + + + + +// wrong +rule checkNextLiquidityRateChangeWhenTotalVariableDebtIncreasesEq() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + GeTotalVariableDebt(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 >= nextLiquidityRate2, "params1 resulted in == nextLiquidityRate"; +} + + + +rule borrowRate_GEQ_liquidityRate(env e) { + DataTypes.CalculateInterestRatesParams params; + + require params.totalStableDebt == 0; + require params.averageStableBorrowRate == 0; + // require params.unbacked==0; + // require params.totalVariableDebt == 1; + //require params.virtualUnderlyingBalance == 1; + require params.reserveFactor == 500; // 5% + + uint256 liquidity_rate; + uint256 nextStableRate1; + uint256 variable_rate; + liquidity_rate, nextStableRate1, variable_rate = calculateInterestRates(e, params); + + // require get___vars_availableLiquidityPlusDebt() > 1; + + assert liquidity_rate <= variable_rate; +} + + +rule _getOverallBorrowRate_with_0_stable(env e) { + // uint256 totalStableDebt; + uint256 _totalVariableDebt; + uint256 _currentVariableBorrowRate; + uint256 _currentAverageStableBorrowRate; + + require 10^23 <= _currentVariableBorrowRate && _currentVariableBorrowRate <= 10^27; + require _totalVariableDebt * 10^9 >= 10^27 / 2; + + uint256 _ret_val = _getOverallBorrowRate(e, + 0, // no stable debt + _totalVariableDebt, + _currentVariableBorrowRate, + _currentAverageStableBorrowRate); + + assert _ret_val == _currentVariableBorrowRate; + +} + + + +/* +0x27abba297a2388bdb0599e976abf +0x27abba297a2389cfe04891b00000 +*/ diff --git a/certora/experimental/specs/Aave/VariableDebtTokenCheck.spec b/certora/experimental/specs/Aave/VariableDebtTokenCheck.spec new file mode 100644 index 00000000..88d657c7 --- /dev/null +++ b/certora/experimental/specs/Aave/VariableDebtTokenCheck.spec @@ -0,0 +1,145 @@ +import "./aToken.spec"; +import "./PoolSummarizationForTokens.spec"; +import "./IncentivesControllerForTokens.spec"; + +import "../generic.spec"; +import "./tokenCheckBase.spec"; + +using VariableDebtTokenInstance as aToken; +using Utilities as utils; + +methods { + // envfree declarations + function utils.nop() external envfree; + function aToken.scaledBalanceOf(address) external returns uint256 envfree; + function aToken.scaledTotalSupply() external returns uint256 envfree; + function aToken.allowance(address,address) external returns uint256 envfree; + // note that balanceOf and totalSupply are not envfree because the calls to the pool are in fact not envfree, + // but our current envfree checks are not precise enough for this +} + +function not_implemented() { + assert false, "Not implemented"; +} + +function dont_care() { + require false; // to be used for methods we don't model in the summary version anyway +} + +definition initialize_method_sig() returns uint32 = sig:initialize(address,address,address,uint8,string,string,bytes).selector; + +// xxx currently missing return value equivalence checks +function run_parametric_with_cvl_equivalent(method f, env e) { + // note there's a strong assumption the side effects of CVL and Solidity versions are disjoint + if (f.selector == sig:transfer(address,uint256).selector) { + address a1; + uint256 u1; + aTokenTransferCVL(aToken, a1, u1, e); + aToken.transfer(e, a1, u1); + } else if (f.selector == sig:transferFrom(address,address,uint256).selector) { + address a1; + address a2; + uint256 u1; + // must have the same allowance at the beginning to have the same effect + require allowanceByToken[aToken][a1][e.msg.sender] == aToken.allowance(e, a1, e.msg.sender); + aTokenTransferFromCVL(aToken, a1, a2, u1, e); + aToken.transferFrom(e, a1, a2, u1); + } else if (f.selector == sig:approve(address,uint256).selector) { + address a1; + uint256 u1; + approveCVL(aToken, e.msg.sender, a1, u1); + aToken.approve(e, a1, u1); + } else if (f.selector == sig:decreaseAllowance(address,uint256).selector) { + address a1; + uint256 u1; + decreaseAllowanceCVL(aToken, e.msg.sender, a1, u1); + aToken.decreaseAllowance(e, a1, u1); + } else if (f.selector == sig:increaseAllowance(address,uint256).selector) { + address a1; + uint256 u1; + increaseAllowanceCVL(aToken, e.msg.sender, a1, u1); + aToken.increaseAllowance(e, a1, u1); + } else if (f.selector == sig:mint(address,address,uint256,uint256).selector) { + address a1; + address a2; + uint256 u1; + uint256 u2; + aTokenMintCVL(aToken, a1, a2, u1, u2); + aToken.mint(e, a1, a2, u1, u2); + } else if (f.selector == sig:burn(address,uint256,uint256).selector) { + address a1; + uint256 u1; + uint256 u2; + variableDebtBurnCVL(aToken, a1, u1, u2); + aToken.burn(e, a1, u1, u2); + } else if (f.selector == sig:setIncentivesController(address).selector) { + address a1; + // no CVL implementation, so nop + aToken.setIncentivesController(e, a1); + } else if (f.selector == initialize_method_sig()) { + // we're running all of our equivalence rules on an 'initialized' state of the AToken + dont_care(); + } else if (f.selector == sig:approveDelegation(address,uint256).selector) { + address a1; + uint256 u1; + // no CVL implementation, so nop + aToken.approveDelegation(e, a1, u1); // sol implementation to see if it's nop-equivalent for our purposes + } else if (f.selector == sig:delegationWithSig(address,address,uint256,uint256,uint8,bytes32,bytes32).selector) { + address a1; + address a2; + uint256 u1; + uint256 u2; + uint8 u3; + bytes32 b1; + bytes32 b2; + // no CVL implementation, so nop + aToken.delegationWithSig(e, a1, a2, u1, u2, u3, b1, b2); + } else { + not_implemented(); + } +} + +hook Sstore _underlyingAsset address newValue { + aTokenToUnderlying[aToken] = newValue; +} + +invariant aTokenUnderlyingMatchesGhost() + aToken._underlyingAsset == aTokenToUnderlying[aToken] + filtered { f -> f.contract == currentContract } + + +// some methods are not implemented, let's find them so we could omit them from other rules +definition unimplemented(method f) returns bool = + f.selector == sig:allowance(address,address).selector + || f.selector == sig:approve(address,uint256).selector + || f.selector == sig:decreaseAllowance(address,uint256).selector + || f.selector == sig:increaseAllowance(address,uint256).selector + || f.selector == sig:transfer(address,uint256).selector + || f.selector == sig:transferFrom(address,address,uint256).selector +; + +use rule alwaysRevert filtered { f -> + f.contract == currentContract + && unimplemented(f) +} + +use builtin rule sanity filtered { f -> f.contract == currentContract && !unimplemented(f) } + +function init_state_invariants() { + requireInvariant aTokensAreNotUnderlyings(); + requireInvariant aTokenUnderlyingMatchesGhost(); + require currentATokenHasAnUnderlying(); // see definition for justification + require tokenToSort[aToken] == VariableDebtToken_token(); + require forall address a. tokenToSort[a] < 3; // ignore stable tokens for now... xxx +} + + +use rule balanceOfEquivalence; +use rule scaledBalanceOfEquivalence; +use rule totalSupplyEquivalence; +use rule scaledTotalSupplyEquivalence; +use rule allowanceEquivalence; +use rule listOtherViewFunctions; +use invariant aTokensAreNotUnderlyings; +use rule currentATokenHasAnUnderlyingAfterInitiailization; +use rule aTokenWithoutUnderlyingIsERC20AfterInitialization; \ No newline at end of file diff --git a/certora/experimental/specs/Aave/aToken.spec b/certora/experimental/specs/Aave/aToken.spec new file mode 100644 index 00000000..7fcafb38 --- /dev/null +++ b/certora/experimental/specs/Aave/aToken.spec @@ -0,0 +1,267 @@ +// decide whether we want to summarize here for VariableDebtToken and StableDebtToken too, as interfaces are similar and so are some of the implementations. + +import "../ERC20/erc20cvlForAave.spec"; +import "./AaveMath.spec"; + +using PoolInstanceHarness as poolInstance; +//using PoolInstanceHarness as poolInstance; + +methods { + /* AToken methods */ + // no side effects: + function _.scaledTotalSupply() external => scaledTotalSupplyCVL(calledContract) expect uint256; + function _.scaledBalanceOf(address user) external => scaledBalanceOfCVL(calledContract, user) expect uint256; + function _.balanceOf(address user) external with (env e) => aTokenBalanceOfCVL(calledContract, user, e) expect uint256; + function _.totalSupply() external with (env e) => aTokenTotalSupplyCVL(calledContract, e) expect uint256; + + // addresses + function _.POOL() external => thePool expect address; + function _.RESERVE_TREASURY_ADDRESS() external => theTreasury expect address; + + // StableDebt only: + function _.getSupplyData() external => NONDET; // expect (uint256, uint256, uint256, uint40); + + // with side effects: + function _.transfer(address to, uint256 amount) external with (env e) => aTokenTransferCVL(calledContract, to, amount, e) expect bool; + function _.transferFrom(address from, address to, uint256 amount) external with (env e) => aTokenTransferFromCVL(calledContract, from, to, amount, e) expect bool; + + // matches for AToken, VariableDebtToken and StableDebtToken + function _.mint( // xxx note that VariableDebtToken expected to return bool, uint256; and StabledDebtToken expected to return bool, uint256, uint256; and aTokens are returning just bool + address caller, + address onBehalfOf, + uint256 amount, + uint256 index + ) external => aTokenMintCVL(calledContract, caller, onBehalfOf, amount, index) expect bool, uint, uint; + + // matches AToken only + function _.burn( + address from, + address receiverOfUnderlying, + uint256 amount, + uint256 index + ) external => aTokenBurnCVL(calledContract, from, receiverOfUnderlying, amount, index) expect void; + + function _.transferUnderlyingTo(address target, uint256 amount) external => aTokenTransferUnderlyingToCVL(calledContract, target, amount) expect void; + + function _.mintToTreasury(uint256 amount, uint256 index) external => aTokenMintToTreasuryCVL(calledContract, amount, index) expect void; + + function _.transferOnLiquidation(address from, address to, uint256 value) external with (env e) => aTokenTransferOnLiquidationCVL(calledContract, from, to, value, e) expect void; + + function _.handleRepayment(address user, address onBehalfOf, uint256 amount) external => aTokenHandleRepaymentCVL(calledContract, user, onBehalfOf, amount) expect void; + + function _.rescueTokens(address token, address to, uint256 amount) external with (env e) => aTokenRescueTokensCVL(calledContract, token, to, amount, e) expect void; + + // matches VariableDebtToken only + function _.burn(address from, uint256 amount, uint256 index) external => variableDebtBurnCVL(calledContract, from, amount, index) expect uint256; + + // matches StableDebtToken only + function _.burn(address from, uint256 amount) external => stableDebtBurnCVL(calledContract, from, amount) expect (uint256, uint256); + + // Side effects we don't care about + function _._setName(string memory) internal => NONDET; + function _._setSymbol(string memory) internal => NONDET; + // Getters with loops + // Pending Johannes' PR? + // function _.name() internal => nameCVL expect string; + // function _.symbol() internal => symbolCVL expect string; +} + +// Pending Johannes' PR? +// ghost string nameCVL; +// ghost string symbolCVL; + +// Pool address - same for all aTokens +// ASSUMES WE HAVE THE POOL IN THE SCENE +persistent ghost address thePool { + axiom thePool == poolInstance; +} + +// Treasury address - same for all aTokens +persistent ghost address theTreasury { + init_state axiom theTreasury == 0; +} + +/// aToken => scaledTotalSupply +// this is just totalSupplyByToken + +/// aToken => account => scaledBalance +// this is just balanceByToken + +/// aToken => underlying +/// We'd like to prove that aTokens never map to aTokens, e.g. +/// forall address aToken. aToken == 0 || aTokenToUnderlying[aToken] == 0 || aTokenToUnderlying[aTokenToUnderlying[aToken]] == 0 +persistent ghost mapping(address => address) aTokenToUnderlying { + init_state axiom forall address a. aTokenToUnderlying[a] == 0; +} + +// xxx can we use a sort instead? +definition VanillaERC20_token() returns mathint = 0; +definition AToken_token() returns mathint = 1; +definition VariableDebtToken_token() returns mathint = 2; +definition StableDebtToken_token() returns mathint = 3; + +persistent ghost mapping (address => mathint) tokenToSort { + axiom forall address a. 0 <= tokenToSort[a] && tokenToSort[a] <= 3; +} + +function scaledTotalSupplyCVL(address token) returns uint256 { + return require_uint256(totalSupplyByToken[token]); +} + +function scaledBalanceOfCVL(address token, address user) returns uint256 { + // return require_uint256(balanceByToken[token][user]); + return balanceByToken[token][user]; +} + +function indexForToken(address token, env e) returns uint256 { + uint index; + mathint tokenSort = tokenToSort[token]; + if (tokenSort == AToken_token()) { + index = poolInstance.getReserveNormalizedIncome(e, aTokenToUnderlying[token]); + } else if (tokenSort == VariableDebtToken_token()) { + index = poolInstance.getReserveNormalizedVariableDebt(e, aTokenToUnderlying[token]); + // } else if (tokenSort == StableDebtToken_token()) { + // seems disabled, so just return index=0 and then balanceOf/totalSupply will be 0... + // index = 0; + } else { + index = 0; + assert false, "unsupported token type"; + } + return index; +} + +// todo: adjust for stable debt token +function aTokenBalanceOfCVL(address token, address user, env e) returns uint256 { + uint storedBalance = balanceOfCVL(token, user); + if (aTokenToUnderlying[token] == 0) { + // not a properly initialized aToken, return the regular ERC20 balance + return storedBalance; + } + // hopefully this is the only place we actually call the pool + uint index = indexForToken(token, e); + uint ret = rayMulCVLPrecise(storedBalance, index); + return ret; +} + +persistent ghost bool PERFORM_INTERNAL_CHECK { + axiom PERFORM_INTERNAL_CHECK == true; +} + +// todo: adjust for stableDebtToken which has a completely different implementation +// and make sure to handle for variableDebtToken (same implementation based on underlying index) +function aTokenTotalSupplyCVL(address token, env e) returns uint256 { + uint storedTotalSupply = totalSupplyCVL(token); + if (aTokenToUnderlying[token] == 0) { + if (PERFORM_INTERNAL_CHECK) + assert false; // If we reach here - we have a bug in the spec ! + // not a properly initialized aToken, return the regular ERC20 totalSupply + return storedTotalSupply; + } + // hopefully this is the only place we actually call the pool + uint index = indexForToken(token, e); + uint ret = rayMulCVLPrecise(storedTotalSupply, index); + return ret; +} + +// xxx for VariableDebtToken and StableDebtToken, all transfer functions are disabled +// so need to have reverting summaries for this +function aTokenTransferCVL(address token, address to, uint256 amount, env e) returns bool { + aTokenTransferCVLInternal(token, e.msg.sender, to, amount, e); + return true; +} + +function aTokenTransferCVLInternal(address token, address from, address to, uint256 amount, env e) { + address underlying = aTokenToUnderlying[token]; + if (underlying == 0) { + // not a properly initialized aToken, use the regular ERC20 transfer + transferCVL(token, from, to, amount); + } else { + // based on AToken.sol + uint index = poolInstance.getReserveNormalizedIncome(e, underlying); + uint scaledAmount = rayDivCVLPrecise(amount, index); + transferCVL(token, from, to, scaledAmount); + // no call to POOL.finalizeTransfer. + } +} + +function aTokenTransferFromCVL(address token, address from, address to, uint256 amount, env e) returns bool { + address spender = e.msg.sender; + // copied from erc20cvl.spec: + if (allowanceByToken[token][from][spender] < amount) return false; + allowanceByToken[token][from][spender] = assert_uint256(allowanceByToken[token][from][spender] - amount); + // custom part: + aTokenTransferCVLInternal(token, from, to, amount, e); + return true; +} + +// mint in AToken: scaled erc20 minut + update user index. +// xxx mint in VariableDebtToken: decrease borrow allowance + same as AToken +// xxx mint in StableDebtToken: found no implementation? +function aTokenMintCVL(address token, address from, address to, uint amount, uint index) returns (bool, uint, uint) { + bool ret = balanceByToken[token][to] == 0; + + uint scaledAmount = rayDivCVLPrecise(amount, index); + balanceByToken[token][to] = require_uint256(balanceByToken[token][to] + scaledAmount); + totalSupplyByToken[token] = require_uint256(totalSupplyByToken[token] + scaledAmount); + + uint nondet; // for StableDebtToken + return (ret, totalSupplyByToken[token], nondet); +} + +// burn in AToken: scaled erc20 burn + update user index + in the underlying, transfer amount to the receiver +function aTokenBurnCVL(address token, address from, address receiverOfUnderlying, uint amount, uint index) { + // based on AToken.sol + uint scaledAmount = rayDivCVLPrecise(amount, index); // amount / index + balanceByToken[token][from] = require_uint256(balanceByToken[token][from] - scaledAmount); + totalSupplyByToken[token] = require_uint256(totalSupplyByToken[token] - scaledAmount); + if (token != receiverOfUnderlying) { + transferCVL(aTokenToUnderlying[token], from, receiverOfUnderlying, amount); + } + + // uint nondet; // for StableDebtToken + // return (require_uint256(totalSupplyByToken[token]), nondet); +} + +function aTokenTransferUnderlyingToCVL(address token, address target, uint amount) { + transferCVL(aTokenToUnderlying[token], token, target, amount); +} + +function aTokenMintToTreasuryCVL(address token, uint amount, uint index) { + // based on AToken.sol + if (amount == 0) { + return; + } + + aTokenMintCVL(token, thePool, theTreasury, amount, index); +} + +function aTokenTransferOnLiquidationCVL(address token, address from, address to, uint value, env e) { + aTokenTransferCVLInternal(token, from, to, value, e); +} + +function aTokenHandleRepaymentCVL(address token, address user, address onBehalfOf, uint amount) { + // In AToken.sol, does nothing. +} + +function aTokenRescueTokensCVL(address tokenCalled, address tokenToRescue, address to, uint256 amount, env e) { + require tokenToRescue != aTokenToUnderlying[tokenCalled]; + // if the tokenToRescue is an AToken, we should make sure to call the variant that checks + // if it's an AToken, and if it is, runs the right transfer function. This is okay since in + // the code of `rescueTokens`, we cast the specified token to an IERC20, so there are no + // internal-call shenaningans + // the env with which the transfer is called is where tokenCalled (our atoken) is the sender, but I'm not sure it matters + aTokenTransferCVLInternal(tokenToRescue, tokenCalled /* from */, to, amount, e); +} + +function variableDebtBurnCVL(address token, address from, uint amount, uint index) returns uint { + // based on VariableDebtToken.sol + aTokenBurnCVL(token, from, 0 /* receiver of underlying */, amount, index); + return require_uint256(totalSupplyByToken[token]); +} + +function stableDebtBurnCVL(address token, address from, uint amount) returns (uint, uint) { + // no implementation found? + uint nondet1; + uint nondet2; + return (nondet1, nondet2); +} diff --git a/certora/experimental/specs/Aave/aTokenCheck.spec b/certora/experimental/specs/Aave/aTokenCheck.spec new file mode 100644 index 00000000..14eccc24 --- /dev/null +++ b/certora/experimental/specs/Aave/aTokenCheck.spec @@ -0,0 +1,176 @@ +import "./aToken.spec"; +import "./PoolSummarizationForTokens.spec"; +import "./IncentivesControllerForTokens.spec"; + +import "./tokenCheckBase.spec"; + +using ATokenInstance as aToken; +using Utilities as utils; + +methods { + // Note/Warning: Must _not summarize_ the super call to balanceOf! Only the top-level in AToken. Same with totalSupply. This will lead to 'empty' verification. + + // envfree declarations + function utils.nop() external envfree; + function aToken.scaledBalanceOf(address) external returns uint256 envfree; + function aToken.scaledTotalSupply() external returns uint256 envfree; + function aToken.allowance(address,address) external returns uint256 envfree; + // note that balanceOf and totalSupply are not envfree because the calls to the pool are in fact not envfree. +} + +function not_implemented() { + assert false, "Not implemented"; +} + +function dont_care() { + require false; // to be used for methods we don't model in the summary version anyway +} + +definition initialize_method_sig() returns uint32 = sig:initialize(address,address,address,address,uint8,string,string,bytes).selector; + +// xxx currently missing return value equivalence checks +function run_parametric_with_cvl_equivalent(method f, env e) { + // note there's a strong assumption the side effects of CVL and Solidity versions are disjoint + if (f.selector == sig:transfer(address,uint256).selector) { + address a1; + uint256 u1; + aTokenTransferCVL(aToken, a1, u1, e); + aToken.transfer(e, a1, u1); + } else if (f.selector == sig:transferFrom(address,address,uint256).selector) { + address a1; + address a2; + uint256 u1; + // must have the same allowance at the beginning to have the same effect + require allowanceByToken[aToken][a1][e.msg.sender] == aToken.allowance(e, a1, e.msg.sender); + aTokenTransferFromCVL(aToken, a1, a2, u1, e); + aToken.transferFrom(e, a1, a2, u1); + } else if (f.selector == sig:transferOnLiquidation(address,address,uint256).selector) { + address a1; + address a2; + uint256 u1; + aTokenTransferOnLiquidationCVL(aToken, a1, a2, u1, e); + aToken.transferOnLiquidation(e, a1, a2, u1); + } else if (f.selector == sig:transferUnderlyingTo(address,uint256).selector) { + address a1; + uint256 u1; + aTokenTransferUnderlyingToCVL(aToken, a1, u1); + aToken.transferUnderlyingTo(e, a1, u1); + } else if (f.selector == sig:permit(address,address,uint256,uint256,uint8,bytes32,bytes32).selector) { + address a1; + address a2; + uint256 u1; + uint256 u2; + uint8 u3; + bytes32 by1; + bytes32 by2; + permitCVL(aToken, a1, a2, u1, u2, u3, by1, by2); + aToken.permit(e, a1, a2, u1, u2, u3, by1, by2); + } else if (f.selector == sig:approve(address,uint256).selector) { + address a1; + uint256 u1; + approveCVL(aToken, e.msg.sender, a1, u1); + aToken.approve(e, a1, u1); + } else if (f.selector == sig:decreaseAllowance(address,uint256).selector) { + address a1; + uint256 u1; + decreaseAllowanceCVL(aToken, e.msg.sender, a1, u1); + aToken.decreaseAllowance(e, a1, u1); + } else if (f.selector == sig:increaseAllowance(address,uint256).selector) { + address a1; + uint256 u1; + increaseAllowanceCVL(aToken, e.msg.sender, a1, u1); + aToken.increaseAllowance(e, a1, u1); + } else if (f.selector == sig:mint(address,address,uint256,uint256).selector) { + address a1; + address a2; + uint256 u1; + uint256 u2; + aTokenMintCVL(aToken, a1, a2, u1, u2); + aToken.mint(e, a1, a2, u1, u2); + } else if (f.selector == sig:mintToTreasury(uint256,uint256).selector) { + uint256 u1; + uint256 u2; + aTokenMintToTreasuryCVL(aToken, u1, u2); + aToken.mintToTreasury(e, u1, u2); + } else if (f.selector == sig:burn(address,address,uint256,uint256).selector) { + address a1; + address a2; + uint256 u1; + uint256 u2; + aTokenBurnCVL(aToken, a1, a2, u1, u2); + aToken.burn(e, a1, a2, u1, u2); + } else if (f.selector == sig:handleRepayment(address,address,uint256).selector) { + // it's literally a no-op + address a1; + address a2; + uint256 u1; + aTokenHandleRepaymentCVL(aToken, a1, a2, u1); + aToken.handleRepayment(e, a1, a2, u1); + } else if (f.selector == initialize_method_sig()) { + // we're running all of our equivalence rules on an 'initialized' state of the AToken + dont_care(); + } else if (f.selector == sig:rescueTokens(address,address,uint256).selector) { + // there is no idempotency here (the summary will just simulate whatever is linked-to by rescueTokens, + // in case the rescued token is an AToken), so rules may fail. + // We will prove equivalence in an alternative way, see rescueTokenEquivalence + dont_care(); + } else if (f.selector == sig:setIncentivesController(address).selector) { + address a1; + // no CVL implementation, so nop + aToken.setIncentivesController(e, a1); + } else { + not_implemented(); + } +} + +rule rescueTokenEquivalence() { + env e; + address a1; + address a2; + uint256 u1; + storage init = lastStorage; + aTokenRescueTokensCVL(aToken, a1, a2, u1, e); + utils.nop(); // to properly set lastStorage, which is only updated in solidity calls + storage afterCVL = lastStorage; + aToken.rescueTokens(e, a1, a2, u1) at init; + storage afterSol = lastStorage; + assert afterCVL == afterSol; +} + +hook Sstore _underlyingAsset address newValue { + aTokenToUnderlying[aToken] = newValue; +} + +hook Sstore _treasury address newValue { + theTreasury = newValue; +} + +invariant aTokenUnderlyingMatchesGhost() + aToken._underlyingAsset == aTokenToUnderlying[aToken] + filtered { f -> f.contract == currentContract } + +invariant aTokenTreasuryMatchesGhost() + aToken._treasury == theTreasury + filtered { f -> f.contract == currentContract } + +use builtin rule sanity filtered { f -> f.contract == currentContract } + +function init_state_invariants() { + requireInvariant aTokensAreNotUnderlyings(); + requireInvariant aTokenUnderlyingMatchesGhost(); + requireInvariant aTokenTreasuryMatchesGhost(); + require currentATokenHasAnUnderlying(); // see definition for justification + require tokenToSort[aToken] == AToken_token(); +} + + +use rule balanceOfEquivalence; +use rule scaledBalanceOfEquivalence; +use rule totalSupplyEquivalence; +use rule scaledTotalSupplyEquivalence; +use rule allowanceEquivalence; +use rule listOtherViewFunctions; +use invariant aTokensAreNotUnderlyings; +use rule currentATokenHasAnUnderlyingAfterInitiailization; +use rule aTokenWithoutUnderlyingIsERC20AfterInitialization; + diff --git a/certora/experimental/specs/Aave/cumulateToLiquidityIndexCVL-check.spec b/certora/experimental/specs/Aave/cumulateToLiquidityIndexCVL-check.spec new file mode 100644 index 00000000..1037f540 --- /dev/null +++ b/certora/experimental/specs/Aave/cumulateToLiquidityIndexCVL-check.spec @@ -0,0 +1,82 @@ +import "../ERC20/WETHcvl.spec"; +import "../ERC721/erc721.spec"; +import "../ERC1967/erc1967.spec"; +import "../PriceAggregators/chainlink.spec"; +import "../PriceAggregators/tellor.spec"; + +// aave imports +import "./aToken.spec"; +import "./AddressProvider.spec"; +import "./PriceOracleSentinel.spec"; +import "./PriceOracle.spec"; +import "./ACLManager.spec"; +import "./FlashLoanReceiver.spec"; + +// standard +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +import "../generic.spec"; // pick additional rules from here + + +function init_state() { + // based on aTokensAreNotUnderlyings + require forall address a. + a == 0 // nothing-token + || aTokenToUnderlying[a] == 0 // underlying + || aTokenToUnderlying[aTokenToUnderlying[a]] == 0 // aTokens map to underlyings which map to 0 + ; + // aTokens have the AToken sort, VariableDebtTokens have the VariableDebt sort, etc... + require forall address a. tokenToSort[currentContract._reserves[a].aTokenAddress] == AToken_token(); + require forall address a. tokenToSort[currentContract._reserves[a].variableDebtTokenAddress] == VariableDebtToken_token(); + require forall address a. tokenToSort[currentContract._reserves[a].stableDebtTokenAddress] == StableDebtToken_token(); +} + + + + + +ghost mathint TOT_SUP_ATOKEN; + + +/* +function cumulateToLiquidityIndexCVL(env e, uint256 totalLiq, uint256 amount) returns uint256 { + uint256 __liqInd_before = getReserveNormalizedIncome(e, ASSET); + assert (__liqInd_before == assert_uint256(currentContract._reserves[ASSET].liquidityIndex)); + + uint256 scaled = scaledTotalSupplyCVL(ATOKEN); + + uint256 new_index; + require to_mathint(rayMulCVLPrecise(new_index, scaled)) == TOT_SUP_ATOKEN + amount; + + return new_index; +} +*/ + + +rule check_cumulateToLiquidityIndexCVL(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + require (_atoken != 0); require (_asset != 0); + uint256 __scaled = scaledTotalSupplyCVL(_atoken); + require aTokenToUnderlying[_atoken]==_asset; + + // INDEX + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + require (__liqInd_before == assert_uint256(currentContract._reserves[_asset].liquidityIndex)); + require 10^27 <= __liqInd_before; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + TOT_SUP_ATOKEN = __totSUP_aToken; + + // THE FUNCTION CALL + uint256 totalLiquidity; uint256 __amount; + require __amount == 0; + uint256 __new_index = cumulateToLiquidityIndex(e,_asset, totalLiquidity, __amount); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + + assert to_mathint(rayMulCVLPrecise(__new_index, __scaled)) == TOT_SUP_ATOKEN + __amount; +} diff --git a/certora/experimental/specs/Aave/indexes_rates.spec b/certora/experimental/specs/Aave/indexes_rates.spec new file mode 100644 index 00000000..550f1926 --- /dev/null +++ b/certora/experimental/specs/Aave/indexes_rates.spec @@ -0,0 +1,142 @@ +import "../ERC20/WETHcvl.spec"; +import "../ERC721/erc721.spec"; +import "../ERC1967/erc1967.spec"; +import "../PriceAggregators/chainlink.spec"; +import "../PriceAggregators/tellor.spec"; + +// aave imports +import "./aToken.spec"; +import "./AddressProvider.spec"; +import "./PriceOracleSentinel.spec"; +import "./PriceOracle.spec"; +import "./ACLManager.spec"; +//import "./ReserveInterestRateStrategy.spec"; +//import "./FlashLoanReceiver.spec"; + +// standard +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + + + +//using DummyWeth as weth; + +methods { + function _.calculateInterestRates(DataTypes.CalculateInterestRatesParams params) external + => calculateInterestRatesCVL(/*calledContract,*/ params) expect (uint256, uint256, uint256); +} + + +function calculateInterestRatesCVL( + // address interestRateStrategy, // redundancy + DataTypes.CalculateInterestRatesParams params +) returns (uint256, uint256, uint256) { + uint256 liquidityRate; // = liquidityRateModel[params.liquidityAdded - params.liquidityTaken]; + uint256 stableBorrowRate = 0; + uint256 variableBorrowRate; + + require (params.usingVirtualBalance && params.totalStableDebt + params.totalVariableDebt != 0) + => params.liquidityTaken <= require_uint256(params.virtualUnderlyingBalance + params.liquidityAdded); + + require liquidityRate <= variableBorrowRate; + return (liquidityRate, stableBorrowRate, variableBorrowRate); +} + + + +//use builtin rule sanity filtered { f -> f.contract == currentContract } + + +function isVirtualAccActive(uint256 data) returns bool { + uint mask = 0xEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + return (data & ~mask) != 0; +} + + + +function init_state() { + // based on aTokensAreNotUnderlyings + require forall address a. + a == 0 // nothing-token + || aTokenToUnderlying[a] == 0 // underlying + || aTokenToUnderlying[aTokenToUnderlying[a]] == 0 // aTokens map to underlyings which map to 0 + ; + // aTokens have the AToken sort, VariableDebtTokens have the VariableDebt sort, etc... + require forall address a. tokenToSort[currentContract._reserves[a].aTokenAddress] == AToken_token(); + require forall address a. tokenToSort[currentContract._reserves[a].variableDebtTokenAddress] == VariableDebtToken_token(); + require forall address a. tokenToSort[currentContract._reserves[a].stableDebtTokenAddress] == StableDebtToken_token(); +} + + + + + + +rule borrowIndex_GEQ_liquidityIndex__borrow(env e, address _asset) { + init_state(); + + mathint __totSUP_aToken; mathint __totSUP_debt; mathint __totSUP_stb; + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + + + require forall address a. balanceByToken[_debt][a] <= totalSupplyByToken[_debt]; + require forall address a. balanceByToken[_atoken][a] <= totalSupplyByToken[_atoken]; + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; //require aTokenToUnderlying[_stb]==_asset; + + require _atoken==10; require _debt==11; require _asset==100; + require weth!=10 && weth!=11; + + + DataTypes.ReserveData reserve1 = getReserveDataExtended(e, _asset); + uint256 __liq1 = reserve1.liquidityIndex; uint256 __debt1 = reserve1.variableBorrowIndex; + + require reserve1.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + //THE MAIN REQUIREMENT + require __liq1 <= __debt1; + require 10^27 <= __liq1 && __liq1 <= 2*10^27; + require reserve1.currentVariableBorrowRate >= reserve1.currentLiquidityRate; + + __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + + require scaledTotalSupplyCVL(_stb)==0; + require __totSUP_debt!=0; + uint128 __virtual_bal = getReserveDataExtended(e,_asset).virtualUnderlyingBalance; + + // require 40 <= __totSUP_aToken && __totSUP_aToken <= 100; + uint256 _amount; uint256 _interestRateMode; address onBehalfOf; uint16 referralCode; + require _interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + require isVirtualAccActive(reserve1.configuration.data)==true; + + // FUNCTION CALL + borrow(e, _asset, _amount, _interestRateMode, referralCode, onBehalfOf); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(e, _asset); + uint256 __liq2 = reserve2.liquidityIndex; uint256 __debt2 = reserve2.variableBorrowIndex; + + // require reserve2.liquidityIndex == reserve2.variableBorrowIndex; + //require 10^27 <= reserve2.liquidityIndex && reserve2.liquidityIndex <= 2*10^27; + + assert reserve1.liquidityIndex <= reserve2.liquidityIndex; + assert reserve1.variableBorrowIndex <= reserve2.variableBorrowIndex; + assert reserve2.liquidityIndex <= reserve2.variableBorrowIndex; +} + + +rule test_rayMulDiv() { + uint256 a; uint256 b; + + require b==10^27; + uint256 out = rayDivCVLPrecise(rayMulCVLPrecise(a,b),b); + + assert out==a; + + assert (out-a)*b <= 10^27 / 2 + b/2; + assert out-a <= (10^27 / 2 + b/2)/b; + assert to_mathint(out) <= a + (5*10^26)/b + 1; + assert to_mathint(out) <= a + (5*10^26)/b; +} diff --git a/certora/experimental/specs/Aave/solvency/README.txt b/certora/experimental/specs/Aave/solvency/README.txt new file mode 100644 index 00000000..5d1e32f0 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/README.txt @@ -0,0 +1,31 @@ + +The property that we prove in this directory is the following solvency invariant (for an arbitrary asset): + (*) Atoken.totalSupply() <= VariableDebtToken.totalSupply() + virtual_balance + +Intuitively, the left hand side is the amount the the pool owes to its users, and the right hand +side is the amount it has (either in hands - the virtual_balance, or what people owe to it - +the VariableDebtToken.totalSupply()) (*) should be proved for the following case: +1. A function call: for example supply, withdraw, borrow, repay, repayWithATokens ... +2. Time passing (without any function being called). This is relevant because the indexes increase + with the time, hence the amounts that appear in (*) + +Note that: +1. The above isn't a real invariant. It can be violated due to rounding errors. What we really prove + is that the left-hand-side minus right-hand-side of (*) can't increase by more than the index + (in RAY units) after each function call. (it is either the liquidity-index or the variableBorrow-index + depending on the specific function call. + +2. The above is proved under the following assumptions: + a. The pool uses virtual accounting for the asset. + b. The asset uses only variable-debt interest (and not stable-debt). Moreover we assume that + StableDebtToken.totalSupply()==0. (Aave is going in that direction.) + c. RAY <= liquidity-index && RAY <= borrow-index. (this should be easy to prove) + d. Atoken.totalSupply() <= RAY. If this assumption is removed, we either get a timeout or an error. + (I suspect that the error is due to an imprecision RAY-calculation with such big numbers, but + haven't checked it yet.) + +3. We deal with each function in a seperate file. For some functions (repay, repayWithATokens) we + dedicate a sub-directory, because the proof is more involved and contains lemmas or case splits. + + + diff --git a/certora/experimental/specs/Aave/solvency/borrow.spec b/certora/experimental/specs/Aave/solvency/borrow.spec new file mode 100644 index 00000000..616eaa2e --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/borrow.spec @@ -0,0 +1,75 @@ + +// aave imports +import "../aToken.spec"; +import "../AddressProvider.spec"; + + +import "common/optimizations.spec"; +import "common/functions.spec"; +import "common/validation_functions.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + + +/*===================================================================================== + Rule: solvency__borrow + Status: PASS + Link: https://prover.certora.com/output/66114/57d6a2730b3f4fd5ba9976a35f394324/?anonymousKey=743a5d838a12d3fd2dd4577c82d080a66a095d88 + =====================================================================================*/ +rule solvency__borrow(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + //THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a failure. + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + uint256 _amount; uint256 _interestRateMode; address onBehalfOf; uint16 referralCode; + require _interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + borrow(e, _asset, _amount, _interestRateMode, referralCode, onBehalfOf); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert __totSUP_aToken__ <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.variableBorrowIndex / RAY() ; +} diff --git a/certora/experimental/specs/Aave/solvency/common/functions.spec b/certora/experimental/specs/Aave/solvency/common/functions.spec new file mode 100644 index 00000000..701f67ab --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/common/functions.spec @@ -0,0 +1,47 @@ + + +methods { + function _.ADDRESSES_PROVIDER() external => NONDET; // expect address + + function getReserveDataExtended(address) external returns (DataTypes.ReserveData memory) envfree; + function getReserveAddressById(uint16 id) external returns (address) envfree; + function getReservesList() external returns (address[]) envfree; + + function rayMul(uint256,uint256) external returns (uint256) envfree; + function rayDiv(uint256,uint256) external returns (uint256) envfree; +} + + + + +function isVirtualAccActive(uint256 data) returns bool { + uint mask = 0xEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + return (data & ~mask) != 0; +} + +function init_state() { + // based on aTokensAreNotUnderlyings + require forall address a. + a == 0 // nothing-token + || aTokenToUnderlying[a] == 0 // underlying + || aTokenToUnderlying[aTokenToUnderlying[a]] == 0 // aTokens map to underlyings which map to 0 + ; + // aTokens have the AToken sort, VariableDebtTokens have the VariableDebt sort, etc... + require forall address a. tokenToSort[currentContract._reserves[a].aTokenAddress] == AToken_token(); + require forall address a. tokenToSort[currentContract._reserves[a].variableDebtTokenAddress] == VariableDebtToken_token(); +} + +function tokens_addresses_limitations(address atoken, address variable, address asset) { + // require atoken==10; require variable==11; require asset==100; + // require weth!=10 && weth!=11 && weth!=12; + + require asset != 0; + require atoken != variable && atoken != asset; + require variable != asset; + // require weth != atoken && weth != variable && atoken != stb; + + // The asset that current rule deals with. It is used in summarization CVL-functions, and other places. + // See for example _accrueToTreasuryCVL(). + ASSET = asset; +} + diff --git a/certora/experimental/specs/Aave/solvency/common/optimizations.spec b/certora/experimental/specs/Aave/solvency/common/optimizations.spec new file mode 100644 index 00000000..84611123 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/common/optimizations.spec @@ -0,0 +1,60 @@ + + +// optimizing summaries +methods { + function ReserveLogic._accrueToTreasury( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache + ) internal => _accrueToTreasuryCVL(); + + function MathUtils.calculateCompoundedInterest( + uint256 rate,uint40 lastUpdateTimestamp, uint256 currentTimestamp + ) internal returns (uint256)=> compoundedInterestCVL(rate, lastUpdateTimestamp, currentTimestamp); + + function MathUtils.calculateLinearInterest(uint256 rate, uint40 lastUpdateTimestamp) + internal returns (uint256) with (env e) => f_linearInterestCVL(e,rate,lastUpdateTimestamp); + + function _.calculateInterestRates(DataTypes.CalculateInterestRatesParams params) external => NONDET; + + function GenericLogic._getUserDebtInBaseCurrency( + address, + DataTypes.ReserveData storage, + uint256, + uint256 + ) internal returns (uint256) => NONDET /* difficulty 106 */; + + function GenericLogic.calculateUserAccountData( + mapping (address => DataTypes.ReserveData) storage, + mapping (uint256 => address) storage, + mapping (uint8 => DataTypes.EModeCategory) storage, + DataTypes.CalculateUserAccountDataParams memory + ) internal returns (uint256,uint256,uint256,uint256,uint256,bool) => NONDET /* difficulty 214 */; + +} // END METHODS BLOCK + + + +persistent ghost linearInterestCVL(mathint,mathint,mathint) returns uint { + axiom forall mathint rate. forall mathint lastUpdateTimestamp. forall mathint currentTimestamp. + to_mathint(linearInterestCVL(rate, lastUpdateTimestamp, currentTimestamp)) == + RAY() + rate * (currentTimestamp - lastUpdateTimestamp) / (365*86400); + // the 365*86400 is SECONDS_PER_YEAR() as defined in MathUtils. +} +persistent ghost compoundedInterestCVL(mathint,mathint,mathint) returns uint { + axiom forall mathint rate. forall mathint lastUpdateTimestamp. forall mathint currentTimestamp. + to_mathint(compoundedInterestCVL(rate, lastUpdateTimestamp, currentTimestamp)) >= + to_mathint(linearInterestCVL(rate, lastUpdateTimestamp, currentTimestamp)); + // RAY() + rate * (currentTimestamp - lastUpdateTimestamp) / (365*86400); + // the 365*86400 is SECONDS_PER_YEAR() as defined in MathUtils. +} + +persistent ghost address ASSET; //The asset that current rule refers to. Should be assigned from within the rule. + +// The function _accrueToTreasury(...) only writes to the field accruedToTreasury. +function _accrueToTreasuryCVL() { + havoc currentContract._reserves[ASSET].accruedToTreasury; +} + +function f_linearInterestCVL(env e,uint256 rate,uint40 lastUpdateTimestamp) returns uint256 { + return linearInterestCVL(rate,lastUpdateTimestamp, e.block.timestamp); +} diff --git a/certora/experimental/specs/Aave/solvency/common/validation_functions.spec b/certora/experimental/specs/Aave/solvency/common/validation_functions.spec new file mode 100644 index 00000000..4659f30e --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/common/validation_functions.spec @@ -0,0 +1,66 @@ + + + +// The validtion function are usually summarized to NONDET +methods { + function ValidationLogic.validateLiquidationCall( + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveData storage collateralReserve, + DataTypes.ReserveData storage debtReserve, + DataTypes.ValidateLiquidationCallParams memory params + ) internal => NONDET; + + function ValidationLogic.validateWithdraw( + DataTypes.ReserveCache memory reserveCache, + uint256 amount, + uint256 userBalance + ) internal => NONDET; + + function ValidationLogic.validateAutomaticUseAsCollateral( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveConfigurationMap memory reserveConfig, + address aTokenAddress + ) internal returns (bool) => NONDET; + + function ValidationLogic.validateSupply( + DataTypes.ReserveCache memory, + DataTypes.ReserveData storage, + uint256, + address + ) internal => NONDET; + + + function ValidationLogic.validateBorrow( + mapping (address => DataTypes.ReserveData) storage reservesData, + mapping (uint256 => address) storage reservesList, + mapping (uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.ValidateBorrowParams memory params + ) internal => NONDET; + /* difficulty 331 ; */ + + function ValidationLogic.validateHealthFactor( + mapping (address => DataTypes.ReserveData) storage, + mapping (uint256 => address) storage, + mapping (uint8 => DataTypes.EModeCategory) storage, + DataTypes.UserConfigurationMap memory, + address, + uint8, + uint256, + address + ) internal returns (uint256,bool) => NONDET /* difficulty 214 */; + + function ValidationLogic.validateHFAndLtv( + mapping (address => DataTypes.ReserveData) storage, + mapping (uint256 => address) storage, + mapping (uint8 => DataTypes.EModeCategory) storage, + DataTypes.UserConfigurationMap memory, + address, + address, + uint256, + address, + uint8 + ) internal => NONDET /* difficulty 216 */; + +} diff --git a/certora/experimental/specs/Aave/solvency/cumulateToLiquidityIndexCVL-check.spec b/certora/experimental/specs/Aave/solvency/cumulateToLiquidityIndexCVL-check.spec new file mode 100644 index 00000000..f128777e --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/cumulateToLiquidityIndexCVL-check.spec @@ -0,0 +1,58 @@ + +// aave imports +import "../aToken.spec"; +import "../AddressProvider.spec"; + +import "common/optimizations.spec"; +import "common/functions.spec"; +import "common/validation_functions.spec"; + + +ghost mathint TOT_SUP_ATOKEN; + +methods { + // function _.rayMul(uint256 a, uint256 b) internal => rayMulCVLPrecise(a, b) expect uint256; // not optimized well by Prover + //function _.rayDiv(uint256 a, uint256 b) internal => rayDivCVLPrecise(a, b) expect uint256; // seems to be optimized well by Prover +} + + +/*===================================================================================== + Rule: check_cumulateToLiquidityIndexCVL + In the proof of solvency__flashLoanSimple we summarize the function + cumulateToLiquidityIndex(...) with its CVL's counterpart: cumulateToLiquidityIndexCVL(). + Here we check that out summarization is indeed correct. + + Status: PASS + Link: https://prover.certora.com/output/66114/398efa550ab44487bb5c958c588d9de9/?anonymousKey=9dfc18bf71c7a3d4074449a2319c2ebf558e5911 + =====================================================================================*/ +rule check_cumulateToLiquidityIndexCVL(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + require (_atoken != 0); require (_asset != 0); + uint256 __scaled = scaledTotalSupplyCVL(_atoken); + require aTokenToUnderlying[_atoken]==_asset; + + // INDEX + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + require (__liqInd_before == assert_uint256(currentContract._reserves[_asset].liquidityIndex)); + require 10^27 <= __liqInd_before; // && __liqInd_before <= 100*10^27; + + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + TOT_SUP_ATOKEN = __totSUP_aToken; + + require __totSUP_aToken <= 10^27; + + // THE FUNCTION CALL + uint256 totalLiquidity; uint256 __amount; + require __totSUP_aToken <= to_mathint(totalLiquidity) && totalLiquidity <= 10^27; + require to_mathint(__amount) <= __totSUP_aToken; + uint256 __new_index = cumulateToLiquidityIndex(e,_asset, totalLiquidity, __amount); // ******* + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + + assert to_mathint(rayMulCVLPrecise(__new_index, __scaled)) <= + TOT_SUP_ATOKEN + __amount + 2*__liqInd_before/10^27; + +} diff --git a/certora/experimental/specs/Aave/solvency/flashloan.spec b/certora/experimental/specs/Aave/solvency/flashloan.spec new file mode 100644 index 00000000..a9f0e24e --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/flashloan.spec @@ -0,0 +1,129 @@ + +// aave imports +import "../aToken.spec"; +import "../AddressProvider.spec"; +import "../FlashLoanReceiver.spec"; + +import "common/optimizations.spec"; +import "common/functions.spec"; +import "common/validation_functions.spec"; + +/*================================================================================================ + See the README.txt file in the solvency/ directory. + + Note that we rely on the summarization of cumulateToLiquidityIndex, that states that the + liquidity-index isn't increased too much. + + We prove that out summarization is indeed sound in the file cumulateToLiquidityIndexCVL-check.spec. + ================================================================================================*/ + +methods { + function ReserveLogic.cumulateToLiquidityIndex(DataTypes.ReserveData storage reserve, + uint256 totalLiquidity, + uint256 amount + ) internal returns uint256 with (env e) + => cumulateToLiquidityIndexCVL(e, totalLiquidity, amount); + + function ValidationLogic.validateFlashloanSimple( + DataTypes.ReserveData storage reserve, + uint256 amount + ) internal => validateFlashloanSimpleCVL(amount); +} + +ghost uint256 AMOUNT; +ghost address ATOKEN; +ghost uint128 VB; +ghost uint256 LIQUIDITY_IND_BEFORE; +ghost mathint TOT_SUP_ATOKEN; +ghost mathint TOT_SUP_DEBT; +ghost uint256 DELTA; + + +function cumulateToLiquidityIndexCVL(env e, uint256 totalLiq, uint256 amount) returns uint256 { + uint256 __liqInd_before = getReserveNormalizedIncome(e, ASSET); + assert (__liqInd_before == assert_uint256(currentContract._reserves[ASSET].liquidityIndex)); + assert to_mathint(amount) <= TOT_SUP_ATOKEN; + assert TOT_SUP_ATOKEN <= to_mathint(totalLiq); + + uint256 scaled = scaledTotalSupplyCVL(ATOKEN); + + + uint256 new_index; + require to_mathint(rayMulCVLPrecise(new_index, scaled)) <= TOT_SUP_ATOKEN + amount + 2*__liqInd_before/10^27; + + havoc currentContract._reserves[ASSET].liquidityIndex; + require currentContract._reserves[ASSET].liquidityIndex==require_uint128(new_index); + + return new_index; +} + +function validateFlashloanSimpleCVL(uint256 amount) { + require to_mathint(amount) <= TOT_SUP_ATOKEN; +} + + +/*===================================================================================== + Rule: solvency__flashLoanSimple + + Status: PASS + Link: https://prover.certora.com/output/66114/0245f79ede1044c787a1c6b834495d65/?anonymousKey=ce44cd008eb5a1e797acb2e2d9fb93f89485d2bb + =====================================================================================*/ +rule solvency__flashLoanSimple(env e, address _asset) { + init_state(); + + require FLASHLOAN_PREMIUM_TOTAL(e) <= 10000; + require FLASHLOAN_PREMIUM_TO_PROTOCOL(e) <= 10000; + + address _atoken = currentContract._reserves[_asset].aTokenAddress; ATOKEN = _atoken; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + require forall address a. balanceByToken[_atoken][a] <= totalSupplyByToken[_atoken]; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); LIQUIDITY_IND_BEFORE = __liqInd_before; + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + require __liqInd_before >= RAY() && __dbtInd_before >= RAY(); + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + TOT_SUP_ATOKEN = __totSUP_aToken; + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + TOT_SUP_DEBT = __totSUP_debt; + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + require to_mathint(__totSUP_aToken) <= to_mathint(__virtual_bal) + __totSUP_debt + DELTA; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + + // THE FUNCTION CALL + address receiverAddress; uint256 _amount; bytes params; uint16 referralCode; + AMOUNT = _amount; + VB = __virtual_bal; + flashLoanSimple(e, receiverAddress, _asset, _amount, params, referralCode); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == require_uint128(__dbtInd_before)); + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + uint256 __dbtInd_after = getReserveNormalizedVariableDebt(e, _asset); + + mathint __totSUP_aToken__; mathint __totSUP_debt__; + __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= to_mathint(__virtual_bal__) + __totSUP_debt__ + DELTA + + 2*__liqInd_before / RAY(); +} + diff --git a/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-common.spec b/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-common.spec new file mode 100644 index 00000000..7971617e --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-common.spec @@ -0,0 +1,78 @@ + + +persistent ghost address _DBT_asset; persistent ghost address _DBT_atoken; persistent ghost address _DBT_debt; +persistent ghost address _COL_asset; persistent ghost address _COL_atoken; persistent ghost address _COL_debt; + + +/*================================================================================================ + Summarizations + ================================================================================================*/ +methods { + function IsolationModeLogic.updateIsolatedDebtIfIsolated( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveCache memory reserveCache, + uint256 repayAmount + ) internal => updateIsolatedDebtIfIsolatedCVL(); + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal returns (uint256,uint256,uint256,uint256) => + _calculateAvailableCollateralToLiquidateCVL(); +} + + +function _calculateAvailableCollateralToLiquidateCVL() returns (uint256,uint256,uint256,uint256) { + uint256 a; uint256 b; uint256 c; uint256 d; // require c==0; + return (a,b,c,d); +} + + +// The function updateIsolatedDebtIfIsolated(...) only writes to the field isolationModeTotalDebt. +function updateIsolatedDebtIfIsolatedCVL() { + address asset; + havoc currentContract._reserves[asset].isolationModeTotalDebt; +} + + + + +/*================================================================================================ + General Function + ================================================================================================*/ + +function tokens_addresses_limitations_LQD(address asset, address atoken, address debt, + address asset2, address atoken2, address debt2 + ) { + require asset==100; require atoken==10; require debt==11; + require asset2==200; require atoken2==20; require debt2==21; + + //require _DBT_asset!=0; require _COL_asset!=0; + //require _DBT_asset != _COL_asset; + //require _DBT_atoken != _COL_atoken; + //require _DBT_debt != _COL_debt; +} + + +function configuration() { + init_state(); + + _DBT_atoken = currentContract._reserves[_DBT_asset].aTokenAddress; + _COL_atoken = currentContract._reserves[_COL_asset].aTokenAddress; + _DBT_debt = currentContract._reserves[_DBT_asset].variableDebtTokenAddress; + _COL_debt = currentContract._reserves[_COL_asset].variableDebtTokenAddress; + tokens_addresses_limitations_LQD(_DBT_asset, _DBT_atoken, _DBT_debt, + _COL_asset, _COL_atoken, _COL_debt); + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} + diff --git a/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-lemma.spec b/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-lemma.spec new file mode 100644 index 00000000..d45b0e9f --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-lemma.spec @@ -0,0 +1,179 @@ + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; +import "DBTasset-common.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + +persistent ghost bool INSIDE_liquidationCall; +persistent ghost bool INSIDE_burnBadDebt; + +persistent ghost uint256 _DBT_liqIND; persistent ghost uint256 _DBT_dbtIND; +persistent ghost uint256 _COL_liqIND; persistent ghost uint256 _COL_dbtIND; + + + + +/*================================================================================================ + Summarizations + ================================================================================================*/ +methods { + //TEMPORARY !!! we remove the following + /* function LiquidationLogic._burnBadDebt( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + uint256 reservesCount, + address user + ) internal => NONDET;*/ + + function ReserveLogic.getNormalizedIncome_hook(uint256 ret_val, address aTokenAddress) + internal => getNormalizedIncome_hook_CVL(ret_val, aTokenAddress); + + function ReserveLogic.getNormalizedDebt_hook(uint256 ret_val, address aTokenAddress) + internal => getNormalizedDebt_hook_CVL(ret_val, aTokenAddress); + + function ReserveLogic._updateIndexes_hook(DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache) + internal => _updateIndexes_hook_CVL(reserveCache); +} + +function getNormalizedIncome_hook_CVL(uint256 ret_val, address aTokenAddress) { + assert INSIDE_liquidationCall && !INSIDE_burnBadDebt => aTokenAddress==_COL_atoken; + assert INSIDE_liquidationCall && !INSIDE_burnBadDebt => ret_val==_COL_liqIND; +} + +function getNormalizedDebt_hook_CVL(uint256 ret_val, address aTokenAddress) {} + +function _updateIndexes_hook_CVL(DataTypes.ReserveCache reserveCache) { + assert (!INSIDE_burnBadDebt && reserveCache.aTokenAddress == _COL_atoken) => currentContract._reserves[_COL_asset].liquidityIndex==_COL_liqIND; + assert (!INSIDE_burnBadDebt && reserveCache.aTokenAddress == _COL_atoken) => currentContract._reserves[_COL_asset].variableBorrowIndex==_COL_dbtIND; +} + + + +/*================================================================================================ + Summarizations of HOOKS function + ================================================================================================*/ +methods { + function LiquidationLogic.get_userCollateralBalance() + internal returns(uint256) => NONDET; + + function LiquidationLogic.HOOK_burnCollateralATokens_after_updateState() + internal => HOOK_burnCollateralATokens_after_updateState_CVL(); + + function LiquidationLogic.HOOK_liquidation_before_burnBadDebt() + internal with (env e) => HOOK_liquidation_before_burnBadDebt_CVL(e); + + function LiquidationLogic.HOOK_liquidation_after_burnBadDebt() + internal with (env e) => HOOK_liquidation_after_burnBadDebt_CVL(e); +} + +// This is immediately after the call to updateState for the COL token +function HOOK_burnCollateralATokens_after_updateState_CVL() { + assert currentContract._reserves[_COL_asset].liquidityIndex == _COL_liqIND; + assert currentContract._reserves[_COL_asset].variableBorrowIndex == _COL_dbtIND; +} + +persistent ghost uint256 COL_liqIND_INTR1; +persistent ghost uint256 COL_dbtIND_INTR1; +function HOOK_liquidation_before_burnBadDebt_CVL(env e) { + INSIDE_liquidationCall = false; + + COL_liqIND_INTR1 = getReserveNormalizedIncome(e, _COL_asset); + assert COL_liqIND_INTR1 == _COL_liqIND; + + COL_dbtIND_INTR1 = getReserveNormalizedVariableDebt(e, _COL_asset); + assert COL_dbtIND_INTR1 == _COL_dbtIND; + + INSIDE_burnBadDebt = true; + INSIDE_liquidationCall = true; +} + +persistent ghost uint256 COL_liqIND_INTR2; +persistent ghost uint256 COL_dbtIND_INTR2; +function HOOK_liquidation_after_burnBadDebt_CVL(env e) { + INSIDE_burnBadDebt = false; + INSIDE_liquidationCall = false; + + COL_liqIND_INTR2 = getReserveNormalizedIncome(e, _COL_asset); + assert COL_liqIND_INTR2 == COL_liqIND_INTR1; + + COL_dbtIND_INTR2 = getReserveNormalizedVariableDebt(e, _COL_asset); + assert COL_dbtIND_INTR2 == COL_dbtIND_INTR1; + + INSIDE_liquidationCall = true; +} + + + +/* +function tokens_addresses_limitations_LQD(address asset, address atoken, address debt, + address asset2, address atoken2, address debt2 + ) { + require asset==100; require atoken==10; require debt==11; + require asset2==200; require atoken2==20; require debt2==21; +} + + +function configuration() { + init_state(); + + _DBT_atoken = currentContract._reserves[_DBT_asset].aTokenAddress; + _COL_atoken = currentContract._reserves[_COL_asset].aTokenAddress; + _DBT_debt = currentContract._reserves[_DBT_asset].variableDebtTokenAddress; + _COL_debt = currentContract._reserves[_COL_asset].variableDebtTokenAddress; + tokens_addresses_limitations_LQD(_DBT_asset, _DBT_atoken, _DBT_debt, + _COL_asset, _COL_atoken, _COL_debt); + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} +*/ + +/*===================================================================================== + Rule: same_indexes__liquidationCall + =====================================================================================*/ +rule same_indexes__liquidationCall(env e) { + INSIDE_liquidationCall = false; + INSIDE_burnBadDebt = false; + configuration(); + + _DBT_liqIND = getReserveNormalizedIncome(e, _DBT_asset); + _COL_liqIND = getReserveNormalizedIncome(e, _COL_asset); + + _DBT_dbtIND = getReserveNormalizedVariableDebt(e, _DBT_asset); + _COL_dbtIND = getReserveNormalizedVariableDebt(e, _COL_asset); + + DataTypes.ReserveData reserve = getReserveDataExtended(_DBT_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_DBT_debt)!=0; // We prove that if ==0 then the call to + // liquidationCall reverts (see DBTasset-totSUP0.spec) + require scaledTotalSupplyCVL(_COL_debt)!=0; // We treat the case where ==0 in the files COLasset-totSUP0... + + // THE FUNCTION CALL + address user; uint256 debtToCover; + bool receiveAToken = true; + + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, user, debtToCover, receiveAToken); + INSIDE_liquidationCall = false; + + uint256 __COL_liqIND_after = getReserveNormalizedIncome(e, _COL_asset); + assert __COL_liqIND_after == _COL_liqIND; + + uint256 __COL_dbtIND_after = getReserveNormalizedVariableDebt(e, _COL_asset); + assert __COL_dbtIND_after == _COL_dbtIND; +} + + diff --git a/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-main.spec b/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-main.spec new file mode 100644 index 00000000..598ab702 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-main.spec @@ -0,0 +1,319 @@ + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + +persistent ghost bool INSIDE_liquidationCall; +persistent ghost bool INSIDE_burnBadDebt; + +persistent ghost address _DBT_asset; persistent ghost address _DBT_atoken; persistent ghost address _DBT_debt; + +persistent ghost address _COL_asset; persistent ghost address _COL_atoken; persistent ghost address _COL_debt; +persistent ghost uint256 _COL_liqIND {axiom _COL_liqIND >= 10^27;} +persistent ghost uint256 _COL_dbtIND {axiom _COL_dbtIND >= 10^27;} + + +persistent ghost address USER; +persistent ghost mathint DELTA; +persistent ghost mathint ORIG_totSUP_aToken; +persistent ghost mathint ORIG_totSUP_debt; +persistent ghost uint128 ORIG_VB; +persistent ghost uint128 ORIG_deficit; + +persistent ghost mathint INTR_totSUP_aToken; +persistent ghost mathint INTR_totSUP_debt; +persistent ghost uint128 INTR_VB; +persistent ghost uint128 INTR_deficit; + +persistent ghost mathint INTR2_totSUP_aToken; +persistent ghost mathint INTR2_totSUP_debt; +persistent ghost uint128 INTR2_VB; +persistent ghost uint128 INTR2_deficit; + +persistent ghost mathint FINAL_totSUP_aToken; +persistent ghost mathint FINAL_totSUP_debt; +persistent ghost uint128 FINAL_VB; +persistent ghost uint128 FINAL_deficit; + + +methods { + function IsolationModeLogic.updateIsolatedDebtIfIsolated( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveCache memory reserveCache, + uint256 repayAmount + ) internal => updateIsolatedDebtIfIsolatedCVL(); + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal returns (uint256,uint256,uint256,uint256) => + _calculateAvailableCollateralToLiquidateCVL(); + + function LiquidationLogic._burnBadDebt( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + uint256 reservesCount, + address user + ) internal with (env e) => _burnBadDebt_CVL(e); +} + +function _burnBadDebt_CVL(env e) { + INSIDE_liquidationCall = false; + + mathint curr_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + mathint curr_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + uint128 curr_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + uint128 curr_deficit = getReserveDataExtended(_COL_asset).deficit; + + if (curr_totSUP_aToken<=curr_VB + curr_totSUP_debt + curr_deficit + DELTA + _COL_liqIND / RAY()) { + havoc_all(e); + mathint after_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + mathint after_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + uint128 after_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + uint128 after_deficit = getReserveDataExtended(_COL_asset).deficit; + + require + after_totSUP_aToken <= + after_VB + after_totSUP_debt + after_deficit + DELTA + + _COL_liqIND / RAY() + _COL_dbtIND / RAY() + ; + } + INSIDE_liquidationCall = true; +} + +// This is immediately after the call to updateState for the COL token +function _calculateAvailableCollateralToLiquidateCVL() returns (uint256,uint256,uint256,uint256) { + uint256 a; uint256 b; uint256 c; uint256 d; + return (a,b,c,d); +} + + +// The function updateIsolatedDebtIfIsolated(...) only writes to the field isolationModeTotalDebt. +function updateIsolatedDebtIfIsolatedCVL() { + address asset; + havoc currentContract._reserves[asset].isolationModeTotalDebt; +} + + +/*============================================================================================== + Summarizations + ==============================================================================================*/ +methods { + function ReserveLogic.getNormalizedIncome(DataTypes.ReserveData storage reserve) + internal returns (uint256) => _COL_liqIND; + + function ReserveLogic.getNormalizedDebt(DataTypes.ReserveData storage reserve) + internal returns (uint256) => getNormalizedDebt_CVL(); +} + +function getNormalizedDebt_CVL() returns uint256 { + if (INSIDE_liquidationCall) { + uint256 any_index; + return any_index; + } + else + return _COL_liqIND; +} + + +/*================================================================================================ + Summarizations of HOOKS function + ================================================================================================*/ +methods { + function LiquidationLogic.HOOK_liquidation_before_burnCollateralATokens(uint256 actualCollateralToLiquidate) + internal with (env e) => HOOK_liquidation_before_burnCollateralATokens_CVL(e, actualCollateralToLiquidate); + + function LiquidationLogic.HOOK_liquidation_after_burnCollateralATokens(uint256 actualCollateralToLiquidate) + internal with (env e) => HOOK_liquidation_after_burnCollateralATokens_CVL(e, actualCollateralToLiquidate); + + function LiquidationLogic.HOOK_burnCollateralATokens_after_updateState() + internal => HOOK_burnCollateralATokens_after_updateState_CVL(); + + function LiquidationLogic.HOOK_liquidation_before_burnBadDebt() + internal with (env e) => HOOK_liquidation_before_burnBadDebt_CVL(e); + + function LiquidationLogic.HOOK_liquidation_after_burnBadDebt() + internal with (env e) => HOOK_liquidation_after_burnBadDebt_CVL(e); +} + +function HOOK_liquidation_before_burnCollateralATokens_CVL(env e, uint256 actualCollateralToLiquidate) { + INSIDE_liquidationCall = false; + + mathint curr_totSUP_aToken; curr_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + mathint curr_totSUP_debt; curr_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + uint128 curr_VB; curr_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + uint128 curr_deficit; curr_deficit = getReserveDataExtended(_COL_asset).deficit; + + assert ORIG_totSUP_aToken == curr_totSUP_aToken; + assert ORIG_totSUP_debt == curr_totSUP_debt; + assert ORIG_VB == curr_VB; + assert ORIG_deficit == curr_deficit; + + assert ORIG_totSUP_aToken <= ORIG_VB + ORIG_totSUP_debt + ORIG_deficit + DELTA; + + uint256 scaled = totalSupplyByToken[_COL_atoken]; uint256 IND = _COL_liqIND; + assert + to_mathint(rayMulCVLPrecise( require_uint256(scaled - rayDivCVLPrecise(actualCollateralToLiquidate,IND)), IND) ) + <= + rayMulCVLPrecise(scaled,IND) - actualCollateralToLiquidate + IND/RAY(); + + INSIDE_liquidationCall = true; +} + +function HOOK_liquidation_after_burnCollateralATokens_CVL(env e, uint256 actualCollateralToLiquidate) { + INSIDE_liquidationCall = false; + + INTR_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + INTR_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + INTR_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + INTR_deficit = getReserveDataExtended(_COL_asset).deficit; + + assert INTR_totSUP_debt == ORIG_totSUP_debt; + assert INTR_deficit == ORIG_deficit; + + assert INTR_VB == ORIG_VB - actualCollateralToLiquidate; + assert INTR_totSUP_aToken <= ORIG_totSUP_aToken - actualCollateralToLiquidate + _COL_liqIND / RAY() ; + + assert INTR_totSUP_aToken <= INTR_VB + INTR_totSUP_debt + INTR_deficit + DELTA + _COL_liqIND / RAY(); + + INSIDE_liquidationCall = true; +} + +function HOOK_burnCollateralATokens_after_updateState_CVL() { + require currentContract._reserves[_COL_asset].liquidityIndex == _COL_liqIND; + require currentContract._reserves[_COL_asset].variableBorrowIndex == _COL_dbtIND; +} + +function HOOK_liquidation_before_burnBadDebt_CVL(env e) { + INSIDE_liquidationCall = false; + + INTR2_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + INTR2_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + INTR2_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + INTR2_deficit = getReserveDataExtended(_COL_asset).deficit; + + assert INTR2_totSUP_aToken <= INTR2_VB + INTR2_totSUP_debt + INTR2_deficit + DELTA + _COL_liqIND / RAY(); + + INSIDE_liquidationCall = true; + INSIDE_burnBadDebt = true; +} + +function HOOK_liquidation_after_burnBadDebt_CVL(env e) { + INSIDE_liquidationCall = false; + INSIDE_burnBadDebt = false; + + FINAL_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + FINAL_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + FINAL_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + FINAL_deficit = getReserveDataExtended(_COL_asset).deficit; + + assert + FINAL_totSUP_aToken <= FINAL_VB + FINAL_totSUP_debt + FINAL_deficit + DELTA + + _COL_liqIND / RAY() + _COL_dbtIND / RAY(); + + INSIDE_liquidationCall = true; +} + + + + + + + + +function tokens_addresses_limitations_LQD(address asset, address atoken, address debt, + address asset2, address atoken2, address debt2 + ) { + //require asset==100; require atoken==10; require debt==11; + //require asset2==200; require atoken2==20; require debt2==21; + + require _DBT_asset!=0; require _COL_asset!=0; + + require _DBT_asset!=_COL_asset; + require _DBT_atoken != _COL_atoken; + require _DBT_debt != _COL_debt; +} + + +function configuration() { + init_state(); + + _DBT_atoken = currentContract._reserves[_DBT_asset].aTokenAddress; + _COL_atoken = currentContract._reserves[_COL_asset].aTokenAddress; + _DBT_debt = currentContract._reserves[_DBT_asset].variableDebtTokenAddress; + _COL_debt = currentContract._reserves[_COL_asset].variableDebtTokenAddress; + tokens_addresses_limitations_LQD(_DBT_asset, _DBT_atoken, _DBT_debt, + _COL_asset, _COL_atoken, _COL_debt); + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} + +/*===================================================================================== + Rule: solvency__liquidationCall + =====================================================================================*/ +rule solvency__liquidationCall_COLasset(env e) { + INSIDE_liquidationCall = false; + INSIDE_burnBadDebt = false; + configuration(); + + DataTypes.ReserveData reserve = getReserveDataExtended(_COL_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + ORIG_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + ORIG_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + ORIG_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + ORIG_deficit = getReserveDataExtended(_COL_asset).deficit; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + require ORIG_totSUP_aToken <= ORIG_VB + ORIG_totSUP_debt + ORIG_deficit + DELTA; + + require ORIG_totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_COL_debt)!=0; + require exists_debt; + + // THE FUNCTION CALL + uint256 _debtToCover; bool _receiveAToken; + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, USER, _debtToCover, _receiveAToken); + INSIDE_liquidationCall = false; + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_COL_asset); + + mathint final_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + mathint final_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + uint128 final_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + uint128 final_deficit = getReserveDataExtended(_COL_asset).deficit; + + //THE ASSERTION + assert + final_totSUP_aToken <= final_VB + final_totSUP_debt + final_deficit + DELTA + + getReserveNormalizedVariableDebt(e, _COL_asset) / RAY() + + getReserveNormalizedIncome(e, _COL_asset) / RAY() + ; +} + diff --git a/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-totSUP0-lemma.spec b/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-totSUP0-lemma.spec new file mode 100644 index 00000000..2aec38a4 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-totSUP0-lemma.spec @@ -0,0 +1,167 @@ + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + +methods { + function ReserveLogic.getNormalizedIncome_hook(uint256 ret_val, address aTokenAddress) + internal => getNormalizedIncome_hook_CVL(ret_val, aTokenAddress); + + // function ReserveLogic.getNormalizedDebt_hook(uint256 ret_val, address aTokenAddress) + // internal => getNormalizedDebt_hook_CVL(ret_val, aTokenAddress); + + //function ReserveLogic._updateIndexes_hook(DataTypes.ReserveData storage reserve, + // DataTypes.ReserveCache memory reserveCache) + // internal => _updateIndexes_hook_CVL(reserveCache); + + // function LiquidationLogic.HOOK_liquidation_after_updateState_DBT() + // internal => HOOK_liquidation_after_updateState_DBT_CVL(); + + function LiquidationLogic.HOOK_liquidation_after_burnCollateralATokens(uint256 actualCollateralToLiquidate) + internal => HOOK_liquidation_after_burnCollateralATokens_CVL(); + + + function IsolationModeLogic.updateIsolatedDebtIfIsolated( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveCache memory reserveCache, + uint256 repayAmount + ) internal => updateIsolatedDebtIfIsolatedCVL(); + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal returns (uint256,uint256,uint256,uint256) => + _calculateAvailableCollateralToLiquidateCVL(); +} + +function getNormalizedIncome_hook_CVL(uint256 ret_val, address aTokenAddress) { + assert INSIDE_liquidationCall => aTokenAddress==_COL_atoken; + assert INSIDE_liquidationCall => ret_val==_COL_liqIND; +} + +function getNormalizedDebt_hook_CVL(uint256 ret_val, address aTokenAddress) { + assert INSIDE_liquidationCall => aTokenAddress!=_COL_atoken; + + // assert INSIDE_liquidationCall => aTokenAddress==_DBT_atoken; + //assert INSIDE_liquidationCall => ret_val==_DBT_dbtIND; +} + +function _updateIndexes_hook_CVL(DataTypes.ReserveCache reserveCache) { + //assert reserveCache.aTokenAddress == _DBT_atoken => currentContract._reserves[_DBT_asset].liquidityIndex==_DBT_liqIND; + //assert reserveCache.aTokenAddress == _DBT_atoken => currentContract._reserves[_DBT_asset].variableBorrowIndex==_DBT_dbtIND; + + assert reserveCache.aTokenAddress == _COL_atoken => currentContract._reserves[_COL_asset].liquidityIndex==_COL_liqIND; + // assert reserveCache.aTokenAddress == _COL_atoken => currentContract._reserves[_COL_asset].variableBorrowIndex==_COL_dbtIND; +} + +// This is immediately after the call to updateState for the DBT token +//function HOOK_liquidation_after_updateState_DBT_CVL() { +// assert currentContract._reserves[_DBT_asset].liquidityIndex == _DBT_liqIND; +// assert currentContract._reserves[_DBT_asset].variableBorrowIndex == _DBT_dbtIND; +//} + +// This is immediately after the call to updateState for the COL token +function HOOK_liquidation_after_burnCollateralATokens_CVL() { + assert currentContract._reserves[_COL_asset].liquidityIndex == _COL_liqIND; + // assert currentContract._reserves[_COL_asset].variableBorrowIndex == _COL_dbtIND; +} + + +persistent ghost bool INSIDE_liquidationCall; + +persistent ghost address _DBT_asset; persistent ghost address _DBT_atoken; persistent ghost address _DBT_debt; +persistent ghost uint256 _DBT_liqIND; persistent ghost uint256 _DBT_dbtIND; + +persistent ghost address _COL_asset; persistent ghost address _COL_atoken; persistent ghost address _COL_debt; +persistent ghost uint256 _COL_liqIND; persistent ghost uint256 _COL_dbtIND; + + +function _calculateAvailableCollateralToLiquidateCVL() returns (uint256,uint256,uint256,uint256) { + uint256 a; uint256 b; uint256 c; uint256 d; // require c==0; + return (a,b,c,d); +} + + +// The function updateIsolatedDebtIfIsolated(...) only writes to the field isolationModeTotalDebt. +function updateIsolatedDebtIfIsolatedCVL() { + address asset; + havoc currentContract._reserves[asset].isolationModeTotalDebt; +} + + +function tokens_addresses_limitations_LQD(address asset, address atoken, address debt, + address asset2, address atoken2, address debt2 + ) { + require asset==100; require atoken==10; require debt==11; + require asset2==200; require atoken2==20; require debt2==21; +} + + +function configuration() { + init_state(); + + _DBT_atoken = currentContract._reserves[_DBT_asset].aTokenAddress; + _COL_atoken = currentContract._reserves[_COL_asset].aTokenAddress; + _DBT_debt = currentContract._reserves[_DBT_asset].variableDebtTokenAddress; + _COL_debt = currentContract._reserves[_COL_asset].variableDebtTokenAddress; + tokens_addresses_limitations_LQD(_DBT_asset, _DBT_atoken, _DBT_debt, + _COL_asset, _COL_atoken, _COL_debt); + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} + + + +/*===================================================================================== + Rule: same_indexes__liquidationCall + =====================================================================================*/ +rule same_indexes__liquidationCall(env e) { + INSIDE_liquidationCall = false; + configuration(); + + _DBT_liqIND = getReserveNormalizedIncome(e, _DBT_asset); + _COL_liqIND = getReserveNormalizedIncome(e, _COL_asset); + + _DBT_dbtIND = getReserveNormalizedVariableDebt(e, _DBT_asset); + _COL_dbtIND = getReserveNormalizedVariableDebt(e, _COL_asset); + + DataTypes.ReserveData reserve = getReserveDataExtended(_DBT_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_DBT_debt)!=0; + //require scaledTotalSupplyCVL(_COL_debt)!=0; + + // THE FUNCTION CALL + address user; uint256 debtToCover; bool receiveAToken; + + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, user, debtToCover, receiveAToken); + INSIDE_liquidationCall = false; + + uint256 __COL_liqIND_after = getReserveNormalizedIncome(e, _COL_asset); + assert __COL_liqIND_after == _COL_liqIND; + + // uint256 __COL_dbtIND_after = getReserveNormalizedVariableDebt(e, _COL_asset); + //assert __COL_dbtIND_after == _COL_dbtIND; +} + + diff --git a/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-totSUP0.spec b/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-totSUP0.spec new file mode 100644 index 00000000..5dbf8555 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/liquidationCall/COLasset-totSUP0.spec @@ -0,0 +1,146 @@ + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + +methods { + function ReserveLogic.getNormalizedIncome(DataTypes.ReserveData storage reserve) + internal returns (uint256) => _COL_liqIND; + + function LiquidationLogic.HOOK_liquidation_after_burnCollateralATokens(uint256 actualCollateralToLiquidate) + internal => HOOK_liquidation_after_burnCollateralATokens_CVL(); + + + function IsolationModeLogic.updateIsolatedDebtIfIsolated( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveCache memory reserveCache, + uint256 repayAmount + ) internal => updateIsolatedDebtIfIsolatedCVL(); + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal returns (uint256,uint256,uint256,uint256) => + _calculateAvailableCollateralToLiquidateCVL(); +} + +// This is immediately after the call to updateState for the COL token +function HOOK_liquidation_after_burnCollateralATokens_CVL() { + require currentContract._reserves[_COL_asset].liquidityIndex == _COL_liqIND; + // require currentContract._reserves[_COL_asset].variableBorrowIndex == _COL_dbtIND; +} + +function _calculateAvailableCollateralToLiquidateCVL() returns (uint256,uint256,uint256,uint256) { + uint256 a; uint256 b; uint256 c; uint256 d; // require c==0; + return (a,b,c,d); +} + +// The function updateIsolatedDebtIfIsolated(...) only writes to the field isolationModeTotalDebt. +function updateIsolatedDebtIfIsolatedCVL() { + address asset; + havoc currentContract._reserves[asset].isolationModeTotalDebt; +} + + +persistent ghost bool INSIDE_liquidationCall; + +persistent ghost address _DBT_asset; persistent ghost address _DBT_atoken; persistent ghost address _DBT_debt; +persistent ghost uint256 _DBT_liqIND; +persistent ghost uint256 _DBT_dbtIND; + +persistent ghost address _COL_asset; persistent ghost address _COL_atoken; persistent ghost address _COL_debt; +persistent ghost uint256 _COL_liqIND {axiom _COL_liqIND >= 10^27;} +persistent ghost uint256 _COL_dbtIND; + + + +function tokens_addresses_limitations_LQD(address asset, address atoken, address debt, + address asset2, address atoken2, address debt2 + ) { + require asset==100; require atoken==10; require debt==11; + require asset2==200; require atoken2==20; require debt2==21; +} + + +function configuration() { + init_state(); + + _DBT_atoken = currentContract._reserves[_DBT_asset].aTokenAddress; + _COL_atoken = currentContract._reserves[_COL_asset].aTokenAddress; + _DBT_debt = currentContract._reserves[_DBT_asset].variableDebtTokenAddress; + _COL_debt = currentContract._reserves[_COL_asset].variableDebtTokenAddress; + tokens_addresses_limitations_LQD(_DBT_asset, _DBT_atoken, _DBT_debt, + _COL_asset, _COL_atoken, _COL_debt); + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} + + + +/*===================================================================================== + Rule: solvency__liquidationCall_totDebt_of_COLasset_EQ_0 + =====================================================================================*/ +rule solvency__liquidationCall_totDebt_of_COLasset_EQ_0(env e) { + INSIDE_liquidationCall = false; + configuration(); + + DataTypes.ReserveData reserve = getReserveDataExtended(_COL_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + //uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal //+ __totSUP_debt + CONST; + ; + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + require scaledTotalSupplyCVL(_COL_debt)==0; + assert __totSUP_debt==0; + + // THE FUNCTION CALL + require _COL_asset != _DBT_asset; + address _user; uint256 _debtToCover; bool _receiveAToken; + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, _user, _debtToCover, _receiveAToken); + INSIDE_liquidationCall = false; + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_COL_asset); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + + assert __totSUP_debt__==0; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ /*+ __totSUP_debt__*/ // + CONST + + reserve2.liquidityIndex / RAY() + ; +} + + + + + + diff --git a/certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-common.spec b/certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-common.spec new file mode 100644 index 00000000..e5c90e0b --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-common.spec @@ -0,0 +1,74 @@ + + +persistent ghost address _DBT_asset; persistent ghost address _DBT_atoken; persistent ghost address _DBT_debt; +persistent ghost address _COL_asset; persistent ghost address _COL_atoken; persistent ghost address _COL_debt; + + +/*================================================================================================ + Summarizations + ================================================================================================*/ +methods { + function IsolationModeLogic.updateIsolatedDebtIfIsolated( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveCache memory reserveCache, + uint256 repayAmount + ) internal => updateIsolatedDebtIfIsolatedCVL(); + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal returns (uint256,uint256,uint256,uint256) => + _calculateAvailableCollateralToLiquidateCVL(); +} + + +/*================================================================================================ + Functions for summarizations + ================================================================================================*/ +function _calculateAvailableCollateralToLiquidateCVL() returns (uint256,uint256,uint256,uint256) { + uint256 a; uint256 b; uint256 c; uint256 d; + return (a,b,c,d); +} + +// The function updateIsolatedDebtIfIsolated(...) only writes to the field isolationModeTotalDebt. +function updateIsolatedDebtIfIsolatedCVL() { + address asset; + havoc currentContract._reserves[asset].isolationModeTotalDebt; +} + + + +/*================================================================================================ + General Function + ================================================================================================*/ +function tokens_addresses_limitations_LQD() { + // _DBT_asset=100; _DBT_atoken=10; _DBT_debt=11; + // _COL_asset=200; _COL_atoken=20; _COL_debt=21; + + require _DBT_asset!=0; require _COL_asset!=0; + + require _DBT_asset!=_COL_asset; + require _DBT_atoken != _COL_atoken; + require _DBT_debt != _COL_debt; +} + +function configuration() { + init_state(); + tokens_addresses_limitations_LQD(); + + require currentContract._reserves[_DBT_asset].aTokenAddress == _DBT_atoken; + require currentContract._reserves[_COL_asset].aTokenAddress == _COL_atoken; + require currentContract._reserves[_DBT_asset].variableDebtTokenAddress == _DBT_debt; + require currentContract._reserves[_COL_asset].variableDebtTokenAddress == _COL_debt; + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} diff --git a/certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-lemma.spec b/certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-lemma.spec new file mode 100644 index 00000000..2be7c208 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-lemma.spec @@ -0,0 +1,92 @@ + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; +import "DBTasset-common.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + +persistent ghost bool INSIDE_liquidationCall; +persistent ghost bool INSIDE_burnBadDebt; + +persistent ghost uint256 _DBT_liqIND; persistent ghost uint256 _DBT_dbtIND; + + + +/*================================================================================================ + Summarizations of HOOKS function + ================================================================================================*/ +methods { + function ReserveLogic.getNormalizedIncome_hook(uint256 ret_val, address aTokenAddress) + internal => getNormalizedIncome_hook_CVL(ret_val, aTokenAddress); + + function ReserveLogic.getNormalizedDebt_hook(uint256 ret_val, address aTokenAddress) + internal => getNormalizedDebt_hook_CVL(ret_val, aTokenAddress); + + function LiquidationLogic.HOOK_liquidation_after_updateState_DBT() + internal => HOOK_liquidation_after_updateState_DBT_CVL(); + + function LiquidationLogic.HOOK_liquidation_before_burnBadDebt() + internal with (env e) => HOOK_liquidation_before_burnBadDebt_CVL(e); + + function LiquidationLogic.HOOK_liquidation_after_burnBadDebt() + internal with (env e) => HOOK_liquidation_after_burnBadDebt_CVL(e); +} + + +function getNormalizedIncome_hook_CVL(uint256 ret_val, address aTokenAddress) {} + +function getNormalizedDebt_hook_CVL(uint256 ret_val, address aTokenAddress) { + assert (INSIDE_liquidationCall && !INSIDE_burnBadDebt) => aTokenAddress==_DBT_atoken; + assert (INSIDE_liquidationCall && !INSIDE_burnBadDebt) => ret_val==_DBT_dbtIND; +} + + +// This is immediately after the call to updateState for the DBT token +function HOOK_liquidation_after_updateState_DBT_CVL() { + assert currentContract._reserves[_DBT_asset].liquidityIndex == _DBT_liqIND; + assert currentContract._reserves[_DBT_asset].variableBorrowIndex == _DBT_dbtIND; +} + +function HOOK_liquidation_before_burnBadDebt_CVL(env e) {INSIDE_burnBadDebt = true;} +function HOOK_liquidation_after_burnBadDebt_CVL(env e) {INSIDE_burnBadDebt = false;} + + + +/*===================================================================================== + Rule: same_indexes__liquidationCall + =====================================================================================*/ +rule same_indexes__liquidationCall(env e) { + INSIDE_liquidationCall = false; + INSIDE_burnBadDebt = false; + configuration(); + + _DBT_liqIND = getReserveNormalizedIncome(e, _DBT_asset); + _DBT_dbtIND = getReserveNormalizedVariableDebt(e, _DBT_asset); + + DataTypes.ReserveData reserve = getReserveDataExtended(_DBT_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_DBT_debt)!=0; + + // THE FUNCTION CALL + address user; uint256 debtToCover; bool receiveAToken; + + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, user, debtToCover, receiveAToken); + INSIDE_liquidationCall = false; + + uint256 __DBT_liqIND_after = getReserveNormalizedIncome(e, _DBT_asset); + assert __DBT_liqIND_after == _DBT_liqIND; + + uint256 __DBT_dbtIND_after = getReserveNormalizedVariableDebt(e, _DBT_asset); + assert __DBT_dbtIND_after == _DBT_dbtIND; +} diff --git a/certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-main.spec b/certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-main.spec new file mode 100644 index 00000000..83975a10 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-main.spec @@ -0,0 +1,301 @@ + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; +import "DBTasset-common.spec"; + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + +persistent ghost bool INSIDE_liquidationCall; +persistent ghost bool INSIDE_burnBadDebt; + +persistent ghost bool HASNoCollateralLeft; +persistent ghost uint256 ACTUAL; +persistent ghost uint256 USER_RESERVE_DEBT; + +persistent ghost uint256 _DBT_liqIND {axiom _DBT_liqIND >= 10^27;} +persistent ghost uint256 _DBT_dbtIND {axiom _DBT_dbtIND >= 10^27;} + + +persistent ghost mathint DELTA; +persistent ghost mathint ORIG_totSUP_aToken; +persistent ghost mathint ORIG_totSUP_debt; +persistent ghost uint128 ORIG_VB; +persistent ghost uint128 ORIG_deficit; + +persistent ghost mathint INTR_totSUP_aToken; +persistent ghost mathint INTR_totSUP_debt; +persistent ghost uint128 INTR_VB; +persistent ghost uint128 INTR_deficit; + +persistent ghost mathint INTR2_totSUP_aToken; +persistent ghost mathint INTR2_totSUP_debt; +persistent ghost uint128 INTR2_VB; +persistent ghost uint128 INTR2_deficit; + +persistent ghost mathint INTR3_totSUP_aToken; +persistent ghost mathint INTR3_totSUP_debt; +persistent ghost uint128 INTR3_VB; +persistent ghost uint128 INTR3_deficit; + + +/*================================================================================================ + Summarizations + ================================================================================================*/ +methods { + function ReserveLogic.getNormalizedIncome(DataTypes.ReserveData storage reserve) + internal returns (uint256) => getNormalizedIncome_CVL(); + + function ReserveLogic.getNormalizedDebt(DataTypes.ReserveData storage reserve) + internal returns (uint256) => getNormalizedDebt_CVL(); + + function LiquidationLogic._burnBadDebt( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + uint256 reservesCount, + address user + ) internal with (env e) => _burnBadDebt_CVL(e); +} + +function getNormalizedIncome_CVL() returns uint256 { + if (INSIDE_liquidationCall) { + uint256 col_index; + return col_index; + } + else + return _DBT_liqIND; +} + +function getNormalizedDebt_CVL() returns uint256 { + if (!INSIDE_burnBadDebt) + return _DBT_dbtIND; + else { + uint256 val; + return val; + } +} + +function _burnBadDebt_CVL(env e) { + INSIDE_liquidationCall = false; + + mathint curr_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + mathint curr_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + uint128 curr_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + uint128 curr_deficit = getReserveDataExtended(_DBT_asset).deficit; + + // ert INTR2_totSUP_aToken <= INTR2_VB + INTR2_totSUP_debt + INTR2_deficit + _DBT_dbtIND / RAY() + DELTA; + if (curr_totSUP_aToken <= curr_VB + curr_totSUP_debt + curr_deficit + DELTA + _DBT_dbtIND / RAY()) { + havoc_all(e); + mathint after_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + mathint after_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + uint128 after_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + uint128 after_deficit = getReserveDataExtended(_DBT_asset).deficit; + + require + after_totSUP_aToken <= + after_VB + after_totSUP_debt + after_deficit + DELTA + + _DBT_dbtIND / RAY() + _DBT_dbtIND / RAY() + ; + + require + getReserveDataExtended(_DBT_asset).variableBorrowIndex == getNormalizedDebt_CVL(); + + require + getReserveDataExtended(_DBT_asset).liquidityIndex == _DBT_liqIND; + } + INSIDE_liquidationCall = true; +} + + +/*================================================================================================ + Summarizations of HOOKS function + ================================================================================================*/ +methods { + function LiquidationLogic.HOOK_liquidation_after_updateState_DBT() + internal => HOOK_liquidation_after_updateState_DBT_CVL(); + + function LiquidationLogic.HOOK_liquidation_before_burnDebtTokens(bool hasNoCollateralLeft) + internal with (env e) => HOOK_liquidation_before_burnDebtTokens_CVL(e, hasNoCollateralLeft); + + function LiquidationLogic.HOOK_liquidation_after_burnDebtTokens + (bool hasNoCollateralLeft, uint256 actualDebtToLiquidate, uint256 userReserveDebt) internal with (env e) => + HOOK_liquidation_after_burnDebtTokens_CVL(e, hasNoCollateralLeft, actualDebtToLiquidate, userReserveDebt); + + function LiquidationLogic.HOOK_liquidation_before_burnBadDebt() + internal with (env e) => HOOK_liquidation_before_burnBadDebt_CVL(e); + + function LiquidationLogic.HOOK_burnBadDebt_inside_loop(address reserveAddress) + internal with (env e) => HOOK_burnBadDebt_inside_loop_CVL(e, reserveAddress); + + function LiquidationLogic.HOOK_liquidation_after_burnBadDebt() + internal with (env e) => HOOK_liquidation_after_burnBadDebt_CVL(e); +} + + +// This is immediately after the call to updateState for the DBT token +function HOOK_liquidation_after_updateState_DBT_CVL() { + require currentContract._reserves[_DBT_asset].liquidityIndex == _DBT_liqIND; + require currentContract._reserves[_DBT_asset].variableBorrowIndex == _DBT_dbtIND; +} + +function HOOK_liquidation_before_burnDebtTokens_CVL(env e, bool hasNoCollateralLeft) { + INSIDE_liquidationCall = false; + + mathint curr_totSUP_aToken; curr_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + mathint curr_totSUP_debt; curr_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + uint128 curr_VB; curr_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + uint128 curr_deficit; curr_deficit = getReserveDataExtended(_DBT_asset).deficit; + + assert ORIG_totSUP_aToken == curr_totSUP_aToken; + assert ORIG_totSUP_debt == curr_totSUP_debt; + assert ORIG_VB == curr_VB; + assert ORIG_deficit == curr_deficit; + + assert ORIG_totSUP_aToken <= ORIG_VB + ORIG_totSUP_debt + ORIG_deficit + DELTA; + + INSIDE_liquidationCall = true; +} + +function HOOK_liquidation_after_burnDebtTokens_CVL(env e, bool hasNoCollateralLeft, + uint256 actualDebtToLiquidate, uint256 userReserveDebt) { + INSIDE_liquidationCall = false; + HASNoCollateralLeft = hasNoCollateralLeft; + ACTUAL = actualDebtToLiquidate; + USER_RESERVE_DEBT = userReserveDebt; + + INTR_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + INTR_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + INTR_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + INTR_deficit = getReserveDataExtended(_DBT_asset).deficit; + + + assert ORIG_totSUP_aToken == INTR_totSUP_aToken; + assert to_mathint(INTR_VB) == ORIG_VB + actualDebtToLiquidate; + assert !hasNoCollateralLeft => + INTR_totSUP_debt >= ORIG_totSUP_debt - actualDebtToLiquidate - _DBT_dbtIND / RAY(); + + assert hasNoCollateralLeft => + INTR_totSUP_debt >= ORIG_totSUP_debt - userReserveDebt - _DBT_dbtIND / RAY(); + assert hasNoCollateralLeft => + to_mathint(INTR_deficit) == ORIG_deficit + (userReserveDebt - actualDebtToLiquidate); + + // THE MAIN ASSERTION + assert INTR_totSUP_aToken <= INTR_VB + INTR_totSUP_debt + INTR_deficit + _DBT_dbtIND / RAY() + DELTA; + + INSIDE_liquidationCall = true; +} + + +function HOOK_liquidation_before_burnBadDebt_CVL(env e) { + INSIDE_liquidationCall = false; + + INTR2_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + INTR2_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + INTR2_deficit = getReserveDataExtended(_DBT_asset).deficit; + INTR2_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + + assert INTR2_totSUP_aToken == INTR_totSUP_aToken; + assert INTR2_totSUP_debt == INTR_totSUP_debt; + assert INTR2_deficit == INTR_deficit; + assert INTR2_VB == INTR_VB; + + // THE MAIN ASSERTION + assert INTR2_totSUP_aToken <= INTR2_VB + INTR2_totSUP_debt + INTR2_deficit + _DBT_dbtIND / RAY() + DELTA; + + INSIDE_burnBadDebt = true; + INSIDE_liquidationCall = true; +} + + +function HOOK_burnBadDebt_inside_loop_CVL(env e, address reserveAddress) { + // assert reserveAddress != _DBT_asset; + //require reserveAddress != _DBT_asset; +} + +function HOOK_liquidation_after_burnBadDebt_CVL(env e) { + INSIDE_burnBadDebt = false; + INSIDE_liquidationCall = false; + + INTR3_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + INTR3_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + INTR3_deficit = getReserveDataExtended(_DBT_asset).deficit; + INTR3_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + + assert INTR3_totSUP_aToken == INTR_totSUP_aToken; + assert INTR3_totSUP_debt == INTR_totSUP_debt; + assert INTR3_deficit == INTR_deficit; + assert INTR3_VB == INTR_VB; + + // THE MAIN ASSERTION + assert INTR3_totSUP_aToken <= INTR3_VB + INTR3_totSUP_debt + INTR3_deficit + _DBT_dbtIND / RAY() + DELTA; + + INSIDE_liquidationCall = true; +} + + + +/*===================================================================================== + Rule: solvency__liquidationCall + =====================================================================================*/ +rule solvency__liquidationCall_DBTasset(env e) { + INSIDE_liquidationCall = false; + INSIDE_burnBadDebt = false; + configuration(); + + DataTypes.ReserveData reserve = getReserveDataExtended(_DBT_asset); + require getReserveAddressById(reserve.id)==_DBT_asset; + require reserve.id!=0 => getReserveAddressById(0) != _DBT_asset; + //_reservesList[reserve.id] = _DBT_asset; + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + ORIG_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + ORIG_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + ORIG_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + ORIG_deficit = getReserveDataExtended(_DBT_asset).deficit; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + require ORIG_totSUP_aToken <= ORIG_VB + ORIG_totSUP_debt + ORIG_deficit + DELTA; + + require ORIG_totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_DBT_debt)!=0; + require exists_debt; + + // THE FUNCTION CALL + address user; uint256 debtToCover; bool receiveAToken; + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, user, debtToCover, receiveAToken); + INSIDE_liquidationCall = false; + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_DBT_asset); + + mathint FINAL_totSUP_aToken; FINAL_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + mathint FINAL_totSUP_debt; FINAL_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + uint128 FINAL_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + uint128 FINAL_deficit = getReserveDataExtended(_DBT_asset).deficit; + + assert FINAL_totSUP_aToken == INTR_totSUP_aToken; + assert FINAL_totSUP_debt == INTR_totSUP_debt; + assert FINAL_deficit == INTR_deficit; + assert FINAL_VB == INTR_VB; + + + //THE ASSERTION + assert + FINAL_totSUP_aToken <= FINAL_VB + FINAL_totSUP_debt + FINAL_deficit + DELTA + + 2*reserve2.variableBorrowIndex / RAY() + ; + +} + diff --git a/certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-totSUP0.spec b/certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-totSUP0.spec new file mode 100644 index 00000000..7201829c --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/liquidationCall/DBTasset-totSUP0.spec @@ -0,0 +1,50 @@ + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +//import "../common/validation_functions.spec"; +import "DBTasset-common.spec"; + + + +methods { + function LiquidationLogic.HOOK_liquidation_before_validateLiquidationCall(uint256 userTotalDebt) + internal => HOOK_liquidation_before_validateLiquidationCall_CVL(userTotalDebt); +} + +function HOOK_liquidation_before_validateLiquidationCall_CVL(uint256 userTotalDebt) { + assert userTotalDebt==0; +} + + + +/*===================================================================================== + Rule: liquidationCall_must_revert_if_totDebt_of_DBTasset_EQ_0 + =====================================================================================*/ +rule liquidationCall_must_revert_if_totDebt_of_DBTasset_EQ_0(env e) { + init_state(); + + configuration(); + + DataTypes.ReserveData reserve = getReserveDataExtended(_DBT_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // BASIC ASSUMPTION FOR THE RULE + // require isVirtualAccActive(reserve.configuration.data); + + require scaledTotalSupplyCVL(_DBT_debt)==0; + + // THE FUNCTION CALL + address user; uint256 debtToCover; bool receiveAToken; + // TODO: prove the following basic erc20 property + require aTokenBalanceOfCVL(_DBT_debt,user,e) <= scaledTotalSupplyCVL(_DBT_debt); + liquidationCall@withrevert(e, _COL_asset, _DBT_asset, user, debtToCover, receiveAToken); + + assert lastReverted; +} + + + diff --git a/certora/experimental/specs/Aave/solvency/liquidationCall/burnBadDebt-assetINloop.spec b/certora/experimental/specs/Aave/solvency/liquidationCall/burnBadDebt-assetINloop.spec new file mode 100644 index 00000000..1c32ce15 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/liquidationCall/burnBadDebt-assetINloop.spec @@ -0,0 +1,146 @@ + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + +/*================================================================================================ + Rules in the file: + PASS: https://prover.certora.com/output/66114/3e82c00d6ccd433988bedffbf910c656/?anonymousKey=aaca4f1c23e82f92a0dbfac22998c6a8511f886e + ================================================================================================*/ + + +persistent ghost address ATOKEN; persistent ghost address DEBT; + +persistent ghost mathint DELTA; +persistent ghost uint256 AMOUNT; + +persistent ghost mathint ORIG_totSUP_aToken; +persistent ghost mathint ORIG_totSUP_debt; +persistent ghost uint128 ORIG_VB; +persistent ghost uint128 ORIG_deficit; + +persistent ghost mathint INTR1_totSUP_aToken; +persistent ghost mathint INTR1_totSUP_debt; +persistent ghost uint128 INTR1_VB; +persistent ghost uint128 INTR1_deficit; + +persistent ghost mathint INTR2_totSUP_aToken; +persistent ghost mathint INTR2_totSUP_debt; +persistent ghost uint128 INTR2_VB; +persistent ghost uint128 INTR2_deficit; + + + +methods { + function LiquidationLogic.HOOK_burnBadDebt_inside_loop(address reserveAddress) + internal with (env e) => HOOK_burnBadDebt_inside_loop_CVL(e, reserveAddress); + + function LiquidationLogic.HOOK_burnBadDebt_before_burnDebtTokens(address reserveAddress, uint256 amount) + internal with (env e) => HOOK_burnBadDebt_before_burnDebtTokens_CVL(e,reserveAddress,amount); + + function LiquidationLogic.HOOK_burnBadDebt_after_burnDebtTokens(address reserveAddress) + internal with (env e) => HOOK_burnBadDebt_after_burnDebtTokens_CVL(e,reserveAddress); +} + +function HOOK_burnBadDebt_inside_loop_CVL(env e, address reserveAddress) { + assert reserveAddress == ASSET; +} + +function HOOK_burnBadDebt_before_burnDebtTokens_CVL(env e, address reserveAddress, uint256 amount) { + AMOUNT = amount; + INTR1_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + INTR1_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + INTR1_VB = getReserveDataExtended(ASSET).virtualUnderlyingBalance; + INTR1_deficit = getReserveDataExtended(ASSET).deficit; + + assert INTR1_totSUP_aToken == ORIG_totSUP_aToken; + assert INTR1_totSUP_debt == ORIG_totSUP_debt; + assert INTR1_VB == ORIG_VB; + assert INTR1_deficit == ORIG_deficit; +} + +function HOOK_burnBadDebt_after_burnDebtTokens_CVL(env e, address reserveAddress) { + INTR2_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + INTR2_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + INTR2_VB = getReserveDataExtended(ASSET).virtualUnderlyingBalance; + INTR2_deficit = getReserveDataExtended(ASSET).deficit; + + assert INTR2_totSUP_aToken == INTR1_totSUP_aToken; + assert INTR2_VB == INTR1_VB; + assert INTR2_deficit == INTR1_deficit + AMOUNT; + assert INTR2_totSUP_debt >= INTR1_totSUP_debt - AMOUNT - getReserveDataExtended(ASSET).variableBorrowIndex / RAY(); + + assert INTR2_totSUP_aToken <= INTR2_VB + INTR2_totSUP_debt + INTR2_deficit + DELTA + + getReserveDataExtended(ASSET).variableBorrowIndex / RAY() ; +} + + + +function configuration(address asset) { + init_state(); + + // Different assets have different Debt tokens. + require forall address a1. forall address a2. + a1!=a2 => currentContract._reserves[a1].variableDebtTokenAddress != currentContract._reserves[a2].variableDebtTokenAddress; + + ATOKEN = currentContract._reserves[asset].aTokenAddress; + DEBT = currentContract._reserves[asset].variableDebtTokenAddress; + tokens_addresses_limitations(ATOKEN,DEBT,asset); // this call makes (among other things ASSET==asset) + + require aTokenToUnderlying[ATOKEN]==asset; require aTokenToUnderlying[DEBT]==asset; +} + + + + + +rule solvency__burnBadDebt(env e, address _asset) { + configuration(_asset); + + DataTypes.ReserveData reserve = getReserveDataExtended(ASSET); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + uint256 __liqInd_before = getReserveNormalizedIncome(e, ASSET); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, ASSET); + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + ORIG_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + ORIG_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + ORIG_VB = getReserveDataExtended(ASSET).virtualUnderlyingBalance; + ORIG_deficit = getReserveDataExtended(ASSET).deficit; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + // We assume that the ASSET is processed in the loop (of _burnBadDebt). The opposite is considered in the + // file burnBadDebt-assetNOTINloop.spec + require currentContract._reservesCount >= 1 && getReservesList()[0]==ASSET; + + //THE MAIN REQUIREMENT + require ORIG_totSUP_aToken <= ORIG_VB + ORIG_totSUP_debt + ORIG_deficit + DELTA; + + require ORIG_totSUP_aToken <= 10^27; // Without this requirement we get a failure. + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + address user; _burnBadDebt_WRP(e, user); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(ASSET); + mathint FINAL_totSUP_aToken; FINAL_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + mathint FINAL_totSUP_debt; FINAL_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + uint128 FINAL_VB = getReserveDataExtended(ASSET).virtualUnderlyingBalance; + uint128 FINAL_deficit = getReserveDataExtended(ASSET).deficit; + + //THE ASSERTION + assert FINAL_totSUP_aToken <= FINAL_VB + FINAL_totSUP_debt + FINAL_deficit + DELTA + + getReserveNormalizedVariableDebt(e, ASSET) / RAY(); +} + diff --git a/certora/experimental/specs/Aave/solvency/liquidationCall/burnBadDebt-assetNOTINloop.spec b/certora/experimental/specs/Aave/solvency/liquidationCall/burnBadDebt-assetNOTINloop.spec new file mode 100644 index 00000000..2d189db5 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/liquidationCall/burnBadDebt-assetNOTINloop.spec @@ -0,0 +1,145 @@ + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + +/*================================================================================================ + Rules in the file: + PASS: https://prover.certora.com/output/66114/cd7e0f9d2fc44e16a1f2c80918ec0fba/?anonymousKey=04d6f84469e7d81aea57a662f69e2f9bf8fae266 + ================================================================================================*/ + + +persistent ghost address ATOKEN; persistent ghost address DEBT; + +persistent ghost mathint DELTA; + +persistent ghost mathint ORIG_totSUP_aToken; +persistent ghost mathint ORIG_totSUP_debt; +persistent ghost uint128 ORIG_VB; +persistent ghost uint128 ORIG_deficit; + +persistent ghost mathint INTR1_totSUP_aToken; +persistent ghost mathint INTR1_totSUP_debt; +persistent ghost uint128 INTR1_VB; +persistent ghost uint128 INTR1_deficit; + +persistent ghost mathint INTR2_totSUP_aToken; +persistent ghost mathint INTR2_totSUP_debt; +persistent ghost uint128 INTR2_VB; +persistent ghost uint128 INTR2_deficit; + + + +methods { + function LiquidationLogic.HOOK_burnBadDebt_inside_loop(address reserveAddress) + internal with (env e) => HOOK_burnBadDebt_inside_loop_CVL(e, reserveAddress); + + function LiquidationLogic.HOOK_burnBadDebt_before_burnDebtTokens(address reserveAddress, uint256 amount) + internal with (env e) => HOOK_burnBadDebt_before_burnDebtTokens_CVL(e,reserveAddress,amount); + + function LiquidationLogic.HOOK_burnBadDebt_after_burnDebtTokens(address reserveAddress) + internal with (env e) => HOOK_burnBadDebt_after_burnDebtTokens_CVL(e,reserveAddress); +} + +function HOOK_burnBadDebt_inside_loop_CVL(env e, address reserveAddress) { + assert reserveAddress != ASSET; +} + +function HOOK_burnBadDebt_before_burnDebtTokens_CVL(env e, address reserveAddress, uint256 amount) { + INTR1_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + INTR1_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + INTR1_VB = getReserveDataExtended(ASSET).virtualUnderlyingBalance; + INTR1_deficit = getReserveDataExtended(ASSET).deficit; + + assert INTR1_totSUP_aToken == ORIG_totSUP_aToken; + assert INTR1_totSUP_debt == ORIG_totSUP_debt; + assert INTR1_VB == ORIG_VB; + assert INTR1_deficit == ORIG_deficit; +} + +function HOOK_burnBadDebt_after_burnDebtTokens_CVL(env e, address reserveAddress) { + INTR2_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + INTR2_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + INTR2_VB = getReserveDataExtended(ASSET).virtualUnderlyingBalance; + INTR2_deficit = getReserveDataExtended(ASSET).deficit; + + assert INTR2_totSUP_aToken == INTR1_totSUP_aToken; + assert INTR2_totSUP_debt == INTR1_totSUP_debt; + assert INTR2_VB == INTR1_VB; + assert INTR2_deficit == INTR1_deficit; +} + + +function configuration(address asset) { + init_state(); + + // Different assets have different Debt tokens. + require forall address a1. forall address a2. + a1!=a2 => currentContract._reserves[a1].variableDebtTokenAddress != currentContract._reserves[a2].variableDebtTokenAddress; + + ATOKEN = currentContract._reserves[asset].aTokenAddress; + DEBT = currentContract._reserves[asset].variableDebtTokenAddress; + tokens_addresses_limitations(ATOKEN,DEBT,asset); // this call makes (among other things ASSET==asset) + + require aTokenToUnderlying[ATOKEN]==asset; require aTokenToUnderlying[DEBT]==asset; +} + + + + +rule solvency__burnBadDebt(env e, address _asset) { + configuration(_asset); + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + ORIG_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + ORIG_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + ORIG_VB = getReserveDataExtended(_asset).virtualUnderlyingBalance; + ORIG_deficit = getReserveDataExtended(_asset).deficit; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + // We assume that the ASSET is NOT processed in the loop (of _burnBadDebt). The opposite is considered in the + // file burnBadDebt-assetINloop.spec + require currentContract._reservesCount < 1 || getReservesList()[0]!=ASSET; + + //THE MAIN REQUIREMENT + require ORIG_totSUP_aToken <= ORIG_VB + ORIG_totSUP_debt + ORIG_deficit + DELTA; + + require ORIG_totSUP_aToken <= 10^27; // Without this requirement we get a failure. + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + address user; _burnBadDebt_WRP(e, user); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + mathint FINAL_totSUP_aToken; FINAL_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + mathint FINAL_totSUP_debt; FINAL_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + uint128 FINAL_VB = getReserveDataExtended(_asset).virtualUnderlyingBalance; + uint128 FINAL_deficit = getReserveDataExtended(_asset).deficit; + + assert FINAL_totSUP_aToken==ORIG_totSUP_aToken; + assert FINAL_totSUP_debt==ORIG_totSUP_debt; + assert FINAL_VB==ORIG_VB; + assert FINAL_deficit==ORIG_deficit; + + //THE ASSERTION + assert + FINAL_totSUP_aToken <= FINAL_VB + FINAL_totSUP_debt + FINAL_deficit + DELTA + + getReserveNormalizedVariableDebt(e, ASSET) / RAY(); +} + diff --git a/certora/experimental/specs/Aave/solvency/liquidationCall/sanity.spec b/certora/experimental/specs/Aave/solvency/liquidationCall/sanity.spec new file mode 100644 index 00000000..29d8005b --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/liquidationCall/sanity.spec @@ -0,0 +1,126 @@ + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + +persistent ghost bool INSIDE_liquidationCall; +persistent ghost bool INSIDE_burnBadDebt; + +persistent ghost address _DBT_asset; persistent ghost address _DBT_atoken; persistent ghost address _DBT_debt; + +persistent ghost address _COL_asset; persistent ghost address _COL_atoken; persistent ghost address _COL_debt; +persistent ghost uint256 _COL_liqIND {axiom _COL_liqIND >= 10^27;} +persistent ghost uint256 _COL_dbtIND {axiom _COL_dbtIND >= 10^27;} + + +methods { + //TEMPORARY !!! we remove the following + // function LiquidationLogic._burnBadDebt( + // mapping(address => DataTypes.ReserveData) storage reservesData, + // mapping(uint256 => address) storage reservesList, + // DataTypes.UserConfigurationMap storage userConfig, + // uint256 reservesCount, + // address user + //) internal => NONDET; + + function ReserveLogic.getNormalizedIncome(DataTypes.ReserveData storage reserve) + internal returns (uint256) => _COL_liqIND; + + function ReserveLogic.getNormalizedDebt(DataTypes.ReserveData storage reserve) + internal returns (uint256) => getNormalizedDebt_CVL(); + + function IsolationModeLogic.updateIsolatedDebtIfIsolated( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveCache memory reserveCache, + uint256 repayAmount + ) internal => updateIsolatedDebtIfIsolatedCVL(); + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal returns (uint256,uint256,uint256,uint256) => + _calculateAvailableCollateralToLiquidateCVL(); +} + + +function getNormalizedDebt_CVL() returns uint256 { + if (INSIDE_liquidationCall) { + uint256 dbt_index; + return dbt_index; + } + else + return _COL_liqIND; +} + + +function _calculateAvailableCollateralToLiquidateCVL() returns (uint256,uint256,uint256,uint256) { + uint256 a; uint256 b; uint256 c; uint256 d; // require c==0; + return (a,b,c,d); +} + + +// The function updateIsolatedDebtIfIsolated(...) only writes to the field isolationModeTotalDebt. +function updateIsolatedDebtIfIsolatedCVL() { + address asset; + havoc currentContract._reserves[asset].isolationModeTotalDebt; +} + + + +function tokens_addresses_limitations_LQD(address asset, address atoken, address debt, + address asset2, address atoken2, address debt2 + ) { + require asset==100; require atoken==10; require debt==11; + require asset2==200; require atoken2==20; require debt2==21; + +} + + +function configuration() { + init_state(); + + _DBT_atoken = currentContract._reserves[_DBT_asset].aTokenAddress; + _COL_atoken = currentContract._reserves[_COL_asset].aTokenAddress; + _DBT_debt = currentContract._reserves[_DBT_asset].variableDebtTokenAddress; + _COL_debt = currentContract._reserves[_COL_asset].variableDebtTokenAddress; + tokens_addresses_limitations_LQD(_DBT_asset, _DBT_atoken, _DBT_debt, + _COL_asset, _COL_atoken, _COL_debt); + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} + +/*===================================================================================== + Rule: solvency__liquidationCall + =====================================================================================*/ +rule sanity__liquidationCall(env e) { + INSIDE_liquidationCall = false; + configuration(); + + // THE FUNCTION CALL + address _user; uint256 _debtToCover; bool _receiveAToken; + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, _user, _debtToCover, _receiveAToken); + INSIDE_liquidationCall = false; + + satisfy true; +} + diff --git a/certora/experimental/specs/Aave/solvency/repay/repay-NONindexSUMM.spec b/certora/experimental/specs/Aave/solvency/repay/repay-NONindexSUMM.spec new file mode 100644 index 00000000..5cc2deb1 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/repay/repay-NONindexSUMM.spec @@ -0,0 +1,135 @@ + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + +/*================================================================================================ + See the top comment at the file repay-indexSUMM.spec (this directory). + ================================================================================================*/ + + +/*===================================================================================== + Rule: same_indexes__repay + + The rule is the following lemma (assumed in the file repay-indexSUMM.spec. + + a: The return value of getNormalizedIncome(...) is the same before and after the function call + and: + b: If the scaled-total-supply of the variable-debt isn't 0, then the return value of getNormalizedDebt(...) + is the same before and after the function call. + Note that getNormalizedIncome(...) return the liquidity index, while getNormalizedDebt(...) returns + the variable-debt index. + + Status: PASS + Link: https://prover.certora.com/output/66114/17b4d0b1734948029433e1ab174ec431/?anonymousKey=35f254e9d99941b9760eda33d00434141edba7e2 + + =====================================================================================*/ +rule same_indexes__repay(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getNormalizedDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + bool exists_debt = scaledTotalSupplyCVL(_debt)!=0; + require exists_debt; + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repay(e, _asset, _amount, interestRateMode, onBehalfOf); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + uint128 __liqInd_afterS = reserve2.liquidityIndex; + uint128 __dbtInd_afterS = reserve2.variableBorrowIndex; + uint256 __liqInd_after = getNormalizedIncome(e, _asset); + uint256 __dbtInd_after = getNormalizedDebt(e, _asset); + + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + + assert __liqInd_after == __liqInd_before; + assert assert_uint256(__liqInd_afterS) == __liqInd_after; + + assert __dbtInd_after == __dbtInd_before; + assert assert_uint256(__dbtInd_afterS) == __dbtInd_after; +} + + + + + + +/*===================================================================================== + Rule: solvency__repay_totDbt_EQ_0 + Details: We prove that if scaled-total-supply of the variable-debt is 0, then repay reverts. + Status: PASS + Link: https://prover.certora.com/output/66114/17b4d0b1734948029433e1ab174ec431/?anonymousKey=35f254e9d99941b9760eda33d00434141edba7e2 + =====================================================================================*/ +rule solvency__repay_totDbt_EQ_0(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + require forall address a. balanceByToken[_debt][a] <= totalSupplyByToken[_debt]; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_debt)!=0; + require !exists_debt; + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repay@withrevert(e, _asset, _amount, interestRateMode, onBehalfOf); + assert lastReverted; +} + + diff --git a/certora/experimental/specs/Aave/solvency/repay/repay-indexSUMM.spec b/certora/experimental/specs/Aave/solvency/repay/repay-indexSUMM.spec new file mode 100644 index 00000000..c7e4e2d6 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/repay/repay-indexSUMM.spec @@ -0,0 +1,98 @@ + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory. + + For the case of the repay functions getNormalizedIncome(...), getNormalizedDebt(...) to return + specific values (see below in the method block). We also assume that after the repay function + is finished, that values will be the same as the values in the storage: + reserve-of-the-asset.liquidityIndex, and reserve-of-the-asset.variableBorrowIndex. + (This indeed holds under the assumption that the scaled-total-supply of the variable-debt isn't 0.) + + We prove that it is indeed the case, and we prove it in the file repay-NONindexSUMM.spec (in this + directory.) We also take care there of the non interesting case where scaled-total-supply of the + variable-debt == 0. + ================================================================================================*/ + +methods { + function ValidationLogic.validateRepay( + DataTypes.ReserveCache memory reserveCache, + uint256 amountSent, + DataTypes.InterestRateMode interestRateMode, + address onBehalfOf, + uint256 debt + ) internal => NONDET; + + function ReserveLogic.getNormalizedIncome(DataTypes.ReserveData storage reserve) + internal returns (uint256) => LIQUIDITY_INDEX; + + function ReserveLogic.getNormalizedDebt(DataTypes.ReserveData storage reserve) + internal returns (uint256) => DEBT_INDEX; +} + +ghost uint256 LIQUIDITY_INDEX {axiom LIQUIDITY_INDEX >= 10^27;} +ghost uint256 DEBT_INDEX {axiom DEBT_INDEX >= 10^27;} + + +/*===================================================================================== + Rule: solvency__repay + Status: PASS + Link: https://prover.certora.com/output/66114/c3bf7d127763413a885fcae19671d662/?anonymousKey=f3f236570d484eeb903cab1cd345d20beb5f664b + =====================================================================================*/ +rule solvency__repay(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_debt)!=0; + require exists_debt; + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repay(e, _asset, _amount, interestRateMode, onBehalfOf); + + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + require assert_uint256(reserve2.liquidityIndex) == LIQUIDITY_INDEX; + require assert_uint256(reserve2.variableBorrowIndex) == DEBT_INDEX; + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.variableBorrowIndex / RAY() ; +} + diff --git a/certora/experimental/specs/Aave/solvency/repayWithATokens/repayWithATokens-HOOKS.spec b/certora/experimental/specs/Aave/solvency/repayWithATokens/repayWithATokens-HOOKS.spec new file mode 100644 index 00000000..44cf54ee --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/repayWithATokens/repayWithATokens-HOOKS.spec @@ -0,0 +1,216 @@ +import "../../../ERC20/WETHcvl.spec"; +import "../../../ERC721/erc721.spec"; +import "../../../ERC1967/erc1967.spec"; +import "../../../PriceAggregators/chainlink.spec"; +import "../../../PriceAggregators/tellor.spec"; + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; +import "../../PriceOracleSentinel.spec"; +import "../../PriceOracle.spec"; +import "../../ACLManager.spec"; +import "../../FlashLoanReceiver.spec"; + +import "../../../problems.spec"; +import "../../../unresolved.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + +import "../../../generic.spec"; // pick additional rules from here + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + + In order to deal with time-outs, we helped the prover by adding several "hooks" inside the function + executeRepay. In these hooks (see their summarizations below) we added several asserts, hence + the prover proves them, and later uses them as requirement for later asserts. + We run with --multi_asset_check. + ================================================================================================*/ + + + +methods { + function _.rayMul(uint256 a, uint256 b) internal => rayMulCVLPrecise(a, b) expect uint256; // not optimized well by Prover + function _.rayDiv(uint256 a, uint256 b) internal => rayDivCVLPrecise(a, b) expect uint256; // seems to be optimized well by Prover + + function ValidationLogic.validateRepay( + DataTypes.ReserveCache memory reserveCache, + uint256 amountSent, + DataTypes.InterestRateMode interestRateMode, + address onBehalfOf, + uint256 debt + ) internal => NONDET; + + function BorrowLogic.repay_hook_1(DataTypes.ReserveCache memory reserveCache) + internal => repay_hook_1_CVL(reserveCache); + + function BorrowLogic.repay_hook_2(DataTypes.ReserveCache memory reserveCache) + internal with (env e) => repay_hook_2_CVL(e, reserveCache); + + function BorrowLogic.repay_hook_3(DataTypes.ReserveCache memory reserveCache, uint256 paybackAmount) + internal with (env e) => repay_hook_3_CVL(e, reserveCache, paybackAmount); + + function BorrowLogic.repay_hook_4(DataTypes.ReserveCache memory reserveCache) + internal with (env e) => repay_hook_4_CVL(e, reserveCache); + + //function BorrowLogic.dummy_get_amount() internal returns(uint256) => NONDET; + + function _.havoc_all_dummy() external => HAVOC_ALL; +} + +function repay_hook_1_CVL(DataTypes.ReserveCache res) { + assert currentContract._reserves[ASSET].liquidityIndex == require_uint128(LIQUIDITY_INDEX); + assert res.nextLiquidityIndex == LIQUIDITY_INDEX; + + assert currentContract._reserves[ASSET].variableBorrowIndex == require_uint128(DEBT_INDEX); + assert res.nextVariableBorrowIndex == DEBT_INDEX; +} + +function repay_hook_2_CVL(env e, DataTypes.ReserveCache res) { + assert res.nextLiquidityIndex==LIQUIDITY_INDEX; + assert res.nextVariableBorrowIndex == DEBT_INDEX; + assert getReserveNormalizedIncome(e, ASSET) == LIQUIDITY_INDEX; + assert getReserveNormalizedVariableDebt(e, ASSET) == DEBT_INDEX; + + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + + // THE MAIN ASSERTION + assert to_mathint(__totSUP_aToken) <= __totSUP_debt + DELTA; +} + +function repay_hook_3_CVL(env e, DataTypes.ReserveCache res, uint256 paybackAmount) { + assert res.nextLiquidityIndex==LIQUIDITY_INDEX; + assert res.nextVariableBorrowIndex == DEBT_INDEX; + assert getReserveNormalizedIncome(e, ASSET) == LIQUIDITY_INDEX; + assert getReserveNormalizedVariableDebt(e, ASSET) == DEBT_INDEX; + assert currentContract._reserves[ASSET].lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert assert_uint256(currentContract._reserves[ASSET].liquidityIndex) == LIQUIDITY_INDEX; + assert assert_uint256(currentContract._reserves[ASSET].variableBorrowIndex) == DEBT_INDEX; + assert VB == currentContract._reserves[ASSET].virtualUnderlyingBalance; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + + // THE MAIN ASSERTION + assert to_mathint(__totSUP_aToken) - paybackAmount <= __totSUP_debt + DEBT_INDEX/RAY() + DELTA; + havoc_all(e); + + // The following requirements are all safe because we've just proved them before the havoc_all. + mathint __totSUP_aToken2; __totSUP_aToken2 = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + mathint __totSUP_debt2; __totSUP_debt2 = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + require res.nextLiquidityIndex==LIQUIDITY_INDEX; + require res.nextVariableBorrowIndex == DEBT_INDEX; + require getReserveNormalizedIncome(e, ASSET) == LIQUIDITY_INDEX; + require getReserveNormalizedVariableDebt(e, ASSET) == DEBT_INDEX; + require currentContract._reserves[ASSET].lastUpdateTimestamp == assert_uint40(e.block.timestamp); + require assert_uint256(currentContract._reserves[ASSET].liquidityIndex) == LIQUIDITY_INDEX; + require assert_uint256(currentContract._reserves[ASSET].variableBorrowIndex) == DEBT_INDEX; + require VB == currentContract._reserves[ASSET].virtualUnderlyingBalance; + + + uint256 scaled = totalSupplyByToken[ATOKEN]; uint256 IND = LIQUIDITY_INDEX; + assert + to_mathint(rayMulCVLPrecise( require_uint256(scaled - rayDivCVLPrecise(paybackAmount,IND)), IND) ) + <= + rayMulCVLPrecise(scaled,IND) - paybackAmount + IND/RAY(); +} + +function repay_hook_4_CVL(env e, DataTypes.ReserveCache res) { + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + + // THE MAIN ASSERTION + assert to_mathint(__totSUP_aToken) <= __totSUP_debt + DEBT_INDEX/RAY() + + LIQUIDITY_INDEX/RAY() + DELTA; +} + +persistent ghost address ATOKEN; //The current atoken in use. Should be assigned from within the rule. +persistent ghost address DEBT; //The current debt-token in use. Should be assigned from within the rule. +persistent ghost uint256 DELTA; +persistent ghost uint128 VB; // the virtual balance. should not be changed in repayWithATokens. + +function _updateIndexesCVL(DataTypes.ReserveCache res) { + havoc currentContract._reserves[ASSET].liquidityIndex;// assuming + require currentContract._reserves[ASSET].liquidityIndex == require_uint128(LIQUIDITY_INDEX); + havoc currentContract._reserves[ASSET].variableBorrowIndex; // assuming + require currentContract._reserves[ASSET].variableBorrowIndex == require_uint128(DEBT_INDEX); + + require res.nextLiquidityIndex == LIQUIDITY_INDEX; + require res.nextVariableBorrowIndex == DEBT_INDEX; +} + + +persistent ghost uint256 LIQUIDITY_INDEX {axiom LIQUIDITY_INDEX >= 10^27;} +persistent ghost uint256 DEBT_INDEX {axiom DEBT_INDEX >= 10^27;} + + +/*===================================================================================== + Rule: solvency__repayWithATokens + Status: PASS + Link: https://prover.certora.com/output/66114/4f2ccd151350485cbc13ae8b6f090592/?anonymousKey=269ecbf902dcd6fa81c201aeed0c7371a605828f + =====================================================================================*/ +rule solvency__repayWithATokens(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; ATOKEN = _atoken; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; DEBT = _debt; + //address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + //require aTokenToUnderlying[_stb]==_asset; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + VB = __virtual_bal; + + // INDEXES + LIQUIDITY_INDEX = getReserveNormalizedIncome(e, _asset); + DEBT_INDEX = getReserveNormalizedVariableDebt(e, _asset); + require RAY() <= LIQUIDITY_INDEX && RAY() <= DEBT_INDEX ; + + + // BASIC ASSUMPTION FOR THE RULE + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + //require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + // Note: + // 1. DELTA: the gap which the invariant can be break upto. This is a ghost variable + // 2. We dropped that virtual-balance from the inequality because we prove it stays + // unchanged during the function repayWithATokens(...). + require to_mathint(__totSUP_aToken) <= __totSUP_debt + DELTA; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_debt)!=0; + require exists_debt; + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repayWithATokens(e, _asset, _amount, interestRateMode); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // The virtual-balance stays unchanged + assert __virtual_bal__==__virtual_bal; + + // THE MAIN ASSERTION + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert to_mathint(__totSUP_aToken__) <= __totSUP_debt__ + DELTA + + reserve2.liquidityIndex / RAY() + reserve2.variableBorrowIndex / RAY(); +} + diff --git a/certora/experimental/specs/Aave/solvency/repayWithATokens/repayWithATokens-noHOOKS.spec b/certora/experimental/specs/Aave/solvency/repayWithATokens/repayWithATokens-noHOOKS.spec new file mode 100644 index 00000000..44703c33 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/repayWithATokens/repayWithATokens-noHOOKS.spec @@ -0,0 +1,70 @@ + +// aave imports +import "../../aToken.spec"; +import "../../AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + + + + + +/*===================================================================================== + Rule: solvency__repayWithATokens_totDbt_EQ_0 + We prove that if scaled-total-supply of the variable-debt is 0, then repayWithATokens reverts. + + Status: PASS + Link: https://prover.certora.com/output/66114/384d747b8ad540bca06387fe00d0a7bf/?anonymousKey=c15ce8ec3bd0ade0b0fa77042d725c28174e137d + =====================================================================================*/ +rule solvency__repayWithATokens_totDbt_EQ_0(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require forall address a. balanceByToken[_debt][a] <= totalSupplyByToken[_debt]; + require forall address a. balanceByToken[_atoken][a] <= totalSupplyByToken[_atoken]; + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_debt)!=0; + require !exists_debt; + + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + assert balanceByToken[_debt][onBehalfOf]==0; // This should help the prover + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repayWithATokens@withrevert(e, _asset, _amount, interestRateMode); + assert lastReverted; +} + + diff --git a/certora/experimental/specs/Aave/solvency/same_indexes.spec.old b/certora/experimental/specs/Aave/solvency/same_indexes.spec.old new file mode 100644 index 00000000..aae60331 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/same_indexes.spec.old @@ -0,0 +1,569 @@ +import "../../ERC20/WETHcvl.spec"; +import "../../ERC721/erc721.spec"; +import "../../ERC1967/erc1967.spec"; +import "../../PriceAggregators/chainlink.spec"; +import "../../PriceAggregators/tellor.spec"; + +// aave imports +import "../aToken.spec"; +import "../AddressProvider.spec"; +import "../PriceOracleSentinel.spec"; +import "../PriceOracle.spec"; +import "../ACLManager.spec"; +import "../FlashLoanReceiver.spec"; + +// standard +import "../../problems.spec"; +import "../../unresolved.spec"; +import "../../optimizations.spec"; + +import "../../generic.spec"; // pick additional rules from here + + +/*================================================================================================ + The property that we prove in this file (and also all the files solvency_...) is the following + solvency invariant (for an arbitrary asset): + (*) Atoken.totalSupply() <= VariableDebtToken.totalSupply() + virtual_balance + Intuitively, the left hand side is the amount the the pool owes to its users, and the right hand + side is the amount it has (either in hands - the virtual_balance, or what people owe to it - + the VariableDebtToken.totalSupply()) (*) should be proved for the following case: + 1. A function call: for example supply, withdraw, borrow, repay, repayWithATokens ... + 2. Time passing (without any function being called). This is relevant because the indexes increase + with the time, hence the amounts that appear in (*) + + Note that: + 1. The above isn't a real invariant. It can be violated due to rounding errors. What we really prove + is that the left-hand-side minus right-hand-side of (*) can't increase by more than the index + (in RAY units) after each function call. (it is either the liquidity-index or the variableBorrow-index + depending on the specific function call. + 2. The above is proved under the following assumptions: + a. The pool uses virtual accounting for the asset. + b. The asset uses only variable-debt interest (and not stable-debt). Moreover we assume that + StableDebtToken.totalSupply()==0. (Aave is going in that direction.) + c. RAY <= liquidity-index && RAY <= borrow-index. (this should be easy to prove) + d. Atoken.totalSupply() <= RAY. If this assumption is removed, we either get a timeout or an error. + (I suspect that the error is due to an imprecision RAY-calculation with such big numbers, but + haven't checked it yet.) + ================================================================================================*/ + + + +methods { + function _.ADDRESSES_PROVIDER() external => NONDET; // expect address + function _.calculateInterestRates(DataTypes.CalculateInterestRatesParams params) external => NONDET; + + function getReserveDataExtended(address) external returns (DataTypes.ReserveData memory) envfree; +} + + +function isVirtualAccActive(uint256 data) returns bool { + uint mask = 0xEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + return (data & ~mask) != 0; +} + + +function init_state() { + // based on aTokensAreNotUnderlyings + require forall address a. + a == 0 // nothing-token + || aTokenToUnderlying[a] == 0 // underlying + || aTokenToUnderlying[aTokenToUnderlying[a]] == 0 // aTokens map to underlyings which map to 0 + ; + // aTokens have the AToken sort, VariableDebtTokens have the VariableDebt sort, etc... + require forall address a. tokenToSort[currentContract._reserves[a].aTokenAddress] == AToken_token(); + require forall address a. tokenToSort[currentContract._reserves[a].variableDebtTokenAddress] == VariableDebtToken_token(); + require forall address a. tokenToSort[currentContract._reserves[a].stableDebtTokenAddress] == StableDebtToken_token(); +} + + +function tokens_addresses_limitations(address atoken, address variable, address stb, address asset) { + //require atoken==10; require variable==11; require stb==12; require asset==100; + //require weth!=10 && weth!=11 && weth!=12; + + require asset != 0; + require atoken != variable && atoken != stb && atoken != asset; + require variable != stb && variable != asset; + require stb != asset; + require weth != atoken && weth != variable && atoken != stb; + + // The asset that current rule deals with. It is used in summarization CVL-functions, + // see for example _accrueToTreasuryCVL(). + ASSET = asset; +} + + + +/*===================================================================================== + Rule: same_index__borrow + =====================================================================================*/ +rule same_index__borrow(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + uint256 __liqInd_before = getNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getNormalizedDebt(e, _asset); + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + // mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + //mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + //uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + //THE MAIN REQUIREMENT + //uint256 CONST; + //require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + //require __totSUP_aToken <= 10^27; // Without this requirement we get a failure. + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_debt)!=0; + + // THE FUNCTION CALL + uint256 _amount; uint256 _interestRateMode; address onBehalfOf; uint16 referralCode; + require _interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + borrow(e, _asset, _amount, _interestRateMode, referralCode, onBehalfOf); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + uint128 __liqInd_afterS = reserve2.liquidityIndex; + uint128 __dbtInd_afterS = reserve2.variableBorrowIndex; + uint256 __liqInd_after = getNormalizedIncome(e, _asset); + uint256 __dbtInd_after = getNormalizedDebt(e, _asset); + + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + + assert __liqInd_after == __liqInd_before; + assert assert_uint256(__liqInd_afterS) == __liqInd_after; + + assert exists_debt => __dbtInd_after == __dbtInd_before; + assert exists_debt => assert_uint256(__dbtInd_afterS) == __dbtInd_after; + +} + + + +/*===================================================================================== + Rule: solvency__supply + =====================================================================================*/ +rule solvency__supply(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + // uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + //uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + //require RAY() <= __liqInd_before && RAY() <= __dbtInd_before ; + + + + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get timeout/error + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + uint256 _amount; address onBehalfOf; uint16 referralCode; + supply(e, _asset, _amount, onBehalfOf, referralCode); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + //require assert_uint256(reserve2.liquidityIndex) == LIQUIDITY_INDEX; + //require assert_uint256(reserve2.variableBorrowIndex) == DEBT_INDEX; + // assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + //assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + //assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + //assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.liquidityIndex/RAY(); +} + + + + +/*===================================================================================== + Rule: solvency__withdraw + =====================================================================================*/ +rule solvency__withdraw(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + uint256 _amount; address _to; + withdraw(e, _asset, _amount, _to); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert __totSUP_aToken__ <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.liquidityIndex / RAY(); +} + + + +/*===================================================================================== + Rule: same_indexes__repay + =====================================================================================*/ +rule same_indexes__repay(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + //mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + //mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + //uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + //uint256 CONST; + //require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + //require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_debt)!=0; + require exists_debt; + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repay(e, _asset, _amount, interestRateMode, onBehalfOf); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + uint128 __liqInd_afterS = reserve2.liquidityIndex; + uint128 __dbtInd_afterS = reserve2.variableBorrowIndex; + uint256 __liqInd_after = getNormalizedIncome(e, _asset); + uint256 __dbtInd_after = getNormalizedDebt(e, _asset); + + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + + assert __liqInd_after == __liqInd_before; + assert assert_uint256(__liqInd_afterS) == __liqInd_after; + + assert __dbtInd_after == __dbtInd_before; + assert assert_uint256(__dbtInd_afterS) == __dbtInd_after; +} + + + +/*===================================================================================== + Rule: solvency__repayWithATokens + =====================================================================================*/ +rule solvency__repayWithATokens(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repayWithATokens(e, _asset, _amount, interestRateMode); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.variableBorrowIndex / RAY() ; +} + + + + + +function is_borrow(method f) returns bool { + return + f.selector == sig:borrow(address,uint256,uint256,uint16,address).selector; +} +function is_repay(method f) returns bool { + return f.selector == sig:repay(address,uint256,uint256,address).selector; +} +function is_repayWithATokens(method f) returns bool { + return f.selector == sig:repayWithATokens(address,uint256,uint256).selector; +} + + +definition method_can_be_called_by_a_user(method f) returns bool = + f.selector == sig:supply(address,uint256,address,uint16).selector + || f.selector == sig:withdraw(address,uint256,address).selector + || f.selector == sig:borrow(address,uint256,uint256,uint16,address).selector + || f.selector == sig:repay(address,uint256,uint256,address).selector + || f.selector == sig:repayWithATokens(address,uint256,uint256).selector + + ; + +/*===================================================================================== + Rule: solvency__all + =====================================================================================*/ +rule solvency__all(env e, address _asset, method f) filtered +{f -> method_can_be_called_by_a_user(f)} { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get timeout/error + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; address _to; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + calldataarg args; + if (is_borrow(f)) + borrow(e, _asset, _amount, interestRateMode, referralCode, onBehalfOf); + else if (is_repay(f)) + repay(e, _asset, _amount, interestRateMode, onBehalfOf); + else if (is_repayWithATokens(f)) + repayWithATokens(e, _asset, _amount, interestRateMode); + else if (f.selector == sig:supply(address,uint256,address,uint16).selector) + supply(e, _asset, _amount, onBehalfOf, referralCode); + else if (f.selector == sig:withdraw(address,uint256,address).selector) + withdraw(e, _asset, _amount, _to); + else + assert false; + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + + (reserve2.liquidityIndex / RAY()) + (reserve2.variableBorrowIndex / RAY()); +} + + + + +/*===================================================================================== + Rule: solvency__liquidationCall + =====================================================================================*/ + +rule solvency__liquidationCall(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + address receiverAddress; uint256 _amount; bytes params; uint16 referralCode; + address collateralAsset; address user; uint256 debtToCover; bool receiveAToken; + liquidationCall(e, collateralAsset, _asset, user, debtToCover, receiveAToken); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + //assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + //assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST; +} + diff --git a/certora/experimental/specs/Aave/solvency/solvency.spec.old b/certora/experimental/specs/Aave/solvency/solvency.spec.old new file mode 100644 index 00000000..e138a2d0 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/solvency.spec.old @@ -0,0 +1,679 @@ +import "../ERC20/WETHcvl.spec"; +import "../ERC721/erc721.spec"; +import "../ERC1967/erc1967.spec"; +import "../PriceAggregators/chainlink.spec"; +import "../PriceAggregators/tellor.spec"; + +// aave imports +import "./aToken.spec"; +import "./AddressProvider.spec"; +import "./PriceOracleSentinel.spec"; +import "./PriceOracle.spec"; +import "./ACLManager.spec"; +import "./FlashLoanReceiver.spec"; + +// standard +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +import "../generic.spec"; // pick additional rules from here + + +/*================================================================================================ + The property that we prove in this file (and also all the files solvency_...) is the following + solvency invariant (for an arbitrary asset): + (*) Atoken.totalSupply() <= VariableDebtToken.totalSupply() + virtual_balance + Intuitively, the left hand side is the amount the the pool owes to its users, and the right hand + side is the amount it has (either in hands - the virtual_balance, or what people owe to it - + the VariableDebtToken.totalSupply()) (*) should be proved for the following case: + 1. A function call: for example supply, withdraw, borrow, repay, repayWithATokens ... + 2. Time passing (without any function being called). This is relevant because the indexes increase + with the time, hence the amounts that appear in (*) + + Note that: + 1. The above isn't a real invariant. It can be violated due to rounding errors. What we really prove + is that the left-hand-side minus right-hand-side of (*) can't increase by more than the index + (in RAY units) after each function call. (it is either the liquidity-index or the variableBorrow-index + depending on the specific function call. + 2. The above is proved under the following assumptions: + a. The pool uses virtual accounting for the asset. + b. The asset uses only variable-debt interest (and not stable-debt). Moreover we assume that + StableDebtToken.totalSupply()==0. (Aave is going in that direction.) + c. RAY <= liquidity-index && RAY <= borrow-index. (this should be easy to prove) + d. Atoken.totalSupply() <= RAY. If this assumption is removed, we either get a timeout or an error. + (I suspect that the error is due to an imprecision RAY-calculation with such big numbers, but + haven't checked it yet.) + ================================================================================================*/ + + + +methods { + function _.ADDRESSES_PROVIDER() external => NONDET; // expect address + function _.calculateInterestRates(DataTypes.CalculateInterestRatesParams params) external => NONDET; + + function ReserveLogic.getNormalizedIncome(DataTypes.ReserveData storage reserve) + internal returns (uint256) => LIQUIDITY_INDEX; + + function ReserveLogic.getNormalizedDebt(DataTypes.ReserveData storage reserve) + internal returns (uint256) => DEBT_INDEX; + + function getReserveDataExtended(address) external returns (DataTypes.ReserveData memory) envfree; +} + + +ghost uint256 LIQUIDITY_INDEX {axiom LIQUIDITY_INDEX >= 10^27;} +ghost uint256 DEBT_INDEX {axiom DEBT_INDEX >= 10^27;} + +function get_DEBT_INDEX() { + + +} + +function isVirtualAccActive(uint256 data) returns bool { + uint mask = 0xEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + return (data & ~mask) != 0; +} + + +function init_state() { + // based on aTokensAreNotUnderlyings + require forall address a. + a == 0 // nothing-token + || aTokenToUnderlying[a] == 0 // underlying + || aTokenToUnderlying[aTokenToUnderlying[a]] == 0 // aTokens map to underlyings which map to 0 + ; + // aTokens have the AToken sort, VariableDebtTokens have the VariableDebt sort, etc... + require forall address a. tokenToSort[currentContract._reserves[a].aTokenAddress] == AToken_token(); + require forall address a. tokenToSort[currentContract._reserves[a].variableDebtTokenAddress] == VariableDebtToken_token(); + require forall address a. tokenToSort[currentContract._reserves[a].stableDebtTokenAddress] == StableDebtToken_token(); +} + + +function tokens_addresses_limitations(address atoken, address variable, address stb, address asset) { + //require atoken==10; require variable==11; require stb==12; require asset==100; + //require weth!=10 && weth!=11 && weth!=12; + + require asset != 0; + require atoken != variable && atoken != stb && atoken != asset; + require variable != stb && variable != asset; + require stb != asset; + require weth != atoken && weth != variable && atoken != stb; + + // The asset that current rule deals with. It is used in summarization CVL-functions, + // see for example _accrueToTreasuryCVL(). + ASSET = asset; +} + + +/*===================================================================================== + Rule: solvency__time_passing + =====================================================================================*/ +rule solvency__time_passing(env e1, env e2, address _asset) { + init_state(); + require require_uint40(e1.block.timestamp) <= require_uint40(e2.block.timestamp); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + //tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + require _atoken==10; require _debt==11; require _stb==12; require _asset==100; + require weth!=10 && weth!=11 && weth!=12; + ASSET = _asset; + + require forall address a. balanceByToken[_debt][a] <= totalSupplyByToken[_debt]; + require forall address a. balanceByToken[_atoken][a] <= totalSupplyByToken[_atoken]; + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve1 = getReserveDataExtended(_asset); + require reserve1.lastUpdateTimestamp == require_uint40(e1.block.timestamp); + + // INDEXES REQUIREMENTS + uint128 __liqInd_beforeS = reserve1.liquidityIndex; + uint128 __dbtInd_beforeS = reserve1.variableBorrowIndex; + require 10^27<=__liqInd_beforeS && 10^27<=__dbtInd_beforeS; + // the following 3 line are redundant because we assume lastUpdateTimestamp == e1.block.timestamp, but hopfully + // it helps the prover. + uint256 __liqInd_before = getReserveNormalizedIncome(e1, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e1, _asset); + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + + uint256 __totSUP_aToken; __totSUP_aToken = aTokenTotalSupplyCVL(_atoken, e1); + uint256 __totSUP_debt; __totSUP_debt = aTokenTotalSupplyCVL(_debt, e1); + // uint256 supply_usage_ratio = rayDivCVLPrecise(__totSUP_debt,__totSUP_aToken); + + // Here we need to require some property about the relation between the liq-rate and borrow-rate. + // The property should follow from the function DefaultReserveInterestRateStrategyV2.calculateInterestRates + // and it should be something like liq-rate <= borrow-rate * supply-usage-ratio + // where supply-usage-ratio is total-debt / (VB + total-debt + unbacked) + // require reserve1.currentLiquidityRate <= reserve1.currentVariableBorrowRate; + // require assert_uint256(reserve1.currentLiquidityRate) <= rayMulCVLPrecise(reserve1.currentVariableBorrowRate, supply_usage_ratio); + + require rayMulCVLPrecise(reserve1.currentLiquidityRate, __totSUP_aToken) <= + rayMulCVLPrecise(reserve1.currentVariableBorrowRate, __totSUP_debt); + + + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE MAIN REQUIREMENT + uint256 CONST; + // require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + require __totSUP_aToken <= __totSUP_debt; + + + // DUBUG HELPERS + require __totSUP_aToken <= 10^27; + require __totSUP_debt <= 10^27; + require e1.block.timestamp==0; + require e2.block.timestamp==365*86400; + + // require __liqInd_before ==10^27 && __dbtInd_before == 10^27; + require reserve1.liquidityIndex == 10^27 && reserve1.variableBorrowIndex==10^27; + //require reserve1.currentLiquidityRate == reserve1.currentVariableBorrowRate; + //require reserve1.currentLiquidityRate == 10^27; + require totalSupplyCVL(_debt)==0; + + + // FUNCTION CALL: NONE !!! + + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + uint256 __liqInd_after = getReserveNormalizedIncome(e2, _asset); + uint256 __dbtInd_after = getReserveNormalizedVariableDebt(e2, _asset); + + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e2)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e2)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + assert __virtual_bal__ == __virtual_bal; + + //THE ASSERTION + //assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + assert __totSUP_aToken__ <= __totSUP_debt__ + /* + some rounding error */; +} + + + + + +/*===================================================================================== + Rule: solvency__borrow + =====================================================================================*/ +rule solvency__borrow(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + //uint128 __liqInd_beforeS = reserve.liquidityIndex; + //uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + //uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + //uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + //require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + //require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + //THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a failure. + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_debt)!=0; + require exists_debt; + + // THE FUNCTION CALL + uint256 _amount; uint256 _interestRateMode; address onBehalfOf; uint16 referralCode; + require _interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + borrow(e, _asset, _amount, _interestRateMode, referralCode, onBehalfOf); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + require assert_uint256(reserve2.liquidityIndex) == LIQUIDITY_INDEX; + require exists_debt => assert_uint256(reserve2.variableBorrowIndex) == DEBT_INDEX; + //assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + //assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + //assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + //assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert __totSUP_aToken__ <= __virtual_bal__ + __totSUP_debt__ + CONST + // + DEBT_INDEX / RAY() ; + + reserve2.variableBorrowIndex / RAY() ; +} + + + +/*===================================================================================== + Rule: solvency__supply + =====================================================================================*/ +rule solvency__supply(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + // uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + //uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + //require RAY() <= __liqInd_before && RAY() <= __dbtInd_before ; + + + + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get timeout/error + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + uint256 _amount; address onBehalfOf; uint16 referralCode; + supply(e, _asset, _amount, onBehalfOf, referralCode); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + require assert_uint256(reserve2.liquidityIndex) == LIQUIDITY_INDEX; + require assert_uint256(reserve2.variableBorrowIndex) == DEBT_INDEX; + // assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + //assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + //assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + //assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.liquidityIndex/RAY(); +} + + + + +/*===================================================================================== + Rule: solvency__withdraw + =====================================================================================*/ +rule solvency__withdraw(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + uint256 _amount; address _to; + withdraw(e, _asset, _amount, _to); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert __totSUP_aToken__ <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.liquidityIndex / RAY(); +} + + + +/*===================================================================================== + Rule: solvency__repay + =====================================================================================*/ +rule solvency__repay(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repay(e, _asset, _amount, interestRateMode, onBehalfOf); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + //assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + //assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + //assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + //assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + //assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.variableBorrowIndex / RAY() ; +} + + + +/*===================================================================================== + Rule: solvency__repayWithATokens + =====================================================================================*/ +rule solvency__repayWithATokens(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repayWithATokens(e, _asset, _amount, interestRateMode); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.variableBorrowIndex / RAY() ; +} + + + + + +function is_borrow(method f) returns bool { + return + f.selector == sig:borrow(address,uint256,uint256,uint16,address).selector; +} +function is_repay(method f) returns bool { + return f.selector == sig:repay(address,uint256,uint256,address).selector; +} +function is_repayWithATokens(method f) returns bool { + return f.selector == sig:repayWithATokens(address,uint256,uint256).selector; +} + + +definition method_can_be_called_by_a_user(method f) returns bool = + f.selector == sig:supply(address,uint256,address,uint16).selector + || f.selector == sig:withdraw(address,uint256,address).selector + || f.selector == sig:borrow(address,uint256,uint256,uint16,address).selector + || f.selector == sig:repay(address,uint256,uint256,address).selector + || f.selector == sig:repayWithATokens(address,uint256,uint256).selector + + ; + +/*===================================================================================== + Rule: solvency__all + =====================================================================================*/ +rule solvency__all(env e, address _asset, method f) filtered +{f -> method_can_be_called_by_a_user(f)} { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get timeout/error + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; address _to; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + calldataarg args; + if (is_borrow(f)) + borrow(e, _asset, _amount, interestRateMode, referralCode, onBehalfOf); + else if (is_repay(f)) + repay(e, _asset, _amount, interestRateMode, onBehalfOf); + else if (is_repayWithATokens(f)) + repayWithATokens(e, _asset, _amount, interestRateMode); + else if (f.selector == sig:supply(address,uint256,address,uint16).selector) + supply(e, _asset, _amount, onBehalfOf, referralCode); + else if (f.selector == sig:withdraw(address,uint256,address).selector) + withdraw(e, _asset, _amount, _to); + else + assert false; + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + + (reserve2.liquidityIndex / RAY()) + (reserve2.variableBorrowIndex / RAY()); +} + + + + +/*===================================================================================== + Rule: solvency__liquidationCall + =====================================================================================*/ + +rule solvency__liquidationCall(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + address receiverAddress; uint256 _amount; bytes params; uint16 referralCode; + address collateralAsset; address user; uint256 debtToCover; bool receiveAToken; + liquidationCall(e, collateralAsset, _asset, user, debtToCover, receiveAToken); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + //assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + //assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST; +} + diff --git a/certora/experimental/specs/Aave/solvency/supply.spec b/certora/experimental/specs/Aave/solvency/supply.spec new file mode 100644 index 00000000..6c01329b --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/supply.spec @@ -0,0 +1,72 @@ + +// aave imports +import "../aToken.spec"; +import "../AddressProvider.spec"; + +import "common/optimizations.spec"; +import "common/functions.spec"; +import "common/validation_functions.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + + + +/*===================================================================================== + Rule: solvency__supply + + Status: PASS + Link: https://prover.certora.com/output/66114/702a83a589334a8a815c18dc79629c0d/?anonymousKey=4d139c38ab10b1a9ea6625410f2de43960e38b12 + =====================================================================================*/ +rule solvency__supply(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + require RAY() <= __liqInd_before && RAY() <= __dbtInd_before ; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get timeout/error + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + uint256 _amount; address onBehalfOf; uint16 referralCode; + supply(e, _asset, _amount, onBehalfOf, referralCode); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.liquidityIndex/RAY(); +} diff --git a/certora/experimental/specs/Aave/solvency/time_passing.spec b/certora/experimental/specs/Aave/solvency/time_passing.spec new file mode 100644 index 00000000..b484d6f7 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/time_passing.spec @@ -0,0 +1,116 @@ + +// aave imports +import "../aToken.spec"; +import "../AddressProvider.spec"; + + +import "common/optimizations.spec"; +import "common/functions.spec"; +import "common/validation_functions.spec"; + + + + + + +/*===================================================================================== + Rule: solvency__time_passing + + Status: + 1. Currently , in order to avoid time-out the are too many restrictions (see under + "// RESTRICTIONS" below. + 2. There is a big assumption that is not proved. See under "// ASSMPTION" and the explanation + above it. + =====================================================================================*/ +rule solvency__time_passing(env e1, env e2, address _asset) { + init_state(); + require require_uint40(e1.block.timestamp) <= require_uint40(e2.block.timestamp); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + //tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + require _atoken==10; require _debt==11; require _stb==12; require _asset==100; + // require weth!=10 && weth!=11 && weth!=12; + ASSET = _asset; + + require forall address a. balanceByToken[_debt][a] <= totalSupplyByToken[_debt]; + require forall address a. balanceByToken[_atoken][a] <= totalSupplyByToken[_atoken]; + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve1 = getReserveDataExtended(_asset); + require reserve1.lastUpdateTimestamp == require_uint40(e1.block.timestamp); + + // INDEXES REQUIREMENTS + uint128 __liqInd_beforeS = reserve1.liquidityIndex; + uint128 __dbtInd_beforeS = reserve1.variableBorrowIndex; + require 10^27<=__liqInd_beforeS && 10^27<=__dbtInd_beforeS; + // the following 3 line are redundant because we assume lastUpdateTimestamp == e1.block.timestamp, but hopfully + // it helps the prover. + uint256 __liqInd_before = getReserveNormalizedIncome(e1, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e1, _asset); + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + + uint256 __totSUP_aToken; __totSUP_aToken = aTokenTotalSupplyCVL(_atoken, e1); + uint256 __totSUP_debt; __totSUP_debt = aTokenTotalSupplyCVL(_debt, e1); + // uint256 supply_usage_ratio = rayDivCVLPrecise(__totSUP_debt,__totSUP_aToken); + + // Here we need to require some property about the relation between the liq-rate and borrow-rate. + // The property should follow from the function DefaultReserveInterestRateStrategyV2.calculateInterestRates + // and it should be something like liq-rate <= borrow-rate * supply-usage-ratio + // where supply-usage-ratio is total-debt / (VB + total-debt + unbacked) + // require reserve1.currentLiquidityRate <= reserve1.currentVariableBorrowRate; + // require assert_uint256(reserve1.currentLiquidityRate) <= rayMulCVLPrecise(reserve1.currentVariableBorrowRate, supply_usage_ratio); + + // ASSUMPTION + require rayMulCVLPrecise(reserve1.currentLiquidityRate, __totSUP_aToken) <= + rayMulCVLPrecise(reserve1.currentVariableBorrowRate, __totSUP_debt); + + + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE MAIN REQUIREMENT + uint256 CONST; + // require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + require __totSUP_aToken <= __totSUP_debt; + + + // RESTRICTIONS + require __totSUP_aToken <= 10^27; + require __totSUP_debt <= 10^27; + require e1.block.timestamp==0; + require e2.block.timestamp==365*86400; + + // require __liqInd_before ==10^27 && __dbtInd_before == 10^27; + require reserve1.liquidityIndex == 10^27 && reserve1.variableBorrowIndex==10^27; + //require reserve1.currentLiquidityRate == reserve1.currentVariableBorrowRate; + //require reserve1.currentLiquidityRate == 10^27; + require totalSupplyCVL(_debt)==0; + + + // FUNCTION CALL: NONE !!! + + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + uint256 __liqInd_after = getReserveNormalizedIncome(e2, _asset); + uint256 __dbtInd_after = getReserveNormalizedVariableDebt(e2, _asset); + + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e2)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e2)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + assert __virtual_bal__ == __virtual_bal; + + //THE ASSERTION + //assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + assert __totSUP_aToken__ <= __totSUP_debt__ + /* + some rounding error */; +} + + + + diff --git a/certora/experimental/specs/Aave/solvency/withdraw.spec b/certora/experimental/specs/Aave/solvency/withdraw.spec new file mode 100644 index 00000000..8dffd167 --- /dev/null +++ b/certora/experimental/specs/Aave/solvency/withdraw.spec @@ -0,0 +1,77 @@ + +// aave imports +import "../aToken.spec"; +import "../AddressProvider.spec"; +//import "../ACLManager.spec"; + +import "common/optimizations.spec"; +import "common/functions.spec"; +import "common/validation_functions.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + + + +/*===================================================================================== + Rule: solvency__withdraw + + Status: PASS + Link: https://prover.certora.com/output/66114/f78383017cfa4b7ead6530b0b6e3a87b/?anonymousKey=58440ef19dd97ec9a5d7ba978481a77673e3f133 + =====================================================================================*/ +rule solvency__withdraw(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + uint256 _amount; address _to; + withdraw(e, _asset, _amount, _to); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert __totSUP_aToken__ <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.liquidityIndex / RAY(); +} + diff --git a/certora/experimental/specs/Aave/tokenCheckBase.spec b/certora/experimental/specs/Aave/tokenCheckBase.spec new file mode 100644 index 00000000..cc17a1f9 --- /dev/null +++ b/certora/experimental/specs/Aave/tokenCheckBase.spec @@ -0,0 +1,196 @@ +// Shared rules for AToken, VariableDebtToken, and StableDebtToken equivalence checks + +// balanceOf equivalence +rule balanceOfEquivalence(method f) +filtered { f -> f.contract == currentContract && !f.isView } +{ + env e; + init_state_invariants(); + address user; + // this is actually not enough to check the balanceOf getters, because + // of rounding that can cause divergence after updates. + // here, we must resort to make sure that the ghost state matches the + // low-level state of the AToken. So the proof is a bit more 'specific' + // per AToken, but that is okay. + // specifically: + require forall address a. to_mathint(aToken._userState[a].balance) == to_mathint(balanceByToken[aToken][a]); + uint _contractVal = aToken.balanceOf(e, user); + uint _specVal = aTokenBalanceOfCVL(aToken, user, e); + // given the low-level require, we can assert equivalence already here! + assert _contractVal == _specVal; + + run_parametric_with_cvl_equivalent(f, e); + + assert forall address a. to_mathint(aToken._userState[a].balance) == to_mathint(balanceByToken[aToken][a]); + + uint contractVal_ = aToken.balanceOf(e, user); + uint specVal_ = aTokenBalanceOfCVL(aToken, user, e); + assert contractVal_ == specVal_; +} + +rule scaledBalanceOfEquivalence(method f) +filtered { f -> f.contract == currentContract && !f.isView } +{ + env e; + init_state_invariants(); + address user; + uint _contractVal = aToken.scaledBalanceOf(user); + uint _specVal = scaledBalanceOfCVL(aToken, user); + // this is proven in balanceOfEquivalence + require forall address a. to_mathint(aToken._userState[a].balance) == to_mathint(balanceByToken[aToken][a]); + assert _contractVal == _specVal; + + run_parametric_with_cvl_equivalent(f, e); + + uint contractVal_ = aToken.scaledBalanceOf(user); + uint specVal_ = scaledBalanceOfCVL(aToken, user); + assert contractVal_ == specVal_; +} + +// totalSupply equivalence +rule totalSupplyEquivalence(method f) +filtered { f -> f.contract == currentContract && !f.isView } +{ + env e; + init_state_invariants(); + // this is actually not enough to check the totalSupply getters, because + // of rounding that can cause divergence after updates. + // here, we must resort to make sure that the ghost state matches the + // low-level state of the AToken. So the proof is a bit more 'specific' + // per AToken, but that is okay. + // specifically: + require to_mathint(aToken._totalSupply) == to_mathint(totalSupplyByToken[aToken]); + uint _contractVal = aToken.totalSupply(e); + uint _specVal = aTokenTotalSupplyCVL(aToken, e); + // given the low-level require, we can assert equivalence already here! + assert _contractVal == _specVal; + + run_parametric_with_cvl_equivalent(f, e); + + assert to_mathint(aToken._totalSupply) == to_mathint(totalSupplyByToken[aToken]); + + uint contractVal_ = aToken.totalSupply(e); + uint specVal_ = aTokenTotalSupplyCVL(aToken, e); + assert contractVal_ == specVal_; +} + +rule scaledTotalSupplyEquivalence(method f) +filtered { f -> f.contract == currentContract && !f.isView } +{ + env e; + init_state_invariants(); + // this is actually not enough to check the totalSupply getters, because + // of rounding that can cause divergence after updates. + // here, we must resort to make sure that the ghost state matches the + // low-level state of the AToken. So the proof is a bit more 'specific' + // per AToken, but that is okay. + // specifically: + require to_mathint(aToken._totalSupply) == to_mathint(totalSupplyByToken[aToken]); + + uint _contractVal = aToken.scaledTotalSupply(); + uint _specVal = scaledTotalSupplyCVL(aToken); + assert _contractVal == _specVal; + + run_parametric_with_cvl_equivalent(f, e); + + assert to_mathint(aToken._totalSupply) == to_mathint(totalSupplyByToken[aToken]); + + + uint contractVal_ = aToken.scaledTotalSupply(); + uint specVal_ = scaledTotalSupplyCVL(aToken); + assert contractVal_ == specVal_; +} + +// allowance equivalence +rule allowanceEquivalence(method f) +filtered { f -> f.contract == currentContract && !f.isView } +{ + env e; + init_state_invariants(); + address owner; + address spender; + uint _contractVal = aToken.allowance(owner, spender); + uint _specVal = allowanceCVL(aToken, owner, spender); + require _contractVal == _specVal; + + run_parametric_with_cvl_equivalent(f, e); + + uint contractVal_ = aToken.allowance(owner, spender); + uint specVal_ = allowanceCVL(aToken, owner, spender); + assert contractVal_ == specVal_; +} + +rule listOtherViewFunctions(method viewF) +filtered { viewF -> + viewF.contract == currentContract + && viewF.isView + && viewF.selector != sig:balanceOf(address).selector + && viewF.selector != sig:scaledBalanceOf(address).selector + && viewF.selector != sig:totalSupply().selector + && viewF.selector != sig:allowance(address,address).selector +} { + env e; + calldataarg arg; + viewF(e, arg); + assert true; +} + +// Expected failure in `initialize`, as there are no checks that we assign a legal underlying. (e.g. assigning an AToken's underlying as itself) +invariant aTokensAreNotUnderlyings() + forall address a. + a == 0 // nothing-token + || aTokenToUnderlying[a] == 0 // underlying + || aTokenToUnderlying[aTokenToUnderlying[a]] == 0 // aTokens map to underlyings which map to 0 + filtered { f -> f.contract == currentContract + // omit initialize function, we know initialization violates it + && f.selector != initialize_method_sig() + } + +// quasi-invariant, it must hold after initialize() was called on the AToken +definition currentATokenHasAnUnderlying() returns bool = + aTokenToUnderlying[aToken] != 0; + +rule currentATokenHasAnUnderlyingAfterInitiailization(method f) +filtered { f -> f.contract == currentContract && f.selector != initialize_method_sig() } +{ + require currentATokenHasAnUnderlying(); + env e; + calldataarg arg; + f(e, arg); + assert currentATokenHasAnUnderlying(); +} + +// quase-invariant. ERC20 tokens don't have an underlying, AToken/VarDebt/StableDebt must have an underlying. +// it must hold after initialize() was called. +// (though if it were possible, re-calling it can nullify the underlying) +definition aTokenWithoutUnderlyingIsERC20(address token) returns bool = + aTokenToUnderlying[token] == 0 <=> tokenToSort[token] == 0; + +rule aTokenWithoutUnderlyingIsERC20AfterInitialization(method f) +filtered { f -> f.contract == currentContract && f.selector != initialize_method_sig() } +{ + address token; + require aTokenWithoutUnderlyingIsERC20(token); + env e; + calldataarg arg; + f(e, arg); + assert aTokenWithoutUnderlyingIsERC20(token); +} + +// xxx all those initialize() failing rules could be prettified if we had an "initialized" +// predicate + +// use-me block +/** + +use rule balanceOfEquivalence; +use rule scaledBalanceOfEquivalence; +use rule totalSupplyEquivalence; +use rule scaledTotalSupplyEquivalence; +use rule allowanceEquivalence; +use rule listOtherViewFunctions; +use invariant aTokensAreNotUnderlyings; +use rule currentATokenHasAnUnderlyingAfterInitiailization; +use rule aTokenWithoutUnderlyingIsERC20AfterInitialization; + + */ \ No newline at end of file diff --git a/certora/experimental/specs/ERC1967/erc1967.spec b/certora/experimental/specs/ERC1967/erc1967.spec new file mode 100644 index 00000000..8c96499c --- /dev/null +++ b/certora/experimental/specs/ERC1967/erc1967.spec @@ -0,0 +1,7 @@ +methods { + // avoids linking messages upon upgradeToAndCall + function _._upgradeToAndCall(address,bytes memory,bool) internal => NONDET; + function _._upgradeToAndCallUUPS(address,bytes memory,bool) internal => NONDET; + // view function + function _.proxiableUUID() external => NONDET; // expect bytes32 +} \ No newline at end of file diff --git a/certora/experimental/specs/ERC20/WETHcvl.spec b/certora/experimental/specs/ERC20/WETHcvl.spec new file mode 100644 index 00000000..8b1d8b70 --- /dev/null +++ b/certora/experimental/specs/ERC20/WETHcvl.spec @@ -0,0 +1,35 @@ +using DummyWeth as weth; // we are limited by the fact that we cannot do transfers from CVL +using Utilities as utils; + +methods { + // Utilities + function Utilities.justRevert() external envfree; + + // WETH + function _.deposit() external with (env e) => wethDeposit(calledContract, e.msg.sender, e.msg.value) expect void; + function _.withdraw(uint256 amount) external with (env e) => wethWithdraw(calledContract, e.msg.sender, amount) expect void; +} + +function wethDeposit(address target, address caller, uint256 value) { + // should be reverting if target != weth. Instead, we will use a contract to revert + if (target != weth) { + utils.justRevert(); // check this works xxx + } else { + // money will be transferred because of the payability of deposit + env e2; + require e2.msg.sender == caller; + require e2.msg.value == value; + weth.deposit(e2); + } +} + +function wethWithdraw(address target, address caller, uint256 amount) { + // should be reverting if target != weth. Instead, we will use a contract to revert + if (target != weth) { + utils.justRevert(); // check this works xxx + } else { + env e2; + require e2.msg.sender == caller; + weth.withdraw(e2, amount); + } +} \ No newline at end of file diff --git a/certora/experimental/specs/ERC20/erc20cvl.spec b/certora/experimental/specs/ERC20/erc20cvl.spec new file mode 100644 index 00000000..4b7a688f --- /dev/null +++ b/certora/experimental/specs/ERC20/erc20cvl.spec @@ -0,0 +1,80 @@ +methods { + // ERC20 standard + function _.name() external => NONDET; // can we use PER_CALLEE_CONSTANT? + function _.symbol() external => NONDET; // can we use PER_CALLEE_CONSTANT? + function _.decimals() external => PER_CALLEE_CONSTANT; + function _.totalSupply() external => totalSupplyCVL(calledContract) expect uint256; + function _.balanceOf(address a) external => balanceOfCVL(calledContract, a) expect uint256; + function _.allowance(address a, address b) external => allowanceCVL(calledContract, a, b) expect uint256; + function _.approve(address a, uint256 x) external with (env e) => approveCVL(calledContract, e.msg.sender, a, x) expect bool; + function _.transfer(address a, uint256 x) external with (env e) => transferCVL(calledContract, e.msg.sender, a, x) expect bool; + function _.transferFrom(address a, address b, uint256 x) external with (env e) => transferFromCVL(calledContract, e.msg.sender, a, b, x) expect bool; + + // Permit + // xxx unsound + function _.permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external => NONDET; // permitCVL(calledContract, owner, spender, value, deadline, v, r, s); + +} + + +/// CVL simple implementations of IERC20: +/// token => totalSupply +ghost mapping(address => uint256) totalSupplyByToken; +/// token => account => balance +ghost mapping(address => mapping(address => uint256)) balanceByToken; +/// token => owner => spender => allowance +ghost mapping(address => mapping(address => mapping(address => uint256))) allowanceByToken; + +// function tokenBalanceOf(address token, address account) returns uint256 { +// return balanceByToken[token][account]; +// } + +function totalSupplyCVL(address token) returns uint256 { + return totalSupplyByToken[token]; +} + +function balanceOfCVL(address token, address a) returns uint256 { + return balanceByToken[token][a]; +} + +function allowanceCVL(address token, address a, address b) returns uint256 { + return allowanceByToken[token][a][b]; +} + +function approveCVL(address token, address approver, address spender, uint256 amount) returns bool { + // should be randomly reverting xxx + bool nondetSuccess; + if (!nondetSuccess) return false; + + allowanceByToken[token][approver][spender] = amount; + return true; +} + +function transferFromCVL(address token, address spender, address from, address to, uint256 amount) returns bool { + // should be randomly reverting xxx + bool nondetSuccess; + if (!nondetSuccess) return false; + + if (allowanceByToken[token][from][spender] < amount) return false; + allowanceByToken[token][from][spender] = assert_uint256(allowanceByToken[token][from][spender] - amount); + return transferCVL(token, from, to, amount); +} + +function transferCVL(address token, address from, address to, uint256 amount) returns bool { + // should be randomly reverting xxx + bool nondetSuccess; + if (!nondetSuccess) return false; + + if(balanceByToken[token][from] < amount) return false; + balanceByToken[token][from] = assert_uint256(balanceByToken[token][from] - amount); + balanceByToken[token][to] = require_uint256(balanceByToken[token][to] + amount); // We neglect overflows. + return true; +} \ No newline at end of file diff --git a/certora/experimental/specs/ERC20/erc20cvlForAave.spec b/certora/experimental/specs/ERC20/erc20cvlForAave.spec new file mode 100644 index 00000000..a0277c23 --- /dev/null +++ b/certora/experimental/specs/ERC20/erc20cvlForAave.spec @@ -0,0 +1,104 @@ +methods { + // ERC20 standard + function _.name() external => NONDET; // can we use PER_CALLEE_CONSTANT? + function _.symbol() external => NONDET; // can we use PER_CALLEE_CONSTANT? + function _.decimals() external => PER_CALLEE_CONSTANT; + // function _.totalSupply() external => totalSupplyCVL(calledContract) expect uint256; // Aave specs will override + // function _.balanceOf(address a) external => balanceOfCVL(calledContract, a) expect uint256; // Aave specs will override + function _.allowance(address a, address b) external => allowanceCVL(calledContract, a, b) expect uint256; + function _.approve(address a, uint256 x) external with (env e) => approveCVL(calledContract, e.msg.sender, a, x) expect bool; + // function _.transfer(address a, uint256 x) external with (env e) => transferCVL(calledContract, e.msg.sender, a, x) expect bool; // Aave specs will override + // function _.transferFrom(address a, address b, uint256 x) external with (env e) => transferFromCVL(calledContract, e.msg.sender, a, b, x) expect bool; // Aave specs will override + + // increase/decrease allowance + function _.increaseAllowance(address spender, uint256 addedValue) external with (env e) => increaseAllowanceCVL(calledContract, e.msg.sender, spender, addedValue) expect bool; + function _.decreaseAllowance(address spender, uint256 subtractedValue) external with (env e) => decreaseAllowanceCVL(calledContract, e.msg.sender, spender, subtractedValue) expect bool; + + // Permit + // xxx unsound + function _.permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external => permitCVL(calledContract, owner, spender, value, deadline, v, r, s) expect void; + +} + + +/// CVL simple implementations of IERC20: +/// token => totalSupply +persistent ghost mapping(address => uint256) totalSupplyByToken { + init_state axiom forall address a. totalSupplyByToken[a]==0; +} + +/// token => account => balance +persistent ghost mapping(address => mapping(address => uint256)) balanceByToken { + init_state axiom forall address a. forall address b. balanceByToken[a][b]==0; +} +/// token => owner => spender => allowance +persistent ghost mapping(address => mapping(address => mapping(address => uint256))) allowanceByToken { + init_state axiom forall address a. forall address b. forall address c. allowanceByToken[a][b][c]==0; +} + +// function tokenBalanceOf(address token, address account) returns uint256 { +// return balanceByToken[token][account]; +// } + +function totalSupplyCVL(address token) returns uint256 { + return totalSupplyByToken[token]; +} + +function balanceOfCVL(address token, address a) returns uint256 { + return balanceByToken[token][a]; +} + +function allowanceCVL(address token, address a, address b) returns uint256 { + return allowanceByToken[token][a][b]; +} + +function approveCVL(address token, address approver, address spender, uint256 amount) returns bool { + allowanceByToken[token][approver][spender] = amount; + return true; +} + +function transferFromCVL(address token, address spender, address from, address to, uint256 amount) returns bool { + if (allowanceByToken[token][from][spender] < amount) return false; + allowanceByToken[token][from][spender] = assert_uint256(allowanceByToken[token][from][spender] - amount); + return transferCVL(token, from, to, amount); +} + +function transferCVL(address token, address from, address to, uint256 amount) returns bool { + //if(balanceByToken[token][from] < amount) return false; + require balanceByToken[token][from] >= amount; + balanceByToken[token][from] = assert_uint256(balanceByToken[token][from] - amount); + balanceByToken[token][to] = require_uint256(balanceByToken[token][to] + amount); // We neglect overflows. + return true; +} + +function increaseAllowanceCVL(address token, address owner, address spender, uint256 increasedAmount) returns bool { + uint256 amt = require_uint256(allowanceCVL(token, owner, spender) + increasedAmount); + return approveCVL(token, owner, spender, amt); +} + +function decreaseAllowanceCVL(address token, address owner, address spender, uint256 decreasedAmount) returns bool { + uint256 amt = require_uint256(allowanceCVL(token, owner, spender) - decreasedAmount); + return approveCVL(token, owner, spender, amt); +} + +function permitCVL( + address token, + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s +) { + // xxx not checking conditions + approveCVL(token, owner, spender, value); +} diff --git a/certora/experimental/specs/ERC20/erc20dispatched.spec b/certora/experimental/specs/ERC20/erc20dispatched.spec new file mode 100644 index 00000000..c40394bb --- /dev/null +++ b/certora/experimental/specs/ERC20/erc20dispatched.spec @@ -0,0 +1,16 @@ +methods { + // ERC20 standard + function _.name() external => DISPATCHER(true); + function _.symbol() external => DISPATCHER(true); + function _.decimals() external => DISPATCHER(true); + function _.totalSupply() external => DISPATCHER(true); + function _.balanceOf(address) external => DISPATCHER(true); + function _.allowance(address,address) external => DISPATCHER(true); + function _.approve(address,uint256) external => DISPATCHER(true); + function _.transfer(address,uint256) external => DISPATCHER(true); + function _.transferFrom(address,address,uint256) external => DISPATCHER(true); + + // WETH + function _.deposit() external => DISPATCHER(true); + function _.withdraw(uint256) external => DISPATCHER(true); +} diff --git a/certora/experimental/specs/ERC721/erc721.spec b/certora/experimental/specs/ERC721/erc721.spec new file mode 100644 index 00000000..474b69f3 --- /dev/null +++ b/certora/experimental/specs/ERC721/erc721.spec @@ -0,0 +1,9 @@ +methods { + // likely unsound, but assumes no callback + function _.onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes data + ) external => NONDET; /* expects bytes4 */ +} \ No newline at end of file diff --git a/certora/experimental/specs/Math/CVLMath.spec b/certora/experimental/specs/Math/CVLMath.spec new file mode 100644 index 00000000..39012c45 --- /dev/null +++ b/certora/experimental/specs/Math/CVLMath.spec @@ -0,0 +1,240 @@ +/****************************************** +----------- CVL Math Library -------------- +*******************************************/ + +// A restriction on the value of w = x * y / z +// The ratio between x (or y) and z is a rational number a/b or b/a. +// Important : do not set a = 0 or b = 0. +// Note: constRatio(x,y,z,a,b,w) <=> constRatio(x,y,z,b,a,w) +definition constRatio(uint256 x, uint256 y, uint256 z, + uint256 a, uint256 b, uint256 w) + returns bool = + ( a * x == b * z && to_mathint(w) == (b * y) / a ) || + ( b * x == a * z && to_mathint(w) == (a * y) / b ) || + ( a * y == b * z && to_mathint(w) == (b * x) / a ) || + ( b * y == a * z && to_mathint(w) == (a * x) / b ); + +// A restriction on the value of w = x * y / z +// The division quotient between x (or y) and z is an integer q or 1/q. +// Important : do not set q=0 +definition constQuotient(uint256 x, uint256 y, uint256 z, + uint256 q, uint256 w) + + returns bool = + ( to_mathint(x) == q * z && to_mathint(w) == q * y ) || + ( q * x == to_mathint(z) && to_mathint(w) == y / q ) || + ( to_mathint(y) == q * z && to_mathint(w) == q * x ) || + ( q * y == to_mathint(z) && to_mathint(w) == x / q ); + +/// Equivalent to the one above, but with implication +definition constQuotientImply(uint256 x, uint256 y, uint256 z, + uint256 q, uint256 w) + + returns bool = + ( to_mathint(x) == q * z => to_mathint(w) == q * y ) && + ( q * x == to_mathint(z) => to_mathint(w) == y / q ) && + ( to_mathint(y) == q * z => to_mathint(w) == q * x ) && + ( q * y == to_mathint(z) => to_mathint(w) == x / q ); + +definition ONE18() returns uint256 = 1000000000000000000; +definition RAY() returns uint256 = 10^27; + +definition _monotonicallyIncreasing(uint256 x, uint256 y, uint256 fx, uint256 fy) returns bool = + (x > y => fx >= fy); + +definition _monotonicallyDecreasing(uint256 x, uint256 y, uint256 fx, uint256 fy) returns bool = + (x > y => fx <= fy); + +definition abs(mathint x) returns mathint = + x >= 0 ? x : 0 - x; + +definition min(mathint x, mathint y) returns mathint = + x > y ? y : x; + +definition max(mathint x, mathint y) returns mathint = + x > y ? x : y; + +/// Returns whether y is equal to x up to error bound of 'err' (18 decs). +/// e.g. 10% relative error => err = 1e17 +definition relativeErrorBound(mathint x, mathint y, mathint err) returns bool = + (x != 0 + ? abs(x - y) * ONE18() <= abs(x) * err + : abs(y) <= err); + +/// Axiom for a weighted average of the form WA = (x * y) / (y + z) +/// This is valid as long as z + y > 0 => make certain of that condition in the use of this definition. +definition weightedAverage(mathint x, mathint y, mathint z, mathint WA) returns bool = + ((x > 0 && y > 0) => (WA >= 0 && WA <= x)) + && + ((x < 0 && y > 0) => (WA <= 0 && WA >= x)) + && + ((x > 0 && y < 0) => (WA <= 0 && WA - x <= 0)) + && + ((x < 0 && y < 0) => (WA >= 0 && WA + x <= 0)) + && + ((x == 0 || y == 0) => (WA == 0)); + + +function mulDivDownAbstract(uint256 x, uint256 y, uint256 z) returns uint256 { + require z !=0; + uint256 xy = require_uint256(x * y); + uint256 res; + mathint rem; + require z * res + rem == to_mathint(xy); + require rem < to_mathint(z); + return res; +} + +function mulDivDownAbstractPlus(uint256 x, uint256 y, uint256 z) returns uint256 { + uint256 res; + require z != 0; + uint256 xy = require_uint256(x * y); + uint256 fz = require_uint256(res * z); + + require xy >= fz; + require fz + z > to_mathint(xy); + return res; +} + +function mulDivUpAbstractPlus(uint256 x, uint256 y, uint256 z) returns uint256 { + uint256 res; + require z != 0; + uint256 xy = require_uint256(x * y); + uint256 fz = require_uint256(res * z); + require xy >= fz; + require fz + z > to_mathint(xy); + + if(xy == fz) { + return res; + } + return require_uint256(res + 1); +} + +function mulDownWad(uint256 x, uint256 y) returns uint256 { + return mulDivDownAbstractPlus(x, y, ONE18()); +} + +function mulUpWad(uint256 x, uint256 y) returns uint256 { + return mulDivUpAbstractPlus(x, y, ONE18()); +} + +function divDownWad(uint256 x, uint256 y) returns uint256 { + return mulDivDownAbstractPlus(x, ONE18(), y); +} + +function divUpWad(uint256 x, uint256 y) returns uint256 { + return mulDivUpAbstractPlus(x, ONE18(), y); +} + +function discreteQuotientMulDiv(uint256 x, uint256 y, uint256 z) returns uint256 +{ + uint256 res; + require z != 0 && noOverFlowMul(x, y); + // Discrete quotients: + require( + ((x ==0 || y ==0) && res == 0) || + (x == z && res == y) || + (y == z && res == x) || + constQuotient(x, y, z, 2, res) || // Division quotient is 1/2 or 2 + constQuotient(x, y, z, 5, res) || // Division quotient is 1/5 or 5 + constQuotient(x, y, z, 100, res) // Division quotient is 1/100 or 100 + ); + return res; +} + +function discreteRatioMulDiv(uint256 x, uint256 y, uint256 z) returns uint256 +{ + uint256 res; + require z != 0 && noOverFlowMul(x, y); + // Discrete ratios: + require( + ((x ==0 || y ==0) && res == 0) || + (x == z && res == y) || + (y == z && res == x) || + constRatio(x, y, z, 2, 1, res) || // f = 2*x or f = x/2 (same for y) + constRatio(x, y, z, 5, 1, res) || // f = 5*x or f = x/5 (same for y) + constRatio(x, y, z, 2, 3, res) || // f = 2*x/3 or f = 3*x/2 (same for y) + constRatio(x, y, z, 2, 7, res) // f = 2*x/7 or f = 7*x/2 (same for y) + ); + return res; +} + +function noOverFlowMul(uint256 x, uint256 y) returns bool +{ + return x * y <= max_uint; +} + +/// @doc Ghost power function that incorporates mathematical pure x^y axioms. +/// @warning Some of these axioms might be false, depending on the Solidity implementation +/// The user must bear in mind that equality-like axioms can be violated because of rounding errors. +ghost _ghostPow(uint256, uint256) returns uint256 { + /// x^0 = 1 + axiom forall uint256 x. _ghostPow(x, 0) == ONE18(); + /// 0^x = 1 + axiom forall uint256 y. _ghostPow(0, y) == 0; + /// x^1 = x + axiom forall uint256 x. _ghostPow(x, ONE18()) == x; + /// 1^y = 1 + axiom forall uint256 y. _ghostPow(ONE18(), y) == ONE18(); + + /// I. x > 1 && y1 > y2 => x^y1 > x^y2 + /// II. x < 1 && y1 > y2 => x^y1 < x^y2 + axiom forall uint256 x. forall uint256 y1. forall uint256 y2. + x >= ONE18() && y1 > y2 => _ghostPow(x, y1) >= _ghostPow(x, y2); + axiom forall uint256 x. forall uint256 y1. forall uint256 y2. + x < ONE18() && y1 > y2 => (_ghostPow(x, y1) <= _ghostPow(x, y2) && _ghostPow(x,y2) <= ONE18()); + axiom forall uint256 x. forall uint256 y. + x < ONE18() && y > ONE18() => (_ghostPow(x, y) <= x); + axiom forall uint256 x. forall uint256 y. + x < ONE18() && y <= ONE18() => (_ghostPow(x, y) >= x); + axiom forall uint256 x. forall uint256 y. + x >= ONE18() && y > ONE18() => (_ghostPow(x, y) >= x); + axiom forall uint256 x. forall uint256 y. + x >= ONE18() && y <= ONE18() => (_ghostPow(x, y) <= x); + /// x1 > x2 && y > 0 => x1^y > x2^y + axiom forall uint256 x1. forall uint256 x2. forall uint256 y. + x1 > x2 => _ghostPow(x1, y) >= _ghostPow(x2, y); + + /* Additional axioms - potentially unsafe + /// x^y * x^(1-y) == x -> 0.01% relative error + axiom forall uint256 x. forall uint256 y. forall uint256 z. + (0 <= y && y <= ONE18() && z + y == to_mathint(ONE18())) => + relativeErrorBound(_ghostPow(x, y) * _ghostPow(x, z), x * ONE18(), ONE18() / 10000); + + /// (x^y)^(1/y) == x -> 1% relative error + axiom forall uint256 x. forall uint256 y. forall uint256 z. + (0 <= y && y <= ONE18() && z * y == ONE18()*ONE18() ) => + relativeErrorBound(_ghostPow(_ghostPow(x, y), z), x, ONE18() / 100); + */ +} + +function CVLPow(uint256 x, uint256 y) returns uint256 { + if (y == 0) {return ONE18();} + if (x == 0) {return 0;} + return _ghostPow(x, y); +} + +function CVLSqrt(uint256 x) returns uint256 { + mathint SQRT; + require SQRT*SQRT <= to_mathint(x) && (SQRT + 1)*(SQRT + 1) > to_mathint(x); + return require_uint256(SQRT); +} + +// For Aave +function rayMulCVLPrecise(uint x, uint y) returns uint256 { + return require_uint256((x*y + RAY()/2) / RAY()); +// return rayMul(x,y); +// uint256 ret; +// require x*y + RAY()/2 <= ret*RAY() && ret*RAY() < x*y + 3*RAY()/2; +// return ret; +} + +function rayDivCVLPrecise(uint x, uint y) returns uint256 { + require y != 0; + return require_uint256((x*RAY() + y/2)/y); + //return rayDiv(x,y); + // require y != 0; + //uint256 ret; + //require x*RAY() + y/2 <= y*ret && y*ret < x*RAY() + y/2 + y; + //return ret; +} diff --git a/certora/experimental/specs/PriceAggregators/chainlink.spec b/certora/experimental/specs/PriceAggregators/chainlink.spec new file mode 100644 index 00000000..889e39e6 --- /dev/null +++ b/certora/experimental/specs/PriceAggregators/chainlink.spec @@ -0,0 +1,4 @@ +methods { + function _.getRoundData(uint80) external => NONDET; + function _.latestRoundData() external => NONDET; +} \ No newline at end of file diff --git a/certora/experimental/specs/PriceAggregators/tellor.spec b/certora/experimental/specs/PriceAggregators/tellor.spec new file mode 100644 index 00000000..03f90c77 --- /dev/null +++ b/certora/experimental/specs/PriceAggregators/tellor.spec @@ -0,0 +1,3 @@ +methods { + function _.getTellorCurrentValue(uint256) external => NONDET; +} \ No newline at end of file diff --git a/certora/experimental/specs/generic.spec b/certora/experimental/specs/generic.spec new file mode 100644 index 00000000..b6234ffd --- /dev/null +++ b/certora/experimental/specs/generic.spec @@ -0,0 +1,88 @@ +/* +This rule find which functions are privileged. +A function is privileged if there is only one address that can call it. + +The rules finds this by finding which functions can be called by two different users. +*/ +rule privilegedOperation(method f, address privileged) { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + + storage initialStorage = lastStorage; + f@withrevert(e1, arg); // privileged succeeds executing candidate privileged operation. + bool firstSucceeded = !lastReverted; + + env e2; + calldataarg arg2; + require e2.msg.sender != privileged; + f@withrevert(e2, arg2) at initialStorage; // unprivileged + bool secondSucceeded = !lastReverted; + + assert !(firstSucceeded && secondSucceeded); +} + +rule timeoutChecker(method f) { + storage before = lastStorage; + env e; calldataarg arg; + f(e,arg); + assert before == lastStorage; +} + +/* +This rule find which functions that can be called, may fail due to someone else calling a function right before. + +This is n expensive rule - might fail on the demo site on big contracts +*/ +rule simpleFrontRunning(method f, address privileged) filtered { f-> !f.isView } { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + storage initialStorage = lastStorage; + f(e1, arg); + bool firstSucceeded = !lastReverted; + env e2; + calldataarg arg2; + require e2.msg.sender != e1.msg.sender; + f(e2, arg2) at initialStorage; + f@withrevert(e1, arg); + bool succeeded = !lastReverted; + assert succeeded; +} + +rule noRevert(method f) { + env e; + calldataarg arg; + require e.msg.value == 0; + f@withrevert(e, arg); + assert !lastReverted; +} + + +rule alwaysRevert(method f) { + env e; + calldataarg arg; + f@withrevert(e, arg); + assert lastReverted; +} + +use builtin rule sanity; +use builtin rule hasDelegateCalls; +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; + +/** + +// Integrate rules from generic.spec in importing specs like this: + +use builtin rule sanity filtered { f -> f.contract == currentContract } +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } + + **/ \ No newline at end of file diff --git a/certora/experimental/specs/optimizations.spec b/certora/experimental/specs/optimizations.spec new file mode 100644 index 00000000..8246442e --- /dev/null +++ b/certora/experimental/specs/optimizations.spec @@ -0,0 +1,159 @@ + + +// optimizing summaries +methods { + function utils.SECONDS_PER_YEAR() external returns (uint256) envfree; + + // function ReserveLogic.cumulateToLiquidityIndex( + // DataTypes.ReserveData storage reserve, + // uint256 totalLiquidity, + // uint256 amount + //) internal returns (uint256) => cumulateToLiquidityIndexCVL(); + + function ValidationLogic.validateLiquidationCall( + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveData storage collateralReserve, + DataTypes.ReserveData storage debtReserve, + DataTypes.ValidateLiquidationCallParams memory params + ) internal => NONDET; + + /* function ValidationLogic.validateRepay( + DataTypes.ReserveCache memory reserveCache, + uint256 amountSent, + DataTypes.InterestRateMode interestRateMode, + address onBehalfOf, + uint256 stableDebt, + uint256 variableDebt + ) internal => NONDET; + */ + function ValidationLogic.validateWithdraw( + DataTypes.ReserveCache memory reserveCache, + uint256 amount, + uint256 userBalance + ) internal => NONDET; + + function ValidationLogic.validateFlashloanSimple( + DataTypes.ReserveData storage reserve, + uint256 amount + ) internal => NONDET; + + function ValidationLogic.validateAutomaticUseAsCollateral( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveConfigurationMap memory reserveConfig, + address aTokenAddress + ) internal returns (bool) => NONDET; + + function ReserveLogic._accrueToTreasury( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache + ) internal => _accrueToTreasuryCVL(); + + function MathUtils.calculateCompoundedInterest(uint256 rate,uint40 lastUpdateTimestamp, uint256 currentTimestamp) + internal returns (uint256)=> compoundedInterestCVL(rate, lastUpdateTimestamp, currentTimestamp); + + function MathUtils.calculateLinearInterest(uint256 rate, uint40 lastUpdateTimestamp) + internal returns (uint256) with (env e) => f_linearInterestCVL(e,rate,lastUpdateTimestamp); + + + function GenericLogic._getUserDebtInBaseCurrency( + address, + DataTypes.ReserveData storage, + uint256, + uint256 + ) internal returns (uint256) => NONDET /* difficulty 106 */; + + function GenericLogic.calculateUserAccountData( + mapping (address => DataTypes.ReserveData) storage, + mapping (uint256 => address) storage, + mapping (uint8 => DataTypes.EModeCategory) storage, + DataTypes.CalculateUserAccountDataParams memory + ) internal returns (uint256,uint256,uint256,uint256,uint256,bool) => NONDET /* difficulty 214 */; + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveData storage, + DataTypes.ReserveCache memory, + address, + address, + uint256, + uint256, + uint256, + address // IPriceOracleGetter + ) internal returns (uint256,uint256,uint256) => NONDET /* difficulty 90 */; + + function ValidationLogic.validateSupply( + DataTypes.ReserveCache memory, + DataTypes.ReserveData storage, + uint256, + address + ) internal => NONDET /* difficulty 52 */; + + function ValidationLogic.validateBorrow( + mapping (address => DataTypes.ReserveData) storage reservesData, + mapping (uint256 => address) storage reservesList, + mapping (uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.ValidateBorrowParams memory params + ) internal => NONDET; + /* difficulty 331 ; */ + + function ValidationLogic.validateHealthFactor( + mapping (address => DataTypes.ReserveData) storage, + mapping (uint256 => address) storage, + mapping (uint8 => DataTypes.EModeCategory) storage, + DataTypes.UserConfigurationMap memory, + address, + uint8, + uint256, + address + ) internal returns (uint256,bool) => NONDET /* difficulty 214 */; + + function ValidationLogic.validateHFAndLtv( + mapping (address => DataTypes.ReserveData) storage, + mapping (uint256 => address) storage, + mapping (uint8 => DataTypes.EModeCategory) storage, + DataTypes.UserConfigurationMap memory, + address, + address, + uint256, + address, + uint8 + ) internal => NONDET /* difficulty 216 */; + + + // xxxx + // monotonically increase stuff in the reserve + // updates lastUpdateTimestamp + // function ReserveLogic.updateState( + // DataTypes.ReserveData storage reserve, + // DataTypes.ReserveCache memory reserveCache + //) internal => NONDET; + +} // END METHODS BLOCK + + + +ghost linearInterestCVL(mathint,mathint,mathint) returns uint { + axiom forall mathint rate. forall mathint lastUpdateTimestamp. forall mathint currentTimestamp. + to_mathint(linearInterestCVL(rate, lastUpdateTimestamp, currentTimestamp)) == + RAY() + rate * (currentTimestamp - lastUpdateTimestamp) / (365*86400); + // the 365*86400 is SECONDS_PER_YEAR() as defined in MathUtils. +} +ghost compoundedInterestCVL(mathint,mathint,mathint) returns uint { + axiom forall mathint rate. forall mathint lastUpdateTimestamp. forall mathint currentTimestamp. + to_mathint(compoundedInterestCVL(rate, lastUpdateTimestamp, currentTimestamp)) >= + to_mathint(linearInterestCVL(rate, lastUpdateTimestamp, currentTimestamp)); + // RAY() + rate * (currentTimestamp - lastUpdateTimestamp) / (365*86400); + // the 365*86400 is SECONDS_PER_YEAR() as defined in MathUtils. +} + +ghost address ASSET; //The asset that current rule refers to. Should be assigned from within the rule. + +// The function _accrueToTreasury(...) only writes to the field accruedToTreasury. +function _accrueToTreasuryCVL() { + havoc currentContract._reserves[ASSET].accruedToTreasury; +} + +function f_linearInterestCVL(env e,uint256 rate,uint40 lastUpdateTimestamp) returns uint256 { + return linearInterestCVL(rate,lastUpdateTimestamp, e.block.timestamp); +} diff --git a/certora/experimental/specs/problems.spec b/certora/experimental/specs/problems.spec new file mode 100644 index 00000000..3ebe9e37 --- /dev/null +++ b/certora/experimental/specs/problems.spec @@ -0,0 +1 @@ +// workarounds for crashes \ No newline at end of file diff --git a/certora/experimental/specs/setup/builtin_assertions.spec b/certora/experimental/specs/setup/builtin_assertions.spec new file mode 100644 index 00000000..147a845f --- /dev/null +++ b/certora/experimental/specs/setup/builtin_assertions.spec @@ -0,0 +1,28 @@ +import "../ERC20/erc20cvl.spec"; +import "../ERC20/WETHcvl.spec"; +import "../ERC721/erc721.spec"; +import "../ERC1967/erc1967.spec"; +import "../PriceAggregators/chainlink.spec"; +import "../PriceAggregators/tellor.spec"; + +// aave imports +import "../Aave/aToken.spec"; +import "../Aave/AddressProvider.spec"; +import "../Aave/PriceOracleSentinel.spec"; +import "../Aave/PriceOracle.spec"; +import "../Aave/ACLManager.spec"; +import "../Aave/ReserveInterestRateStrategy.spec"; +import "../Aave/FlashLoanReceiver.spec"; + +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +rule check_builtin_assertions(method f) + filtered { f -> f.contract == currentContract } +{ + env e; + calldataarg arg; + f(e, arg); + assert true; +} diff --git a/certora/experimental/specs/setup/sanity.spec b/certora/experimental/specs/setup/sanity.spec new file mode 100644 index 00000000..851f54f7 --- /dev/null +++ b/certora/experimental/specs/setup/sanity.spec @@ -0,0 +1,5 @@ +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract }; \ No newline at end of file diff --git a/certora/experimental/specs/setup/sanity_with_all_default_summaries.spec b/certora/experimental/specs/setup/sanity_with_all_default_summaries.spec new file mode 100644 index 00000000..34355e82 --- /dev/null +++ b/certora/experimental/specs/setup/sanity_with_all_default_summaries.spec @@ -0,0 +1,37 @@ +import "../ERC20/WETHcvl.spec"; +import "../ERC721/erc721.spec"; +import "../ERC1967/erc1967.spec"; +import "../PriceAggregators/chainlink.spec"; +import "../PriceAggregators/tellor.spec"; + +// aave imports +import "../Aave/aToken.spec"; +import "../Aave/AddressProvider.spec"; +import "../Aave/PriceOracleSentinel.spec"; +import "../Aave/PriceOracle.spec"; +import "../Aave/ACLManager.spec"; +import "../Aave/ReserveInterestRateStrategy.spec"; +import "../Aave/FlashLoanReceiver.spec"; +import "../Aave/Pool.spec"; + +// standard +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +import "../generic.spec"; // pick additional rules from here + +use builtin rule sanity filtered { f -> f.contract == currentContract } + +// Simple rule - depends on better summaries for `IReserveInterestRateStrategy` OR linking it (with the risk of longer running time) +// See in https://prover.certora.com/output/60724/b7043622211340e98e602bd4e46c9aff/?anonymousKey=ff16ebf727d2aa6631a187c862a845a624bf64a5 +// by filtering for `virtualUnderlyingBalance` accesses in the trace +rule user_withdraw_cannot_surpass_VA() { + env e; + uint256 amount; + address to; + address asset; + uint128 virtual_bal_before = getReserveDataExtended(e,asset).virtualUnderlyingBalance; + withdraw(e,asset, amount, to); + assert amount <= assert_uint256(virtual_bal_before); +} diff --git a/certora/experimental/specs/setup/sanity_with_erc20cvl.spec b/certora/experimental/specs/setup/sanity_with_erc20cvl.spec new file mode 100644 index 00000000..3281dff0 --- /dev/null +++ b/certora/experimental/specs/setup/sanity_with_erc20cvl.spec @@ -0,0 +1,8 @@ +import "../ERC20/erc20cvl.spec"; +import "../ERC20/WETHcvl.spec"; + +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract }; \ No newline at end of file diff --git a/certora/experimental/specs/setup/sanity_with_erc20dispatched.spec b/certora/experimental/specs/setup/sanity_with_erc20dispatched.spec new file mode 100644 index 00000000..f3301977 --- /dev/null +++ b/certora/experimental/specs/setup/sanity_with_erc20dispatched.spec @@ -0,0 +1,7 @@ +import "../ERC20/erc20dispatched.spec"; + +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract }; \ No newline at end of file diff --git a/certora/experimental/specs/unresolved.spec b/certora/experimental/specs/unresolved.spec new file mode 100644 index 00000000..8d0a8f5d --- /dev/null +++ b/certora/experimental/specs/unresolved.spec @@ -0,0 +1,4 @@ +// summaries for unresolved calls +methods { + // xxx may need a dispatcher list for flashLoan where PTA-for-linking failed +} \ No newline at end of file diff --git a/certora/solvency/BorrowLogic.sol b/certora/solvency/BorrowLogic.sol new file mode 100644 index 00000000..0eb38022 --- /dev/null +++ b/certora/solvency/BorrowLogic.sol @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {GPv2SafeERC20} from '../../../dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +import {SafeCast} from '../../../dependencies/openzeppelin/contracts/SafeCast.sol'; +import {IERC20} from '../../../dependencies/openzeppelin/contracts/IERC20.sol'; +import {IVariableDebtToken} from '../../../interfaces/IVariableDebtToken.sol'; +import {IAToken} from '../../../interfaces/IAToken.sol'; +import {UserConfiguration} from '../configuration/UserConfiguration.sol'; +import {ReserveConfiguration} from '../configuration/ReserveConfiguration.sol'; +import {DataTypes} from '../types/DataTypes.sol'; +import {ValidationLogic} from './ValidationLogic.sol'; +import {ReserveLogic} from './ReserveLogic.sol'; +import {IsolationModeLogic} from './IsolationModeLogic.sol'; + +/** + * @title BorrowLogic library + * @author Aave + * @notice Implements the base logic for all the actions related to borrowing + */ +library BorrowLogic { + using ReserveLogic for DataTypes.ReserveCache; + using ReserveLogic for DataTypes.ReserveData; + using GPv2SafeERC20 for IERC20; + using UserConfiguration for DataTypes.UserConfigurationMap; + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + using SafeCast for uint256; + + // See `IPool` for descriptions + event Borrow( + address indexed reserve, + address user, + address indexed onBehalfOf, + uint256 amount, + DataTypes.InterestRateMode interestRateMode, + uint256 borrowRate, + uint16 indexed referralCode + ); + event Repay( + address indexed reserve, + address indexed user, + address indexed repayer, + uint256 amount, + bool useATokens + ); + event IsolationModeTotalDebtUpdated(address indexed asset, uint256 totalDebt); + event ReserveUsedAsCollateralDisabled(address indexed reserve, address indexed user); + + /** + * @notice Implements the borrow feature. Borrowing allows users that provided collateral to draw liquidity from the + * Aave protocol proportionally to their collateralization power. For isolated positions, it also increases the + * isolated debt. + * @dev Emits the `Borrow()` event + * @param reservesData The state of all the reserves + * @param reservesList The addresses of all the active reserves + * @param eModeCategories The configuration of all the efficiency mode categories + * @param userConfig The user configuration mapping that tracks the supplied/borrowed assets + * @param params The additional parameters needed to execute the borrow function + */ + function executeBorrow( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteBorrowParams memory params + ) external { + DataTypes.ReserveData storage reserve = reservesData[params.asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + reserve.updateState(reserveCache); + + ( + bool isolationModeActive, + address isolationModeCollateralAddress, + uint256 isolationModeDebtCeiling + ) = userConfig.getIsolationModeState(reservesData, reservesList); + + ValidationLogic.validateBorrow( + reservesData, + reservesList, + eModeCategories, + DataTypes.ValidateBorrowParams({ + reserveCache: reserveCache, + userConfig: userConfig, + asset: params.asset, + userAddress: params.onBehalfOf, + amount: params.amount, + interestRateMode: params.interestRateMode, + reservesCount: params.reservesCount, + oracle: params.oracle, + userEModeCategory: params.userEModeCategory, + priceOracleSentinel: params.priceOracleSentinel, + isolationModeActive: isolationModeActive, + isolationModeCollateralAddress: isolationModeCollateralAddress, + isolationModeDebtCeiling: isolationModeDebtCeiling + }) + ); + + bool isFirstBorrowing = false; + + (isFirstBorrowing, reserveCache.nextScaledVariableDebt) = IVariableDebtToken( + reserveCache.variableDebtTokenAddress + ).mint(params.user, params.onBehalfOf, params.amount, reserveCache.nextVariableBorrowIndex); + + if (isFirstBorrowing) { + userConfig.setBorrowing(reserve.id, true); + } + + if (isolationModeActive) { + uint256 nextIsolationModeTotalDebt = reservesData[isolationModeCollateralAddress] + .isolationModeTotalDebt += (params.amount / + 10 ** + (reserveCache.reserveConfiguration.getDecimals() - + ReserveConfiguration.DEBT_CEILING_DECIMALS)).toUint128(); + emit IsolationModeTotalDebtUpdated( + isolationModeCollateralAddress, + nextIsolationModeTotalDebt + ); + } + + reserve.updateInterestRatesAndVirtualBalance( + reserveCache, + params.asset, + 0, + params.releaseUnderlying ? params.amount : 0 + ); + + if (params.releaseUnderlying) { + IAToken(reserveCache.aTokenAddress).transferUnderlyingTo(params.user, params.amount); + } + + emit Borrow( + params.asset, + params.user, + params.onBehalfOf, + params.amount, + DataTypes.InterestRateMode.VARIABLE, + reserve.currentVariableBorrowRate, + params.referralCode + ); + } + + /** + * @notice Implements the repay feature. Repaying transfers the underlying back to the aToken and clears the + * equivalent amount of debt for the user by burning the corresponding debt token. For isolated positions, it also + * reduces the isolated debt. + * @dev Emits the `Repay()` event + * @param reservesData The state of all the reserves + * @param reservesList The addresses of all the active reserves + * @param userConfig The user configuration mapping that tracks the supplied/borrowed assets + * @param params The additional parameters needed to execute the repay function + * @return The actual amount being repaid + */ + function executeRepay( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteRepayParams memory params + ) external returns (uint256) { + DataTypes.ReserveData storage reserve = reservesData[params.asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + reserve.updateState(reserveCache); + repay_hook_1(reserveCache); + uint256 variableDebt = IERC20(reserveCache.variableDebtTokenAddress).balanceOf( + params.onBehalfOf + ); + + ValidationLogic.validateRepay( + reserveCache, + params.amount, + params.interestRateMode, + params.onBehalfOf, + variableDebt + ); + + uint256 paybackAmount = variableDebt; + + // Allows a user to repay with aTokens without leaving dust from interest. + if (params.useATokens && params.amount == type(uint256).max) { + params.amount = IAToken(reserveCache.aTokenAddress).balanceOf(msg.sender); + } + + if (params.amount < paybackAmount) { + paybackAmount = params.amount; + } + repay_hook_2(reserveCache); + reserveCache.nextScaledVariableDebt = IVariableDebtToken(reserveCache.variableDebtTokenAddress) + .burn(params.onBehalfOf, paybackAmount, reserveCache.nextVariableBorrowIndex); + repay_hook_3(reserveCache, paybackAmount); + reserve.updateInterestRatesAndVirtualBalance( + reserveCache, + params.asset, + params.useATokens ? 0 : paybackAmount, + 0 + ); + + if (variableDebt - paybackAmount == 0) { + userConfig.setBorrowing(reserve.id, false); + } + + IsolationModeLogic.updateIsolatedDebtIfIsolated( + reservesData, + reservesList, + userConfig, + reserveCache, + paybackAmount + ); + + if (params.useATokens) { + IAToken(reserveCache.aTokenAddress).burn( + msg.sender, + reserveCache.aTokenAddress, + paybackAmount, + reserveCache.nextLiquidityIndex + ); + repay_hook_4(reserveCache); + bool isCollateral = userConfig.isUsingAsCollateral(reserve.id); + // in case of aToken repayment the msg.sender must always repay on behalf of itself + if (isCollateral && IAToken(reserveCache.aTokenAddress).scaledBalanceOf(msg.sender) == 0) { + userConfig.setUsingAsCollateral(reserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.asset, msg.sender); + } + } else { + IERC20(params.asset).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, paybackAmount); + IAToken(reserveCache.aTokenAddress).handleRepayment( + msg.sender, + params.onBehalfOf, + paybackAmount + ); + } + + emit Repay(params.asset, params.onBehalfOf, msg.sender, paybackAmount, params.useATokens); + + return paybackAmount; + } + function repay_hook_1(DataTypes.ReserveCache memory reserveCache) internal {} + function repay_hook_2(DataTypes.ReserveCache memory reserveCache) internal {} + function repay_hook_3(DataTypes.ReserveCache memory reserveCache, uint256 paybackAmount) internal {} + function repay_hook_4(DataTypes.ReserveCache memory reserveCache) internal {} +} diff --git a/certora/solvency/LiquidationLogic.sol b/certora/solvency/LiquidationLogic.sol new file mode 100644 index 00000000..adb22b62 --- /dev/null +++ b/certora/solvency/LiquidationLogic.sol @@ -0,0 +1,607 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IERC20} from '../../../dependencies/openzeppelin/contracts//IERC20.sol'; +import {GPv2SafeERC20} from '../../../dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +import {PercentageMath} from '../../libraries/math/PercentageMath.sol'; +import {WadRayMath} from '../../libraries/math/WadRayMath.sol'; +import {DataTypes} from '../../libraries/types/DataTypes.sol'; +import {ReserveLogic} from './ReserveLogic.sol'; +import {ValidationLogic} from './ValidationLogic.sol'; +import {GenericLogic} from './GenericLogic.sol'; +import {IsolationModeLogic} from './IsolationModeLogic.sol'; +import {EModeLogic} from './EModeLogic.sol'; +import {UserConfiguration} from '../../libraries/configuration/UserConfiguration.sol'; +import {ReserveConfiguration} from '../../libraries/configuration/ReserveConfiguration.sol'; +import {EModeConfiguration} from '../../libraries/configuration/EModeConfiguration.sol'; +import {IAToken} from '../../../interfaces/IAToken.sol'; +import {IVariableDebtToken} from '../../../interfaces/IVariableDebtToken.sol'; +import {IPriceOracleGetter} from '../../../interfaces/IPriceOracleGetter.sol'; +import {SafeCast} from '../../../dependencies/openzeppelin/contracts/SafeCast.sol'; +import {Errors} from '../helpers/Errors.sol'; + +/** + * @title LiquidationLogic library + * @author Aave + * @notice Implements actions involving management of collateral in the protocol, the main one being the liquidations + */ +library LiquidationLogic { + using WadRayMath for uint256; + using PercentageMath for uint256; + using ReserveLogic for DataTypes.ReserveCache; + using ReserveLogic for DataTypes.ReserveData; + using UserConfiguration for DataTypes.UserConfigurationMap; + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + using GPv2SafeERC20 for IERC20; + using SafeCast for uint256; + + // See `IPool` for descriptions + event ReserveUsedAsCollateralEnabled(address indexed reserve, address indexed user); + event ReserveUsedAsCollateralDisabled(address indexed reserve, address indexed user); + event DeficitCreated(address indexed user, address indexed debtAsset, uint256 amount); + event LiquidationCall( + address indexed collateralAsset, + address indexed debtAsset, + address indexed user, + uint256 debtToCover, + uint256 liquidatedCollateralAmount, + address liquidator, + bool receiveAToken + ); + + /** + * @dev Default percentage of borrower's debt to be repaid in a liquidation. + * @dev Percentage applied when the users health factor is above `CLOSE_FACTOR_HF_THRESHOLD` + * Expressed in bps, a value of 0.5e4 results in 50.00% + */ + uint256 internal constant DEFAULT_LIQUIDATION_CLOSE_FACTOR = 0.5e4; + + /** + * @dev This constant represents the upper bound on the health factor, below(inclusive) which the full amount of debt becomes liquidatable. + * A value of 0.95e18 results in 0.95 + */ + uint256 public constant CLOSE_FACTOR_HF_THRESHOLD = 0.95e18; + + /** + * @dev This constant represents a base value threshold. + * If the total collateral or debt on a position is below this threshold, the close factor is raised to 100%. + * @notice The default value assumes that the basePrice is usd denominated by 8 decimals and needs to be adjusted in a non USD-denominated pool. + */ + uint256 public constant MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD = 2000e8; + + /** + * @dev This constant represents the minimum amount of assets in base currency that need to be leftover after a liquidation, if not clearing a position completely. + * This parameter is inferred from MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD as the logic is dependent. + * Assuming a MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD of `n` a liquidation of `n+1` might result in `n/2` leftover which is assumed to be still economically liquidatable. + * This mechanic was introduced to ensure liquidators don't optimize gas by leaving some wei on the liquidation. + */ + uint256 public constant MIN_LEFTOVER_BASE = MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD / 2; + + struct LiquidationCallLocalVars { + uint256 userCollateralBalance; + uint256 userReserveDebt; + uint256 actualDebtToLiquidate; + uint256 actualCollateralToLiquidate; + uint256 liquidationBonus; + uint256 healthFactor; + uint256 liquidationProtocolFeeAmount; + uint256 totalCollateralInBaseCurrency; + uint256 totalDebtInBaseCurrency; + uint256 collateralToLiquidateInBaseCurrency; + uint256 userReserveDebtInBaseCurrency; + uint256 userReserveCollateralInBaseCurrency; + uint256 collateralAssetPrice; + uint256 debtAssetPrice; + uint256 collateralAssetUnit; + uint256 debtAssetUnit; + IAToken collateralAToken; + DataTypes.ReserveCache debtReserveCache; + } + + /** + * @notice Function to liquidate a position if its Health Factor drops below 1. The caller (liquidator) + * covers `debtToCover` amount of debt of the user getting liquidated, and receives + * a proportional amount of the `collateralAsset` plus a bonus to cover market risk + * @dev Emits the `LiquidationCall()` event, and the `DeficitCreated()` event if the liquidation results in bad debt + * @param reservesData The state of all the reserves + * @param reservesList The addresses of all the active reserves + * @param usersConfig The users configuration mapping that track the supplied/borrowed assets + * @param eModeCategories The configuration of all the efficiency mode categories + * @param params The additional parameters needed to execute the liquidation function + */ + function executeLiquidationCall( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(address => DataTypes.UserConfigurationMap) storage usersConfig, + mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.ExecuteLiquidationCallParams memory params + ) external { + LiquidationCallLocalVars memory vars; + + DataTypes.ReserveData storage collateralReserve = reservesData[params.collateralAsset]; + DataTypes.ReserveData storage debtReserve = reservesData[params.debtAsset]; + DataTypes.UserConfigurationMap storage userConfig = usersConfig[params.user]; + vars.debtReserveCache = debtReserve.cache(); + debtReserve.updateState(vars.debtReserveCache); HOOK_liquidation_after_updateState_DBT(); + + ( + vars.totalCollateralInBaseCurrency, + vars.totalDebtInBaseCurrency, + , + , + vars.healthFactor, + + ) = GenericLogic.calculateUserAccountData( + reservesData, + reservesList, + eModeCategories, + DataTypes.CalculateUserAccountDataParams({ + userConfig: userConfig, + reservesCount: params.reservesCount, + user: params.user, + oracle: params.priceOracle, + userEModeCategory: params.userEModeCategory + }) + ); HOOK_liquidation_after_calculateDebt(vars.userTotalDebt); + + vars.collateralAToken = IAToken(collateralReserve.aTokenAddress); + vars.userCollateralBalance = vars.collateralAToken.balanceOf(params.user); + vars.userReserveDebt = IERC20(vars.debtReserveCache.variableDebtTokenAddress).balanceOf( + params.user + ); + + ValidationLogic.validateLiquidationCall( + userConfig, + collateralReserve, + debtReserve, + DataTypes.ValidateLiquidationCallParams({ + debtReserveCache: vars.debtReserveCache, + totalDebt: vars.userReserveDebt, + healthFactor: vars.healthFactor, + priceOracleSentinel: params.priceOracleSentinel + }) + ); + + if ( + params.userEModeCategory != 0 && + EModeConfiguration.isReserveEnabledOnBitmap( + eModeCategories[params.userEModeCategory].collateralBitmap, + collateralReserve.id + ) + ) { + vars.liquidationBonus = eModeCategories[params.userEModeCategory].liquidationBonus; + } else { + vars.liquidationBonus = collateralReserve.configuration.getLiquidationBonus(); + } + vars.collateralAssetPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice( + params.collateralAsset + ); + vars.debtAssetPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice(params.debtAsset); + vars.collateralAssetUnit = 10 ** collateralReserve.configuration.getDecimals(); + vars.debtAssetUnit = 10 ** vars.debtReserveCache.reserveConfiguration.getDecimals(); + + vars.userReserveDebtInBaseCurrency = + (vars.userReserveDebt * vars.debtAssetPrice) / + vars.debtAssetUnit; + + vars.userReserveCollateralInBaseCurrency = + (vars.userCollateralBalance * vars.collateralAssetPrice) / + vars.collateralAssetUnit; + + // by default whole debt in the reserve could be liquidated + uint256 maxLiquidatableDebt = vars.userReserveDebt; + // but if debt and collateral is above or equal MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD + // and health factor is above CLOSE_FACTOR_HF_THRESHOLD this amount may be adjusted + if ( + vars.userReserveCollateralInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + vars.userReserveDebtInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + vars.healthFactor > CLOSE_FACTOR_HF_THRESHOLD + ) { + uint256 totalDefaultLiquidatableDebtInBaseCurrency = vars.totalDebtInBaseCurrency.percentMul( + DEFAULT_LIQUIDATION_CLOSE_FACTOR + ); + + // if the debt is more then DEFAULT_LIQUIDATION_CLOSE_FACTOR % of the whole, + // then we CAN liquidate only up to DEFAULT_LIQUIDATION_CLOSE_FACTOR % + if (vars.userReserveDebtInBaseCurrency > totalDefaultLiquidatableDebtInBaseCurrency) { + maxLiquidatableDebt = + (totalDefaultLiquidatableDebtInBaseCurrency * vars.debtAssetUnit) / + vars.debtAssetPrice; + } + } + + vars.actualDebtToLiquidate = params.debtToCover > maxLiquidatableDebt + ? maxLiquidatableDebt + : params.debtToCover; + + ( + vars.actualCollateralToLiquidate, + vars.actualDebtToLiquidate, + vars.liquidationProtocolFeeAmount, + vars.collateralToLiquidateInBaseCurrency + ) = _calculateAvailableCollateralToLiquidate( + collateralReserve.configuration, + vars.collateralAssetPrice, + vars.collateralAssetUnit, + vars.debtAssetPrice, + vars.debtAssetUnit, + vars.actualDebtToLiquidate, + vars.userCollateralBalance, + vars.liquidationBonus + ); + + // to prevent accumulation of dust on the protocol, it is enforced that you either + // 1. liquidate all debt + // 2. liquidate all collateral + // 3. leave more than MIN_LEFTOVER_BASE of collateral & debt + if ( + vars.actualDebtToLiquidate < vars.userReserveDebt && + vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount < + vars.userCollateralBalance + ) { + bool isDebtMoreThanLeftoverThreshold = ((vars.userReserveDebt - vars.actualDebtToLiquidate) * + vars.debtAssetPrice) / + vars.debtAssetUnit >= + MIN_LEFTOVER_BASE; + + bool isCollateralMoreThanLeftoverThreshold = ((vars.userCollateralBalance - + vars.actualCollateralToLiquidate - + vars.liquidationProtocolFeeAmount) * vars.collateralAssetPrice) / + vars.collateralAssetUnit >= + MIN_LEFTOVER_BASE; + + require( + isDebtMoreThanLeftoverThreshold && isCollateralMoreThanLeftoverThreshold, + Errors.MUST_NOT_LEAVE_DUST + ); + } + + // If the collateral being liquidated is equal to the user balance, + // we set the currency as not being used as collateral anymore + if ( + vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount == + vars.userCollateralBalance + ) { + userConfig.setUsingAsCollateral(collateralReserve.id, false); + emit ReserveUsedAsCollateralDisabled(params.collateralAsset, params.user); + } + + bool hasNoCollateralLeft = vars.totalCollateralInBaseCurrency == + vars.collateralToLiquidateInBaseCurrency; + _burnDebtTokens( + vars.debtReserveCache, + debtReserve, + userConfig, + params.user, + params.debtAsset, + vars.userReserveDebt, + vars.actualDebtToLiquidate, + hasNoCollateralLeft + ); + + // IsolationModeTotalDebt only discounts `actualDebtToLiquidate`, not the fully burned amount in case of deficit creation. + // This is by design as otherwise debt debt ceiling would render ineffective if a collateral asset faces bad debt events. + // The governance can decide the raise the ceiling to discount manifested deficit. + IsolationModeLogic.updateIsolatedDebtIfIsolated( + reservesData, + reservesList, + userConfig, + vars.debtReserveCache, + vars.actualDebtToLiquidate + ); + + if (params.receiveAToken) { + _liquidateATokens(reservesData, reservesList, usersConfig, collateralReserve, params, vars); + } else { + _burnCollateralATokens(collateralReserve, params, vars); HOOK_liquidation_after_updateState_COL(); + } + + // Transfer fee to treasury if it is non-zero + if (vars.liquidationProtocolFeeAmount != 0) { + uint256 liquidityIndex = collateralReserve.getNormalizedIncome(); + uint256 scaledDownLiquidationProtocolFee = vars.liquidationProtocolFeeAmount.rayDiv( + liquidityIndex + ); + uint256 scaledDownUserBalance = vars.collateralAToken.scaledBalanceOf(params.user); + // To avoid trying to send more aTokens than available on balance, due to 1 wei imprecision + if (scaledDownLiquidationProtocolFee > scaledDownUserBalance) { + vars.liquidationProtocolFeeAmount = scaledDownUserBalance.rayMul(liquidityIndex); + } + vars.collateralAToken.transferOnLiquidation( + params.user, + vars.collateralAToken.RESERVE_TREASURY_ADDRESS(), + vars.liquidationProtocolFeeAmount + ); + } + + // burn bad debt if necessary + // Each additional debt asset already adds around ~75k gas to the liquidation. + // To keep the liquidation gas under control, 0 usd collateral positions are not touched, as there is no immediate benefit in burning or transfering to treasury. + if (hasNoCollateralLeft && userConfig.isBorrowingAny()) { + _burnBadDebt(reservesData, reservesList, userConfig, params.reservesCount, params.user); + } + + // Transfers the debt asset being repaid to the aToken, where the liquidity is kept + IERC20(params.debtAsset).safeTransferFrom( + msg.sender, + vars.debtReserveCache.aTokenAddress, + vars.actualDebtToLiquidate + ); + + IAToken(vars.debtReserveCache.aTokenAddress).handleRepayment( + msg.sender, + params.user, + vars.actualDebtToLiquidate + ); + + emit LiquidationCall( + params.collateralAsset, + params.debtAsset, + params.user, + vars.actualDebtToLiquidate, + vars.actualCollateralToLiquidate, + msg.sender, + params.receiveAToken + ); + } + + /** + * @notice Burns the collateral aTokens and transfers the underlying to the liquidator. + * @dev The function also updates the state and the interest rate of the collateral reserve. + * @param collateralReserve The data of the collateral reserve + * @param params The additional parameters needed to execute the liquidation function + * @param vars The executeLiquidationCall() function local vars + */ + function _burnCollateralATokens( + DataTypes.ReserveData storage collateralReserve, + DataTypes.ExecuteLiquidationCallParams memory params, + LiquidationCallLocalVars memory vars + ) internal { + DataTypes.ReserveCache memory collateralReserveCache = collateralReserve.cache(); + collateralReserve.updateState(collateralReserveCache); + collateralReserve.updateInterestRatesAndVirtualBalance( + collateralReserveCache, + params.collateralAsset, + 0, + vars.actualCollateralToLiquidate + ); + + // Burn the equivalent amount of aToken, sending the underlying to the liquidator + vars.collateralAToken.burn( + params.user, + msg.sender, + vars.actualCollateralToLiquidate, + collateralReserveCache.nextLiquidityIndex + ); + } + + /** + * @notice Liquidates the user aTokens by transferring them to the liquidator. + * @dev The function also checks the state of the liquidator and activates the aToken as collateral + * as in standard transfers if the isolation mode constraints are respected. + * @param reservesData The state of all the reserves + * @param reservesList The addresses of all the active reserves + * @param usersConfig The users configuration mapping that track the supplied/borrowed assets + * @param collateralReserve The data of the collateral reserve + * @param params The additional parameters needed to execute the liquidation function + * @param vars The executeLiquidationCall() function local vars + */ + function _liquidateATokens( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + mapping(address => DataTypes.UserConfigurationMap) storage usersConfig, + DataTypes.ReserveData storage collateralReserve, + DataTypes.ExecuteLiquidationCallParams memory params, + LiquidationCallLocalVars memory vars + ) internal { + uint256 liquidatorPreviousATokenBalance = IERC20(vars.collateralAToken).balanceOf(msg.sender); + vars.collateralAToken.transferOnLiquidation( + params.user, + msg.sender, + vars.actualCollateralToLiquidate + ); + + if (liquidatorPreviousATokenBalance == 0) { + DataTypes.UserConfigurationMap storage liquidatorConfig = usersConfig[msg.sender]; + if ( + ValidationLogic.validateAutomaticUseAsCollateral( + reservesData, + reservesList, + liquidatorConfig, + collateralReserve.configuration, + collateralReserve.aTokenAddress + ) + ) { + liquidatorConfig.setUsingAsCollateral(collateralReserve.id, true); + emit ReserveUsedAsCollateralEnabled(params.collateralAsset, msg.sender); + } + } + } + + /** + * @notice Burns the debt tokens of the user up to the amount being repaid by the liquidator + * or the entire debt if the user is in a bad debt scenario. + * @dev The function alters the `debtReserveCache` state in `vars` to update the debt related data. + * @param debtReserveCache The cached debt reserve parameters + * @param debtReserve The storage pointer of the debt reserve parameters + * @param userConfig The pointer of the user configuration + * @param user The user address + * @param debtAsset The debt asset address + * @param actualDebtToLiquidate The actual debt to liquidate + * @param hasNoCollateralLeft The flag representing, will user will have no collateral left after liquidation + */ + function _burnDebtTokens( + DataTypes.ReserveCache memory debtReserveCache, + DataTypes.ReserveData storage debtReserve, + DataTypes.UserConfigurationMap storage userConfig, + address user, + address debtAsset, + uint256 userReserveDebt, + uint256 actualDebtToLiquidate, + bool hasNoCollateralLeft + ) internal { + // Prior v3.1, there were cases where, after liquidation, the `isBorrowing` flag was left on + // even after the user debt was fully repaid, so to avoid this function reverting in the `_burnScaled` + // (see ScaledBalanceTokenBase contract), we check for any debt remaining. + if (userReserveDebt != 0) { + debtReserveCache.nextScaledVariableDebt = IVariableDebtToken( + debtReserveCache.variableDebtTokenAddress + ).burn( + user, + hasNoCollateralLeft ? userReserveDebt : actualDebtToLiquidate, + debtReserveCache.nextVariableBorrowIndex + ); + } + + uint256 outstandingDebt = userReserveDebt - actualDebtToLiquidate; + if (hasNoCollateralLeft && outstandingDebt != 0) { + debtReserve.deficit += outstandingDebt.toUint128(); + emit DeficitCreated(user, debtAsset, outstandingDebt); + + outstandingDebt = 0; + } + + if (outstandingDebt == 0) { + userConfig.setBorrowing(debtReserve.id, false); + } + + debtReserve.updateInterestRatesAndVirtualBalance( + debtReserveCache, + debtAsset, + actualDebtToLiquidate, + 0 + ); + } + + struct AvailableCollateralToLiquidateLocalVars { + uint256 maxCollateralToLiquidate; + uint256 baseCollateral; + uint256 bonusCollateral; + uint256 collateralAmount; + uint256 debtAmountNeeded; + uint256 liquidationProtocolFeePercentage; + uint256 liquidationProtocolFee; + uint256 collateralToLiquidateInBaseCurrency; + uint256 collateralAssetPrice; + } + + /** + * @notice Calculates how much of a specific collateral can be liquidated, given + * a certain amount of debt asset. + * @dev This function needs to be called after all the checks to validate the liquidation have been performed, + * otherwise it might fail. + * @param collateralReserveConfiguration The data of the collateral reserve + * @param collateralAssetPrice The price of the underlying asset used as collateral + * @param collateralAssetUnit The asset units of the collateral + * @param debtAssetPrice The price of the underlying borrowed asset to be repaid with the liquidation + * @param debtAssetUnit The asset units of the debt + * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover + * @param userCollateralBalance The collateral balance for the specific `collateralAsset` of the user being liquidated + * @param liquidationBonus The collateral bonus percentage to receive as result of the liquidation + * @return The maximum amount that is possible to liquidate given all the liquidation constraints (user balance, close factor) + * @return The amount to repay with the liquidation + * @return The fee taken from the liquidation bonus amount to be paid to the protocol + * @return The collateral amount to liquidate in the base currency used by the price feed + */ + function _calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal pure returns (uint256, uint256, uint256, uint256) { + AvailableCollateralToLiquidateLocalVars memory vars; + vars.collateralAssetPrice = collateralAssetPrice; + vars.liquidationProtocolFeePercentage = collateralReserveConfiguration + .getLiquidationProtocolFee(); + + // This is the base collateral to liquidate based on the given debt to cover + vars.baseCollateral = + ((debtAssetPrice * debtToCover * collateralAssetUnit)) / + (vars.collateralAssetPrice * debtAssetUnit); + + vars.maxCollateralToLiquidate = vars.baseCollateral.percentMul(liquidationBonus); + + if (vars.maxCollateralToLiquidate > userCollateralBalance) { + vars.collateralAmount = userCollateralBalance; + vars.debtAmountNeeded = ((vars.collateralAssetPrice * vars.collateralAmount * debtAssetUnit) / + (debtAssetPrice * collateralAssetUnit)).percentDiv(liquidationBonus); + } else { + vars.collateralAmount = vars.maxCollateralToLiquidate; + vars.debtAmountNeeded = debtToCover; + } + + vars.collateralToLiquidateInBaseCurrency = + (vars.collateralAmount * vars.collateralAssetPrice) / + collateralAssetUnit; + + if (vars.liquidationProtocolFeePercentage != 0) { + vars.bonusCollateral = + vars.collateralAmount - + vars.collateralAmount.percentDiv(liquidationBonus); + + vars.liquidationProtocolFee = vars.bonusCollateral.percentMul( + vars.liquidationProtocolFeePercentage + ); + vars.collateralAmount -= vars.liquidationProtocolFee; + } + return ( + vars.collateralAmount, + vars.debtAmountNeeded, + vars.liquidationProtocolFee, + vars.collateralToLiquidateInBaseCurrency + ); + } + + /** + * @notice Remove a user's bad debt by burning debt tokens. + * @dev This function iterates through all active reserves where the user has a debt position, + * updates their state, and performs the necessary burn. + * @param reservesData The state of all the reserves + * @param reservesList The addresses of all the active reserves + * @param userConfig The user configuration + * @param reservesCount The total number of valid reserves + * @param user The user from which the debt will be burned. + */ + function _burnBadDebt( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + uint256 reservesCount, + address user + ) internal { + for (uint256 i; i < reservesCount; i++) { + if (!userConfig.isBorrowing(i)) { + continue; + } + + address reserveAddress = reservesList[i]; + if (reserveAddress == address(0)) { + continue; + } + + DataTypes.ReserveData storage currentReserve = reservesData[reserveAddress]; + DataTypes.ReserveCache memory reserveCache = currentReserve.cache(); + if (!reserveCache.reserveConfiguration.getActive()) continue; + + currentReserve.updateState(reserveCache); + + _burnDebtTokens( + reserveCache, + currentReserve, + userConfig, + user, + reserveAddress, + IERC20(reserveCache.variableDebtTokenAddress).balanceOf(user), + 0, + true + ); + } + } + function HOOK_liquidation_after_updateState_DBT() internal {} + function HOOK_liquidation_after_updateState_COL() internal {} + function HOOK_liquidation_after_calculateDebt(uint256 userTotalDebt) internal {} +} diff --git a/certora/solvency/Makefile b/certora/solvency/Makefile new file mode 100644 index 00000000..611d7a80 --- /dev/null +++ b/certora/solvency/Makefile @@ -0,0 +1,28 @@ +default: help + +PATCH = applyHarness.patch +CONTRACTS_DIR = ../../src +MUNGED_DIR = munged + +help: + @echo "usage:" + @echo " make clean: remove all generated files (those ignored by git)" + @echo " make $(MUNGED_DIR): create $(MUNGED_DIR) directory by applying the patch file to $(CONTRACTS_DIR)" + @echo " make record: record a new patch file capturing the differences between $(CONTRACTS_DIR) and $(MUNGED_DIR)" + +munged: $(wildcard $(CONTRACTS_DIR)/*.sol) $(PATCH) + rm -rf $@ + mkdir $@ + cp -r ../../src $@ + patch -p0 -d $@ < $(PATCH) + +record: + mkdir tmp + cp -r ../../src tmp + diff -ruN tmp $(MUNGED_DIR) | sed 's+tmp/++g' | sed 's+$(MUNGED_DIR)/++g' > $(PATCH) + rm -rf tmp + +clean: + git clean -fdX + touch $(PATCH) + diff --git a/certora/solvency/ReserveLogic.sol b/certora/solvency/ReserveLogic.sol new file mode 100644 index 00000000..69fc7cbb --- /dev/null +++ b/certora/solvency/ReserveLogic.sol @@ -0,0 +1,396 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IERC20} from '../../../dependencies/openzeppelin/contracts/IERC20.sol'; +import {GPv2SafeERC20} from '../../../dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +import {IVariableDebtToken} from '../../../interfaces/IVariableDebtToken.sol'; +import {IReserveInterestRateStrategy} from '../../../interfaces/IReserveInterestRateStrategy.sol'; +import {ReserveConfiguration} from '../configuration/ReserveConfiguration.sol'; +import {MathUtils} from '../math/MathUtils.sol'; +import {WadRayMath} from '../math/WadRayMath.sol'; +import {PercentageMath} from '../math/PercentageMath.sol'; +import {Errors} from '../helpers/Errors.sol'; +import {DataTypes} from '../types/DataTypes.sol'; +import {SafeCast} from '../../../dependencies/openzeppelin/contracts/SafeCast.sol'; +import {IAToken} from '../../../interfaces/IAToken.sol'; +import {UserConfiguration} from '../configuration/UserConfiguration.sol'; + +/** + * @title ReserveLogic library + * @author Aave + * @notice Implements the logic to update the reserves state + */ +library ReserveLogic { + using WadRayMath for uint256; + using PercentageMath for uint256; + using SafeCast for uint256; + using GPv2SafeERC20 for IERC20; + using ReserveLogic for DataTypes.ReserveData; + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + using UserConfiguration for DataTypes.UserConfigurationMap; + + // See `IPool` for descriptions + event ReserveDataUpdated( + address indexed reserve, + uint256 liquidityRate, + uint256 stableBorrowRate, + uint256 variableBorrowRate, + uint256 liquidityIndex, + uint256 variableBorrowIndex + ); + + event ReserveUsedAsCollateralDisabled(address indexed reserve, address indexed user); + event DeficitCovered(address indexed reserve, address caller, uint256 amountDecreased); + + /** + * @notice Returns the ongoing normalized income for the reserve. + * @dev A value of 1e27 means there is no income. As time passes, the income is accrued + * @dev A value of 2*1e27 means for each unit of asset one unit of income has been accrued + * @param reserve The reserve object + * @return The normalized income, expressed in ray + */ + function getNormalizedIncome( + DataTypes.ReserveData storage reserve + ) internal view returns (uint256) { + uint40 timestamp = reserve.lastUpdateTimestamp; + uint256 ret_val; + //solium-disable-next-line + if (timestamp == block.timestamp) { + //if the index was updated in the same block, no need to perform any calculation + ret_val = reserve.liquidityIndex; + } else { + ret_val = + MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp).rayMul( + reserve.liquidityIndex + ); + } + getNormalizedIncome_hook(ret_val,reserve.aTokenAddress); + return ret_val; + } + + /** + * @notice Returns the ongoing normalized variable debt for the reserve. + * @dev A value of 1e27 means there is no debt. As time passes, the debt is accrued + * @dev A value of 2*1e27 means that for each unit of debt, one unit worth of interest has been accumulated + * @param reserve The reserve object + * @return The normalized variable debt, expressed in ray + */ + function getNormalizedDebt( + DataTypes.ReserveData storage reserve + ) internal view returns (uint256) { + uint40 timestamp = reserve.lastUpdateTimestamp; + uint256 ret_val; + //solium-disable-next-line + if (timestamp == block.timestamp) { + //if the index was updated in the same block, no need to perform any calculation + ret_val = reserve.variableBorrowIndex; + } else { + ret_val = + MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp).rayMul( + reserve.variableBorrowIndex + ); + } + getNormalizedDebt_hook(ret_val,reserve.aTokenAddress); + return ret_val; + } + + /** + * @notice Updates the liquidity cumulative index and the variable borrow index. + * @param reserve The reserve object + * @param reserveCache The caching layer for the reserve data + */ + function updateState( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache + ) internal { + // If time didn't pass since last stored timestamp, skip state update + //solium-disable-next-line + if (reserveCache.reserveLastUpdateTimestamp == uint40(block.timestamp)) { + return; + } + + _updateIndexes(reserve, reserveCache); + _accrueToTreasury(reserve, reserveCache); + + //solium-disable-next-line + reserve.lastUpdateTimestamp = uint40(block.timestamp); + } + + /** + * @notice Accumulates a predefined amount of asset to the reserve as a fixed, instantaneous income. Used for example + * to accumulate the flashloan fee to the reserve, and spread it between all the suppliers. + * @param reserve The reserve object + * @param totalLiquidity The total liquidity available in the reserve + * @param amount The amount to accumulate + * @return The next liquidity index of the reserve + */ + function cumulateToLiquidityIndex( + DataTypes.ReserveData storage reserve, + uint256 totalLiquidity, + uint256 amount + ) internal returns (uint256) { + //next liquidity index is calculated this way: `((amount / totalLiquidity) + 1) * liquidityIndex` + //division `amount / totalLiquidity` done in ray for precision + uint256 result = (amount.wadToRay().rayDiv(totalLiquidity.wadToRay()) + WadRayMath.RAY).rayMul( + reserve.liquidityIndex + ); + reserve.liquidityIndex = result.toUint128(); + return result; + } + + /** + * @notice Initializes a reserve. + * @param reserve The reserve object + * @param aTokenAddress The address of the overlying atoken contract + * @param variableDebtTokenAddress The address of the overlying variable debt token contract + * @param interestRateStrategyAddress The address of the interest rate strategy contract + */ + function init( + DataTypes.ReserveData storage reserve, + address aTokenAddress, + address variableDebtTokenAddress, + address interestRateStrategyAddress + ) internal { + require(reserve.aTokenAddress == address(0), Errors.RESERVE_ALREADY_INITIALIZED); + + reserve.liquidityIndex = uint128(WadRayMath.RAY); + reserve.variableBorrowIndex = uint128(WadRayMath.RAY); + reserve.aTokenAddress = aTokenAddress; + reserve.variableDebtTokenAddress = variableDebtTokenAddress; + reserve.interestRateStrategyAddress = interestRateStrategyAddress; + } + + /** + * @notice Updates the reserve current variable borrow rate and the current liquidity rate. + * @param reserve The reserve reserve to be updated + * @param reserveCache The caching layer for the reserve data + * @param reserveAddress The address of the reserve to be updated + * @param liquidityAdded The amount of liquidity added to the protocol (supply or repay) in the previous action + * @param liquidityTaken The amount of liquidity taken from the protocol (redeem or borrow) + */ + function updateInterestRatesAndVirtualBalance( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache, + address reserveAddress, + uint256 liquidityAdded, + uint256 liquidityTaken + ) internal { + uint256 totalVariableDebt = reserveCache.nextScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex + ); + + (uint256 nextLiquidityRate, uint256 nextVariableRate) = IReserveInterestRateStrategy( + reserve.interestRateStrategyAddress + ).calculateInterestRates( + DataTypes.CalculateInterestRatesParams({ + unbacked: reserve.unbacked, + liquidityAdded: liquidityAdded, + liquidityTaken: liquidityTaken, + totalDebt: totalVariableDebt, + reserveFactor: reserveCache.reserveFactor, + reserve: reserveAddress, + usingVirtualBalance: reserveCache.reserveConfiguration.getIsVirtualAccActive(), + virtualUnderlyingBalance: reserve.virtualUnderlyingBalance + }) + ); + + reserve.currentLiquidityRate = nextLiquidityRate.toUint128(); + reserve.currentVariableBorrowRate = nextVariableRate.toUint128(); + + // Only affect virtual balance if the reserve uses it + if (reserveCache.reserveConfiguration.getIsVirtualAccActive()) { + if (liquidityAdded > 0) { + reserve.virtualUnderlyingBalance += liquidityAdded.toUint128(); + } + if (liquidityTaken > 0) { + reserve.virtualUnderlyingBalance -= liquidityTaken.toUint128(); + } + } + + emit ReserveDataUpdated( + reserveAddress, + nextLiquidityRate, + 0, + nextVariableRate, + reserveCache.nextLiquidityIndex, + reserveCache.nextVariableBorrowIndex + ); + } + + /** + * @notice Mints part of the repaid interest to the reserve treasury as a function of the reserve factor for the + * specific asset. + * @param reserve The reserve to be updated + * @param reserveCache The caching layer for the reserve data + */ + function _accrueToTreasury( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache + ) internal { + if (reserveCache.reserveFactor == 0) { + return; + } + + //calculate the total variable debt at moment of the last interaction + uint256 prevTotalVariableDebt = reserveCache.currScaledVariableDebt.rayMul( + reserveCache.currVariableBorrowIndex + ); + + //calculate the new total variable debt after accumulation of the interest on the index + uint256 currTotalVariableDebt = reserveCache.currScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex + ); + + //debt accrued is the sum of the current debt minus the sum of the debt at the last update + uint256 totalDebtAccrued = currTotalVariableDebt - prevTotalVariableDebt; + + uint256 amountToMint = totalDebtAccrued.percentMul(reserveCache.reserveFactor); + + if (amountToMint != 0) { + reserve.accruedToTreasury += amountToMint.rayDiv(reserveCache.nextLiquidityIndex).toUint128(); + } + } + + /** + * @notice Updates the reserve indexes and the timestamp of the update. + * @param reserve The reserve reserve to be updated + * @param reserveCache The cache layer holding the cached protocol data + */ + function _updateIndexes( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache + ) internal { + // Only cumulating on the supply side if there is any income being produced + // The case of Reserve Factor 100% is not a problem (currentLiquidityRate == 0), + // as liquidity index should not be updated + if (reserveCache.currLiquidityRate != 0) { + uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest( + reserveCache.currLiquidityRate, + reserveCache.reserveLastUpdateTimestamp + ); + reserveCache.nextLiquidityIndex = cumulatedLiquidityInterest.rayMul( + reserveCache.currLiquidityIndex + ); + reserve.liquidityIndex = reserveCache.nextLiquidityIndex.toUint128(); + } + + // Variable borrow index only gets updated if there is any variable debt. + // reserveCache.currVariableBorrowRate != 0 is not a correct validation, + // because a positive base variable rate can be stored on + // reserveCache.currVariableBorrowRate, but the index should not increase + if (reserveCache.currScaledVariableDebt != 0) { + uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest( + reserveCache.currVariableBorrowRate, + reserveCache.reserveLastUpdateTimestamp + ); + reserveCache.nextVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul( + reserveCache.currVariableBorrowIndex + ); + reserve.variableBorrowIndex = reserveCache.nextVariableBorrowIndex.toUint128(); + } + _updateIndexes_hook(reserve,reserveCache); + } + + /** + * @notice Creates a cache object to avoid repeated storage reads and external contract calls when updating state and + * interest rates. + * @param reserve The reserve object for which the cache will be filled + * @return The cache object + */ + function cache( + DataTypes.ReserveData storage reserve + ) internal view returns (DataTypes.ReserveCache memory) { + DataTypes.ReserveCache memory reserveCache; + + reserveCache.reserveConfiguration = reserve.configuration; + reserveCache.reserveFactor = reserveCache.reserveConfiguration.getReserveFactor(); + reserveCache.currLiquidityIndex = reserveCache.nextLiquidityIndex = reserve.liquidityIndex; + reserveCache.currVariableBorrowIndex = reserveCache.nextVariableBorrowIndex = reserve + .variableBorrowIndex; + reserveCache.currLiquidityRate = reserve.currentLiquidityRate; + reserveCache.currVariableBorrowRate = reserve.currentVariableBorrowRate; + + reserveCache.aTokenAddress = reserve.aTokenAddress; + reserveCache.variableDebtTokenAddress = reserve.variableDebtTokenAddress; + + reserveCache.reserveLastUpdateTimestamp = reserve.lastUpdateTimestamp; + + reserveCache.currScaledVariableDebt = reserveCache.nextScaledVariableDebt = IVariableDebtToken( + reserveCache.variableDebtTokenAddress + ).scaledTotalSupply(); + + return reserveCache; + } + + /** + * @notice Reduces a portion or all of the deficit of a specified reserve by burning the equivalent aToken `amount`. + * The caller of this method MUST always be the Umbrella contract and the Umbrella contract is assumed to never have debt. + * @dev Emits the `DeficitCovered() event`. + * @dev If the coverage admin covers its entire balance, `ReserveUsedAsCollateralDisabled()` is emitted. + * @param reservesData The state of all the reserves + * @param userConfig The user configuration mapping that tracks the supplied/borrowed assets + * @param params The additional parameters needed to execute the eliminateDeficit function + */ + function executeEliminateDeficit( + mapping(address => DataTypes.ReserveData) storage reservesData, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteEliminateDeficitParams memory params + ) external { + require(params.amount != 0, Errors.INVALID_AMOUNT); + + DataTypes.ReserveData storage reserve = reservesData[params.asset]; + uint256 currentDeficit = reserve.deficit; + + require(currentDeficit != 0, Errors.RESERVE_NOT_IN_DEFICIT); + require(!userConfig.isBorrowingAny(), Errors.USER_CANNOT_HAVE_DEBT); + + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + reserve.updateState(reserveCache); + bool isActive = reserveCache.reserveConfiguration.getActive(); + require(isActive, Errors.RESERVE_INACTIVE); + + uint256 balanceWriteOff = params.amount; + + if (params.amount > currentDeficit) { + balanceWriteOff = currentDeficit; + } + + if (reserveCache.reserveConfiguration.getIsVirtualAccActive()) { + IAToken(reserveCache.aTokenAddress).burn( + msg.sender, + reserveCache.aTokenAddress, + balanceWriteOff, + reserveCache.nextLiquidityIndex + ); + // update ir due to updateState + reserve.updateInterestRatesAndVirtualBalance(reserveCache, params.asset, 0, 0); + } else { + // This is a special case to allow mintable assets (ex. GHO), which by definition cannot be supplied + // and thus do not use virtual underlying balances. + // In that case, the procedure is 1) sending the underlying asset to the aToken and + // 2) trigger the handleRepayment() for the aToken to dispose of those assets + IERC20(params.asset).safeTransferFrom( + msg.sender, + reserveCache.aTokenAddress, + balanceWriteOff + ); + IAToken(reserveCache.aTokenAddress).handleRepayment( + msg.sender, + // In the context of GHO it's only relevant that the address has no debt. + // Passing the pool is fitting as it's handeling the repayment on behalf of the protocol. + address(this), + balanceWriteOff + ); + // updating the IR is not needed in this case, as the IR is constant for assets without virtual accounting. + } + + reserve.deficit -= balanceWriteOff.toUint128(); + + emit DeficitCovered(params.asset, msg.sender, balanceWriteOff); + } + + function getNormalizedIncome_hook(uint256 ret_val, address aTokenAddress) internal view {} + function getNormalizedDebt_hook(uint256 ret_val, address aTokenAddress) internal view {} + function _updateIndexes_hook(DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache) internal view {} +} diff --git a/certora/solvency/applyHarness.patch b/certora/solvency/applyHarness.patch new file mode 100644 index 00000000..ac645499 --- /dev/null +++ b/certora/solvency/applyHarness.patch @@ -0,0 +1,302 @@ +diff -ruN .gitignore .gitignore +--- .gitignore 1970-01-01 02:00:00.000000000 +0200 ++++ .gitignore 2024-12-01 11:47:54.000000000 +0200 +@@ -0,0 +1,2 @@ ++* ++!.gitignore +\ No newline at end of file +diff -ruN src/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol src/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol +--- src/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol 2024-12-12 12:17:18.477210656 +0200 ++++ src/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol 2024-12-01 11:47:54.000000000 +0200 +@@ -10,6 +10,8 @@ + /// @dev Wrapper around a call to the ERC20 function `transfer` that reverts + /// also when the token returns `false`. + function safeTransfer(IERC20 token, address to, uint256 value) internal { ++ require(token.transfer(to,value)); ++ /* + bytes4 selector_ = token.transfer.selector; + + // solhint-disable-next-line no-inline-assembly +@@ -24,6 +26,7 @@ + revert(0, returndatasize()) + } + } ++ */ + + require(getLastTransferResult(token), 'GPv2: failed transfer'); + } +@@ -31,6 +34,8 @@ + /// @dev Wrapper around a call to the ERC20 function `transferFrom` that + /// reverts also when the token returns `false`. + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { ++ require(token.transferFrom(from,to,value)); ++ /* + bytes4 selector_ = token.transferFrom.selector; + + // solhint-disable-next-line no-inline-assembly +@@ -48,6 +53,7 @@ + } + + require(getLastTransferResult(token), 'GPv2: failed transferFrom'); ++ */ + } + + /// @dev Verifies that the last return was a successful `transfer*` call. +diff -ruN src/contracts/protocol/libraries/logic/BorrowLogic.sol src/contracts/protocol/libraries/logic/BorrowLogic.sol +--- src/contracts/protocol/libraries/logic/BorrowLogic.sol 2024-12-12 12:17:18.481210672 +0200 ++++ src/contracts/protocol/libraries/logic/BorrowLogic.sol 2024-12-01 11:47:54.000000000 +0200 +@@ -160,7 +160,7 @@ + DataTypes.ReserveData storage reserve = reservesData[params.asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + reserve.updateState(reserveCache); +- ++ repay_hook_1(reserveCache); + uint256 variableDebt = IERC20(reserveCache.variableDebtTokenAddress).balanceOf( + params.onBehalfOf + ); +@@ -183,10 +183,10 @@ + if (params.amount < paybackAmount) { + paybackAmount = params.amount; + } +- ++ repay_hook_2(reserveCache); + reserveCache.nextScaledVariableDebt = IVariableDebtToken(reserveCache.variableDebtTokenAddress) + .burn(params.onBehalfOf, paybackAmount, reserveCache.nextVariableBorrowIndex); +- ++ repay_hook_3(reserveCache, paybackAmount); + reserve.updateInterestRatesAndVirtualBalance( + reserveCache, + params.asset, +@@ -214,6 +214,7 @@ + paybackAmount, + reserveCache.nextLiquidityIndex + ); ++ repay_hook_4(reserveCache); + bool isCollateral = userConfig.isUsingAsCollateral(reserve.id); + if (isCollateral && IAToken(reserveCache.aTokenAddress).scaledBalanceOf(msg.sender) == 0) { + userConfig.setUsingAsCollateral(reserve.id, false); +@@ -232,4 +233,8 @@ + + return paybackAmount; + } ++ function repay_hook_1(DataTypes.ReserveCache memory reserveCache) internal {} ++ function repay_hook_2(DataTypes.ReserveCache memory reserveCache) internal {} ++ function repay_hook_3(DataTypes.ReserveCache memory reserveCache, uint256 paybackAmount) internal {} ++ function repay_hook_4(DataTypes.ReserveCache memory reserveCache) internal {} + } +diff -ruN src/contracts/protocol/libraries/logic/LiquidationLogic.sol src/contracts/protocol/libraries/logic/LiquidationLogic.sol +--- src/contracts/protocol/libraries/logic/LiquidationLogic.sol 2024-12-12 12:17:18.481210672 +0200 ++++ src/contracts/protocol/libraries/logic/LiquidationLogic.sol 2024-12-01 11:47:54.000000000 +0200 +@@ -126,7 +126,9 @@ + DataTypes.ReserveData storage debtReserve = reservesData[params.debtAsset]; + DataTypes.UserConfigurationMap storage userConfig = usersConfig[params.user]; + vars.debtReserveCache = debtReserve.cache(); ++ /*HOOK !!*/ HOOK_liquidation_before_updateState_DBT(); + debtReserve.updateState(vars.debtReserveCache); ++ /*HOOK !!*/ HOOK_liquidation_after_updateState_DBT(); + + ( + vars.totalCollateralInBaseCurrency, +@@ -149,11 +151,14 @@ + ); + + vars.collateralAToken = IAToken(collateralReserve.aTokenAddress); +- vars.userCollateralBalance = vars.collateralAToken.balanceOf(params.user); ++ ++ vars.userCollateralBalance = get_userCollateralBalance(); ++ //vars.userCollateralBalance = vars.collateralAToken.balanceOf(params.user); + vars.userReserveDebt = IERC20(vars.debtReserveCache.variableDebtTokenAddress).balanceOf( + params.user + ); + ++ /*HOOK !!*/ HOOK_liquidation_before_validateLiquidationCall(vars.userReserveDebt); + ValidationLogic.validateLiquidationCall( + userConfig, + collateralReserve, +@@ -165,7 +170,7 @@ + priceOracleSentinel: params.priceOracleSentinel + }) + ); +- ++ /* MUNGED + if ( + params.userEModeCategory != 0 && + EModeConfiguration.isReserveEnabledOnBitmap( +@@ -191,6 +196,14 @@ + vars.userReserveCollateralInBaseCurrency = + (vars.userCollateralBalance * vars.collateralAssetPrice) / + vars.collateralAssetUnit; ++ */ ++ vars.liquidationBonus = get_liquidationBonus(); ++ vars.collateralAssetPrice = get_collateralAssetPrice(); ++ vars.debtAssetPrice = get_debtAssetPrice(); ++ vars.collateralAssetUnit = get_collateralAssetUnit(); ++ vars.debtAssetUnit = get_debtAssetUnit(); ++ vars.userReserveDebtInBaseCurrency = get_userReserveDebtInBaseCurrency(); ++ vars.userReserveCollateralInBaseCurrency = get_userReserveCollateralInBaseCurrency(); + + // by default whole debt in the reserve could be liquidated + uint256 maxLiquidatableDebt = vars.userReserveDebt; +@@ -272,6 +285,7 @@ + + bool hasNoCollateralLeft = vars.totalCollateralInBaseCurrency == + vars.collateralToLiquidateInBaseCurrency; ++ /*HOOK !!*/ HOOK_liquidation_before_burnDebtTokens(hasNoCollateralLeft); + _burnDebtTokens( + vars.debtReserveCache, + debtReserve, +@@ -282,7 +296,8 @@ + vars.actualDebtToLiquidate, + hasNoCollateralLeft + ); +- ++ /*HOOK !!*/ HOOK_liquidation_after_burnDebtTokens(hasNoCollateralLeft, vars.actualDebtToLiquidate, vars.userReserveDebt); ++ + // IsolationModeTotalDebt only discounts `actualDebtToLiquidate`, not the fully burned amount in case of deficit creation. + // This is by design as otherwise debt debt ceiling would render ineffective if a collateral asset faces bad debt events. + // The governance can decide the raise the ceiling to discount manifested deficit. +@@ -294,10 +309,12 @@ + vars.actualDebtToLiquidate + ); + ++ /*HOOK !!*/ HOOK_liquidation_before_burnCollateralATokens(vars.actualCollateralToLiquidate); + if (params.receiveAToken) { + _liquidateATokens(reservesData, reservesList, usersConfig, collateralReserve, params, vars); + } else { + _burnCollateralATokens(collateralReserve, params, vars); ++ /*HOOK !!*/ HOOK_liquidation_after_burnCollateralATokens(vars.actualCollateralToLiquidate); + } + + // Transfer fee to treasury if it is non-zero +@@ -321,9 +338,11 @@ + // burn bad debt if necessary + // Each additional debt asset already adds around ~75k gas to the liquidation. + // To keep the liquidation gas under control, 0 usd collateral positions are not touched, as there is no immediate benefit in burning or transfering to treasury. ++ /*HOOK !!*/ HOOK_liquidation_before_burnBadDebt(); + if (hasNoCollateralLeft && userConfig.isBorrowingAny()) { + _burnBadDebt(reservesData, reservesList, userConfig, params.reservesCount, params.user); + } ++ /*HOOK !!*/ HOOK_liquidation_after_burnBadDebt(); + + // Transfers the debt asset being repaid to the aToken, where the liquidity is kept + IERC20(params.debtAsset).safeTransferFrom( +@@ -363,6 +382,8 @@ + ) internal { + DataTypes.ReserveCache memory collateralReserveCache = collateralReserve.cache(); + collateralReserve.updateState(collateralReserveCache); ++ /*HOOK !!*/ HOOK_burnCollateralATokens_after_updateState(); ++ + collateralReserve.updateInterestRatesAndVirtualBalance( + collateralReserveCache, + params.collateralAsset, +@@ -624,9 +645,9 @@ + DataTypes.ReserveData storage currentReserve = reservesData[reserveAddress]; + DataTypes.ReserveCache memory reserveCache = currentReserve.cache(); + if (!reserveCache.reserveConfiguration.getActive()) continue; +- ++ /*HOOK !!*/ HOOK_burnBadDebt_inside_loop(reserveAddress); + currentReserve.updateState(reserveCache); +- ++ /*HOOK !!*/ HOOK_burnBadDebt_before_burnDebtTokens(reserveAddress, IERC20(reserveCache.variableDebtTokenAddress).balanceOf(user)); + _burnDebtTokens( + reserveCache, + currentReserve, +@@ -637,6 +658,31 @@ + 0, + true + ); ++ /*HOOK !!*/ HOOK_burnBadDebt_after_burnDebtTokens(reserveAddress); + } + } ++ function HOOK_liquidation_before_updateState_DBT() internal {} ++ function HOOK_liquidation_after_updateState_DBT() internal {} ++ function HOOK_burnCollateralATokens_after_updateState() internal {} ++ function HOOK_liquidation_before_validateLiquidationCall(uint256 userTotalDebt) internal {} ++ function HOOK_liquidation_before_burnDebtTokens(bool hasNoCollateralLeft) internal {} ++ function HOOK_liquidation_after_burnDebtTokens(bool hasNoCollateralLeft, uint256 actualDebtToLiquidate, uint256 userReserveDebt) internal {} ++ function HOOK_liquidation_before_burnCollateralATokens(uint256 actualCollateralToLiquidate) internal {} ++ function HOOK_liquidation_after_burnCollateralATokens(uint256 actualCollateralToLiquidate) internal {} ++ function HOOK_liquidation_before_burnBadDebt() internal {} ++ function HOOK_burnBadDebt_inside_loop(address reserveAddress) internal {} ++ function HOOK_burnBadDebt_before_burnDebtTokens(address reserveAddress, uint256 amount) internal {} ++ function HOOK_burnBadDebt_after_burnDebtTokens(address reserveAddress) internal {} ++ function HOOK_liquidation_after_burnBadDebt() internal {} ++ ++ function get_userCollateralBalance() internal returns(uint256) {return 0;} ++ ++ function get_liquidationBonus() internal returns(uint256) {return 0;} ++ function get_collateralAssetPrice() internal returns(uint256) {return 0;} ++ function get_debtAssetPrice() internal returns(uint256) {return 0;} ++ function get_collateralAssetUnit() internal returns(uint256) {return 0;} ++ function get_debtAssetUnit() internal returns(uint256) {return 0;} ++ function get_userReserveDebtInBaseCurrency() internal returns(uint256) {return 0;} ++ function get_userReserveCollateralInBaseCurrency() internal returns(uint256) {return 0;} ++ + } +diff -ruN src/contracts/protocol/libraries/logic/ReserveLogic.sol src/contracts/protocol/libraries/logic/ReserveLogic.sol +--- src/contracts/protocol/libraries/logic/ReserveLogic.sol 2024-12-12 12:17:18.481210672 +0200 ++++ src/contracts/protocol/libraries/logic/ReserveLogic.sol 2024-12-01 11:47:54.000000000 +0200 +@@ -53,17 +53,19 @@ + DataTypes.ReserveData storage reserve + ) internal view returns (uint256) { + uint40 timestamp = reserve.lastUpdateTimestamp; +- ++ uint256 ret_val; + //solium-disable-next-line + if (timestamp == block.timestamp) { + //if the index was updated in the same block, no need to perform any calculation +- return reserve.liquidityIndex; ++ ret_val = reserve.liquidityIndex; + } else { +- return ++ ret_val = + MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp).rayMul( + reserve.liquidityIndex + ); + } ++ getNormalizedIncome_hook(ret_val,reserve.aTokenAddress); ++ return ret_val; + } + + /** +@@ -77,17 +79,19 @@ + DataTypes.ReserveData storage reserve + ) internal view returns (uint256) { + uint40 timestamp = reserve.lastUpdateTimestamp; +- ++ uint256 ret_val; + //solium-disable-next-line + if (timestamp == block.timestamp) { + //if the index was updated in the same block, no need to perform any calculation +- return reserve.variableBorrowIndex; ++ ret_val = reserve.variableBorrowIndex; + } else { +- return ++ ret_val = + MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp).rayMul( + reserve.variableBorrowIndex + ); + } ++ getNormalizedDebt_hook(ret_val,reserve.aTokenAddress); ++ return ret_val; + } + + /** +@@ -285,6 +289,7 @@ + ); + reserve.variableBorrowIndex = reserveCache.nextVariableBorrowIndex.toUint128(); + } ++ _updateIndexes_hook(reserve,reserveCache); + } + + /** +@@ -401,4 +406,9 @@ + + emit DeficitCovered(params.asset, msg.sender, balanceWriteOff); + } ++ ++ function getNormalizedIncome_hook(uint256 ret_val, address aTokenAddress) internal view {} ++ function getNormalizedDebt_hook(uint256 ret_val, address aTokenAddress) internal view {} ++ function _updateIndexes_hook(DataTypes.ReserveData storage reserve, ++ DataTypes.ReserveCache memory reserveCache) internal view {} + } diff --git a/certora/solvency/confs/ATokenEqCheck.conf b/certora/solvency/confs/ATokenEqCheck.conf new file mode 100644 index 00000000..a175b36a --- /dev/null +++ b/certora/solvency/confs/ATokenEqCheck.conf @@ -0,0 +1,22 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "src/core/instances/ATokenInstance.sol", + "certora/harnesses/Aave/PoolInstanceForAToken.sol:PoolInstance" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "aToken check", + "optimistic_loop": true, // for name, symbol + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": true, // required for autofinders in ATokenInstance to compile + "verify": "ATokenInstance:certora/specs/Aave/aTokenCheck.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/AaveMathCheck.conf b/certora/solvency/confs/AaveMathCheck.conf new file mode 100644 index 00000000..aaf79721 --- /dev/null +++ b/certora/solvency/confs/AaveMathCheck.conf @@ -0,0 +1,18 @@ +{ + "assert_autofinder_success": true, + // including dummy file + "files": [ + "certora/harnesses/Utilities.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "generic Aave math checks", + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "verify": "Utilities:certora/specs/Aave/AaveMathCheck.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/DefaultReserveInterestRateStrategyV2_check.conf b/certora/solvency/confs/DefaultReserveInterestRateStrategyV2_check.conf new file mode 100644 index 00000000..763dc263 --- /dev/null +++ b/certora/solvency/confs/DefaultReserveInterestRateStrategyV2_check.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "certora/munged/src/core/contracts/protocol/pool/DefaultReserveInterestRateStrategyV2.sol" + ], + "process": "emv", + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "disable_solc_optimizers": ["cse","peephole"], + // "optimistic_loop": true, + /*"prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ],*/ + "smt_timeout": "3000", + "prover_version": "master", + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": false, + "verify": "DefaultReserveInterestRateStrategyV2:certora/specs/Aave/ReserveInterestRateStrategyCheck.spec", + "msg": "Aave V3 DefaultReserveInterestRateStrategyV2 - check summaries", +} \ No newline at end of file diff --git a/certora/solvency/confs/PoolInstance.conf b/certora/solvency/confs/PoolInstance.conf new file mode 100644 index 00000000..fb7c10a6 --- /dev/null +++ b/certora/solvency/confs/PoolInstance.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "src/core/instances/PoolInstance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "Aave V3 Pool", + "optimistic_loop": true, + "process": "emv", + /*"prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ],*/ + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": false, + "verify": "PoolInstance:certora/specs/Aave/Pool.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/VariableDebtTokenEqCheck.conf b/certora/solvency/confs/VariableDebtTokenEqCheck.conf new file mode 100644 index 00000000..3b11cae4 --- /dev/null +++ b/certora/solvency/confs/VariableDebtTokenEqCheck.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "src/core/instances/VariableDebtTokenInstance.sol", + "certora/harnesses/Aave/PoolInstanceForAToken.sol:PoolInstance" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "VariableDebtToken check", + "optimistic_loop": true, // for name, symbol + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "verify": "VariableDebtTokenInstance:certora/specs/Aave/VariableDebtTokenCheck.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/indexes_rates.conf b/certora/solvency/confs/indexes_rates.conf new file mode 100644 index 00000000..95a21fc0 --- /dev/null +++ b/certora/solvency/confs/indexes_rates.conf @@ -0,0 +1,29 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "certora/munged/src/core/instances/PoolInstance.sol" + ], + "disable_solc_optimizers": [ + "cse", + "peephole" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "Indexes and Rates", + "optimistic_loop": true, + "process": "emv", +// "prover_args": ["-numOfUnsatCores 1"], + "prover_version": "master", + "server": "prover", + "smt_timeout": "3000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", +// "coverage_info": "basic", + "verify": "PoolInstance:certora/specs/Aave/indexes_rates.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/irrelevant/PoolInstance_builtin_assertions.conf b/certora/solvency/confs/irrelevant/PoolInstance_builtin_assertions.conf new file mode 100644 index 00000000..3e588c77 --- /dev/null +++ b/certora/solvency/confs/irrelevant/PoolInstance_builtin_assertions.conf @@ -0,0 +1,20 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "src/core/instances/PoolInstance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": false, + "verify": "PoolInstance:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/irrelevant/PoolInstance_sanity.conf b/certora/solvency/confs/irrelevant/PoolInstance_sanity.conf new file mode 100644 index 00000000..86df8b0f --- /dev/null +++ b/certora/solvency/confs/irrelevant/PoolInstance_sanity.conf @@ -0,0 +1,18 @@ +{ + "assert_autofinder_success": true, + "files": [ + "src/core/instances/PoolInstance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": false, + "verify": "PoolInstance:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/irrelevant/PoolInstance_sanity_with_erc20cvl.conf b/certora/solvency/confs/irrelevant/PoolInstance_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..53e9a954 --- /dev/null +++ b/certora/solvency/confs/irrelevant/PoolInstance_sanity_with_erc20cvl.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "src/core/instances/PoolInstance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": false, + "verify": "PoolInstance:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/irrelevant/PoolInstance_sanity_with_erc20dispatched.conf b/certora/solvency/confs/irrelevant/PoolInstance_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..6063317c --- /dev/null +++ b/certora/solvency/confs/irrelevant/PoolInstance_sanity_with_erc20dispatched.conf @@ -0,0 +1,22 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "src/core/instances/PoolInstance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.19", + "solc_optimize": "200", + "solc_via_ir": false, + "verify": "PoolInstance:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/borrow.conf b/certora/solvency/confs/solvency/borrow.conf new file mode 100644 index 00000000..65690e04 --- /dev/null +++ b/certora/solvency/confs/solvency/borrow.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - BORROW", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/borrow.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/cumulateToLiquidityIndexCVL-check.conf b/certora/solvency/confs/solvency/cumulateToLiquidityIndexCVL-check.conf new file mode 100644 index 00000000..e71f8666 --- /dev/null +++ b/certora/solvency/confs/solvency/cumulateToLiquidityIndexCVL-check.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "cumulateToLiquidityIndex check", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/cumulateToLiquidityIndexCVL-check.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/flashloan.conf b/certora/solvency/confs/solvency/flashloan.conf new file mode 100644 index 00000000..4e5ac561 --- /dev/null +++ b/certora/solvency/confs/solvency/flashloan.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY-FLASHLOAN RULE", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/flashloan.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/liquidationCall-COLasset-lemma.conf b/certora/solvency/confs/solvency/liquidationCall-COLasset-lemma.conf new file mode 100644 index 00000000..288c879a --- /dev/null +++ b/certora/solvency/confs/solvency/liquidationCall-COLasset-lemma.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - COLasset lemma", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, +// "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/liquidationCall/COLasset-lemma.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/liquidationCall-COLasset-main.conf b/certora/solvency/confs/solvency/liquidationCall-COLasset-main.conf new file mode 100644 index 00000000..be83fd87 --- /dev/null +++ b/certora/solvency/confs/solvency/liquidationCall-COLasset-main.conf @@ -0,0 +1,22 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - COLasset main", + "optimistic_loop": true, + "process": "emv", +// "prover_args": [" -split false" ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, +// "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/liquidationCall/COLasset-main.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/liquidationCall-COLasset-totSUP0-lemma.conf b/certora/solvency/confs/solvency/liquidationCall-COLasset-totSUP0-lemma.conf new file mode 100644 index 00000000..ecf9d38b --- /dev/null +++ b/certora/solvency/confs/solvency/liquidationCall-COLasset-totSUP0-lemma.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - COLasset totSUP==0 lemma", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/liquidationCall/COLasset-totSUP0-lemma.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/liquidationCall-COLasset-totSUP0.conf b/certora/solvency/confs/solvency/liquidationCall-COLasset-totSUP0.conf new file mode 100644 index 00000000..59faee25 --- /dev/null +++ b/certora/solvency/confs/solvency/liquidationCall-COLasset-totSUP0.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - COLasset case totDebt==0", + "optimistic_loop": true, + "process": "emv", +// "prover_args": [ " -split false" ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, + "verify": "PoolInstanceHarness:certora/solvency/specs/liquidationCall/COLasset-totSUP0.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/liquidationCall-DBTasset-lemma.conf b/certora/solvency/confs/solvency/liquidationCall-DBTasset-lemma.conf new file mode 100644 index 00000000..d61c22cc --- /dev/null +++ b/certora/solvency/confs/solvency/liquidationCall-DBTasset-lemma.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - DBTasset lemma", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, +// "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/liquidationCall/DBTasset-lemma.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/liquidationCall-DBTasset-main.conf b/certora/solvency/confs/solvency/liquidationCall-DBTasset-main.conf new file mode 100644 index 00000000..01a760c2 --- /dev/null +++ b/certora/solvency/confs/solvency/liquidationCall-DBTasset-main.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - DBTasset main", + "optimistic_loop": true, + "process": "emv", +// "prover_args": [ " -split false" ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, +// "precise_bitwise_ops": true, +// "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/liquidationCall/DBTasset-main.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/liquidationCall-DBTasset-totSUP0.conf b/certora/solvency/confs/solvency/liquidationCall-DBTasset-totSUP0.conf new file mode 100644 index 00000000..94a84f54 --- /dev/null +++ b/certora/solvency/confs/solvency/liquidationCall-DBTasset-totSUP0.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - DBTasset must REVERT if totDebt==0", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, + "verify": "PoolInstanceHarness:certora/solvency/specs/liquidationCall/DBTasset-totSUP0.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/liquidationCall-burnBadDebt-assetINloop.conf b/certora/solvency/confs/solvency/liquidationCall-burnBadDebt-assetINloop.conf new file mode 100644 index 00000000..f7a40efe --- /dev/null +++ b/certora/solvency/confs/solvency/liquidationCall-burnBadDebt-assetINloop.conf @@ -0,0 +1,22 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - _burnBadDebt - asset==loopAsset", + "optimistic_loop": true, + "process": "emv", +// "prover_args": [" -split false" ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", +// "rule_sanity": "basic", + "multi_assert_check" : true, + "verify": "PoolInstanceHarness:certora/solvency/specs/liquidationCall/burnBadDebt-assetINloop.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/liquidationCall-burnBadDebt-assetNOTINloop.conf b/certora/solvency/confs/solvency/liquidationCall-burnBadDebt-assetNOTINloop.conf new file mode 100644 index 00000000..4ccfcec3 --- /dev/null +++ b/certora/solvency/confs/solvency/liquidationCall-burnBadDebt-assetNOTINloop.conf @@ -0,0 +1,22 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - _burnBadDebt - asset NEQ loopAsset", + "optimistic_loop": true, + "process": "emv", +// "prover_args": [" -split false" ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", +// "rule_sanity": "basic", + "multi_assert_check" : true, + "verify": "PoolInstanceHarness:certora/solvency/specs/liquidationCall/burnBadDebt-assetNOTINloop.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/liquidationCall-sanity.conf b/certora/solvency/confs/solvency/liquidationCall-sanity.conf new file mode 100644 index 00000000..39dc4e5c --- /dev/null +++ b/certora/solvency/confs/solvency/liquidationCall-sanity.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - LIQUIDATION-CALL - SANITY", + "optimistic_loop": true, + "process": "emv", +// "prover_args": [ " -split false" ], + "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "multi_assert_check" : true, +// "precise_bitwise_ops": true, +// "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/liquidationCall/sanity.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/repay-NONindexSUMM.conf b/certora/solvency/confs/solvency/repay-NONindexSUMM.conf new file mode 100644 index 00000000..46142543 --- /dev/null +++ b/certora/solvency/confs/solvency/repay-NONindexSUMM.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - REPAY - NON INDEX SUMMARIZATION", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/repay/repay-NONindexSUMM.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/repay-indexSUMM.conf b/certora/solvency/confs/solvency/repay-indexSUMM.conf new file mode 100644 index 00000000..581aabc0 --- /dev/null +++ b/certora/solvency/confs/solvency/repay-indexSUMM.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - REPAY - INDEX SUMMARIZATION", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/repay/repay-indexSUMM.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/repayWithATokens-HOOKS.conf b/certora/solvency/confs/solvency/repayWithATokens-HOOKS.conf new file mode 100644 index 00000000..9a0e7ba4 --- /dev/null +++ b/certora/solvency/confs/solvency/repayWithATokens-HOOKS.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/ERC20Like/DummyWeth.sol", + "certora/solvency/harnesses/Utilities.sol", + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - REPAY WITH ATOKENS - HOOKS", + "optimistic_loop": true, + "process": "emv", + "prover_args": ["-prettifyCEX false", +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/repayWithATokens/repayWithATokens-HOOKS.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/repayWithATokens-noHOOKS.conf b/certora/solvency/confs/solvency/repayWithATokens-noHOOKS.conf new file mode 100644 index 00000000..31989a01 --- /dev/null +++ b/certora/solvency/confs/solvency/repayWithATokens-noHOOKS.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - REPAY-WITH-ATOKENS - no HOOKS", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/repayWithATokens/repayWithATokens-noHOOKS.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/supply.conf b/certora/solvency/confs/solvency/supply.conf new file mode 100644 index 00000000..65acb6ab --- /dev/null +++ b/certora/solvency/confs/solvency/supply.conf @@ -0,0 +1,20 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": ["certora/solvency/harnesses/Aave/PoolInstanceHarness.sol"], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - SUPPLY", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/supply.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/time_passing.conf b/certora/solvency/confs/solvency/time_passing.conf new file mode 100644 index 00000000..35742cef --- /dev/null +++ b/certora/solvency/confs/solvency/time_passing.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/ERC20Like/DummyWeth.sol", + "certora/solvency/harnesses/Utilities.sol", + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY : TIME PASSING", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/time_passing.spec" +} \ No newline at end of file diff --git a/certora/solvency/confs/solvency/withdraw.conf b/certora/solvency/confs/solvency/withdraw.conf new file mode 100644 index 00000000..dbf43629 --- /dev/null +++ b/certora/solvency/confs/solvency/withdraw.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "build_cache": true, + "cache": "aave_inv", + "files": [ + "certora/solvency/harnesses/Aave/PoolInstanceHarness.sol" + ], + "disable_solc_optimizers": ["cse","peephole"], + "java_args": [" -ea -Dlevel.setup.helpers=info"], + "msg": "SOLVENCY - WITHDRAW", + "optimistic_loop": true, + "process": "emv", + "prover_args": [ +// " -split false" + ], +// "prover_version": "master", + "server": "prover", + "smt_timeout": "6000", + "solc": "solc8.19", + "solc_optimize": "200", + "rule_sanity": "basic", + "verify": "PoolInstanceHarness:certora/solvency/specs/withdraw.spec" +} \ No newline at end of file diff --git a/certora/solvency/harnesses/Aave/DummyContract.sol b/certora/solvency/harnesses/Aave/DummyContract.sol new file mode 100644 index 00000000..bc1dc298 --- /dev/null +++ b/certora/solvency/harnesses/Aave/DummyContract.sol @@ -0,0 +1,8 @@ + +contract DummyContract { + function havoc_all_dummy() external { + // havoc_all_dummy_internal(); + } + + //function havoc_all_dummy_internal() internal {} +} diff --git a/certora/solvency/harnesses/Aave/PoolInstanceForAToken.sol b/certora/solvency/harnesses/Aave/PoolInstanceForAToken.sol new file mode 100644 index 00000000..1f5a4730 --- /dev/null +++ b/certora/solvency/harnesses/Aave/PoolInstanceForAToken.sol @@ -0,0 +1,34 @@ +import "../Utilities.sol"; + +contract PoolInstance is Utilities { + uint h; + + function getReserveNormalizedIncomeInt( + address asset + ) internal returns (uint256) { + // should be summarized, if not, h will be nondet and other 'bad' things + this.havocAll(); + return h; + } + + function getReserveNormalizedIncome( + address asset + ) external returns (uint256) { + return getReserveNormalizedIncomeInt(asset); + } + + + function getReserveNormalizedVariableDebtInt( + address asset + ) internal returns (uint256) { + // should be summarized, if not, h will be nondet and other 'bad' things + this.havocAll(); + return h; + } + + function getReserveNormalizedVariableDebt( + address asset + ) external returns (uint256) { + return getReserveNormalizedVariableDebtInt(asset); + } +} \ No newline at end of file diff --git a/certora/solvency/harnesses/Aave/PoolInstanceHarness.sol b/certora/solvency/harnesses/Aave/PoolInstanceHarness.sol new file mode 100644 index 00000000..38a27c09 --- /dev/null +++ b/certora/solvency/harnesses/Aave/PoolInstanceHarness.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Pool} from '../../munged/src/contracts/protocol/pool/Pool.sol'; +import {IPoolAddressesProvider} from '../../munged/src/contracts/interfaces/IPoolAddressesProvider.sol'; +import {PoolInstance} from '../../munged/src/contracts/instances/PoolInstance.sol'; +import {DataTypes} from '../../munged/src/contracts/protocol/libraries/types/DataTypes.sol'; +import {ReserveLogic} from '../../munged/src/contracts/protocol/libraries/logic/ReserveLogic.sol'; +import {WadRayMath} from '../../munged/src/contracts/protocol/libraries/math/WadRayMath.sol'; +import {LiquidationLogic} from '../../munged/src/contracts/protocol/libraries/logic/LiquidationLogic.sol'; + +import {DummyContract} from "./DummyContract.sol"; + + +contract PoolInstanceHarness is PoolInstance { + DummyContract DUMMY; + + constructor(IPoolAddressesProvider provider) PoolInstance(provider) {} + + function cumulateToLiquidityIndex(address asset, + uint256 totalLiquidity, + uint256 amount + ) external returns (uint256) { + return ReserveLogic.cumulateToLiquidityIndex(_reserves[asset], totalLiquidity, amount); + } + + function getNormalizedIncome(address asset) external returns (uint256) { + return ReserveLogic.getNormalizedIncome(_reserves[asset]); + } + + function getNormalizedDebt(address asset) external returns (uint256) { + return ReserveLogic.getNormalizedDebt(_reserves[asset]); + } + + function havoc_all() public { + DUMMY.havoc_all_dummy(); + } + + function rayMul(uint256 a, uint256 b) external returns (uint256) { + return WadRayMath.rayMul(a,b); + } + + function rayDiv(uint256 a, uint256 b) external returns (uint256) { + return WadRayMath.rayDiv(a,b); + } + + function getReserveDataExtended(address asset) + external view returns (DataTypes.ReserveData memory) { + return _reserves[asset]; + } + + function _burnBadDebt_WRP(address user) external { + LiquidationLogic._burnBadDebt(_reserves, _reservesList, _usersConfig[user], _reservesCount, user); + } +} diff --git a/certora/solvency/harnesses/ERC20Like/DummyERC20A.sol b/certora/solvency/harnesses/ERC20Like/DummyERC20A.sol new file mode 100644 index 00000000..de4d8a41 --- /dev/null +++ b/certora/solvency/harnesses/ERC20Like/DummyERC20A.sol @@ -0,0 +1,55 @@ +// Represents a symbolic/dummy ERC20 token + +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +contract DummyERC20A { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + + return true; + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + + return true; + } +} diff --git a/certora/solvency/harnesses/ERC20Like/DummyERC20B.sol b/certora/solvency/harnesses/ERC20Like/DummyERC20B.sol new file mode 100644 index 00000000..2dc4ffdc --- /dev/null +++ b/certora/solvency/harnesses/ERC20Like/DummyERC20B.sol @@ -0,0 +1,55 @@ +// Represents a symbolic/dummy ERC20 token + +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +contract DummyERC20B { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + + return true; + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + + return true; + } +} diff --git a/certora/solvency/harnesses/ERC20Like/DummyWeth.sol b/certora/solvency/harnesses/ERC20Like/DummyWeth.sol new file mode 100644 index 00000000..426378d6 --- /dev/null +++ b/certora/solvency/harnesses/ERC20Like/DummyWeth.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity >=0.8.0; + +/** + * Dummy Weth token. + */ +contract DummyWeth { + uint256 t; + + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + return true; + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + return true; + } + + // WETH + function deposit() external payable { + b[msg.sender] += msg.value; + } + + function withdraw(uint256 amt) external { + b[msg.sender] -= amt; + payable(msg.sender).transfer(amt); // use optimistic_fallback here + } +} diff --git a/certora/solvency/harnesses/Utilities.sol b/certora/solvency/harnesses/Utilities.sol new file mode 100644 index 00000000..81d264cc --- /dev/null +++ b/certora/solvency/harnesses/Utilities.sol @@ -0,0 +1,22 @@ + +import {MathUtils} from '../munged/src/contracts/protocol/libraries/math/MathUtils.sol'; +import {FlashLoanLogic} from '../munged/src/contracts/protocol/libraries/logic/FlashLoanLogic.sol'; +import {DataTypes} from '../munged/src/contracts/protocol/libraries/types/DataTypes.sol'; + + +contract Utilities { + function havocAll() external { + (bool success, ) = address(0xdeadbeef).call(abi.encodeWithSelector(0x12345678)); + require(success); + } + + function justRevert() external { + revert(); + } + + function nop() external {} + + function SECONDS_PER_YEAR() external view returns (uint256) { + return MathUtils.SECONDS_PER_YEAR; + } +} diff --git a/certora/solvency/munged/.gitignore b/certora/solvency/munged/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/certora/solvency/munged/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/certora/solvency/scripts/run-all.sh b/certora/solvency/scripts/run-all.sh new file mode 100644 index 00000000..2a49a159 --- /dev/null +++ b/certora/solvency/scripts/run-all.sh @@ -0,0 +1,87 @@ +#CMN="--compilation_steps_only" + + + +#echo "******** Running: 1 ***************" +#certoraRun $CMN certora/solvency/confs/solvency/time_passing.conf --prover_version master \ +# --msg "1: time_passing.conf" + +echo "******** Running: 2 ***************" +certoraRun $CMN certora/solvency/confs/solvency/borrow.conf --prover_version master \ + --msg "2: borrow.conf" + +echo "******** Running: 3 ***************" +certoraRun $CMN certora/solvency/confs/solvency/supply.conf --prover_version master \ + --msg "3: supply.conf" + +echo "******** Running: 4 ***************" +certoraRun $CMN certora/solvency/confs/solvency/withdraw.conf --prover_version master \ + --msg "4: withdraw.conf" + +echo "******** Running: 5a ***************" +certoraRun $CMN certora/solvency/confs/solvency/repay-indexSUMM.conf --prover_version master \ + --msg "5a: repay-indexSUMM.conf" + +echo "******** Running: 5b ***************" +certoraRun $CMN certora/solvency/confs/solvency/repay-NONindexSUMM.conf --prover_version master \ + --msg "5b: repay-NONindexSUMM.conf" + +echo "******** Running: 6a ***************" +certoraRun $CMN certora/solvency/confs/solvency/repayWithATokens-HOOKS.conf --prover_version master \ + --multi_assert_check \ + --msg "6a: repayWithATokens-HOOKS.conf" + +echo "******** Running: 6b ***************" +certoraRun $CMN certora/solvency/confs/solvency/repayWithATokens-noHOOKS.conf --prover_version master \ + --msg "6b: repayWithATokens-noHOOKS.conf" + +echo "******** Running: 7 ***************" +certoraRun $CMN certora/solvency/confs/solvency/flashloan.conf --rule solvency__flashLoanSimple --prover_version master \ + --multi_assert_check \ + --msg "7: flashloan.conf" + +echo "******** Running: 8 ***************" +certoraRun $CMN certora/solvency/confs/solvency/cumulateToLiquidityIndexCVL-check.conf --rule check_cumulateToLiquidityIndexCVL --prover_version master \ + --msg "8: cumulateToLiquidityIndexCVL-check.conf :: check_cumulateToLiquidityIndexCVL" + + +echo "******** Running: 9a ***************" +certoraRun $CMN certora/solvency/confs/solvency/liquidationCall-DBTasset-main.conf --prover_version master \ + --msg "9a: liquidationCall-DBTasset-main.conf" + +echo "******** Running: 9b ***************" +certoraRun $CMN certora/solvency/confs/solvency/liquidationCall-DBTasset-lemma.conf --prover_version master \ + --msg "9b: liquidationCall-DBTasset-lemma.conf" + +echo "******** Running: 9c ***************" +certoraRun $CMN certora/solvency/confs/solvency/liquidationCall-DBTasset-totSUP0.conf --prover_version master \ + --msg "9c: liquidationCall-DBTasset-totSUP0.conf" + +echo "******** Running: 9d ***************" +certoraRun $CMN certora/solvency/confs/solvency/liquidationCall-COLasset-main.conf --prover_version master \ + --msg "9d: liquidationCall-COLasset-main.conf" + +echo "******** Running: 9e ***************" +certoraRun $CMN certora/solvency/confs/solvency/liquidationCall-COLasset-lemma.conf --prover_version master \ + --msg "9e: liquidationCall-COLasset-lemma.conf" + +echo "******** Running: 9f ***************" +certoraRun $CMN certora/solvency/confs/solvency/liquidationCall-COLasset-totSUP0.conf --prover_version master \ + --msg "9f: liquidationCall-COLasset-totSUP0.conf" + +echo "******** Running: 9g ***************" +certoraRun $CMN certora/solvency/confs/solvency/liquidationCall-COLasset-totSUP0-lemma.conf --prover_version master \ + --msg "9g: liquidationCall-COLasset-totSUP0-lemma.conf" + +echo "******** Running: 9h ***************" +certoraRun $CMN certora/solvency/confs/solvency/liquidationCall-burnBadDebt-assetINloop.conf --prover_version master \ + --msg "9h: liquidationCall-burnBadDebt-assetINloop.conf" + +echo "******** Running: 9i ***************" +certoraRun $CMN certora/solvency/confs/solvency/liquidationCall-burnBadDebt-assetNOTINloop.conf --prover_version master \ + --msg "9i: liquidationCall-burnBadDebt-assetNOTINloop.conf" + + + + + diff --git a/certora/solvency/specs/.ERC1967/erc1967.spec b/certora/solvency/specs/.ERC1967/erc1967.spec new file mode 100644 index 00000000..8c96499c --- /dev/null +++ b/certora/solvency/specs/.ERC1967/erc1967.spec @@ -0,0 +1,7 @@ +methods { + // avoids linking messages upon upgradeToAndCall + function _._upgradeToAndCall(address,bytes memory,bool) internal => NONDET; + function _._upgradeToAndCallUUPS(address,bytes memory,bool) internal => NONDET; + // view function + function _.proxiableUUID() external => NONDET; // expect bytes32 +} \ No newline at end of file diff --git a/certora/solvency/specs/.ERC721/erc721.spec b/certora/solvency/specs/.ERC721/erc721.spec new file mode 100644 index 00000000..474b69f3 --- /dev/null +++ b/certora/solvency/specs/.ERC721/erc721.spec @@ -0,0 +1,9 @@ +methods { + // likely unsound, but assumes no callback + function _.onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes data + ) external => NONDET; /* expects bytes4 */ +} \ No newline at end of file diff --git a/certora/solvency/specs/.PriceAggregators/chainlink.spec b/certora/solvency/specs/.PriceAggregators/chainlink.spec new file mode 100644 index 00000000..889e39e6 --- /dev/null +++ b/certora/solvency/specs/.PriceAggregators/chainlink.spec @@ -0,0 +1,4 @@ +methods { + function _.getRoundData(uint80) external => NONDET; + function _.latestRoundData() external => NONDET; +} \ No newline at end of file diff --git a/certora/solvency/specs/.PriceAggregators/tellor.spec b/certora/solvency/specs/.PriceAggregators/tellor.spec new file mode 100644 index 00000000..03f90c77 --- /dev/null +++ b/certora/solvency/specs/.PriceAggregators/tellor.spec @@ -0,0 +1,3 @@ +methods { + function _.getTellorCurrentValue(uint256) external => NONDET; +} \ No newline at end of file diff --git a/certora/solvency/specs/.optimizations.spec b/certora/solvency/specs/.optimizations.spec new file mode 100644 index 00000000..8246442e --- /dev/null +++ b/certora/solvency/specs/.optimizations.spec @@ -0,0 +1,159 @@ + + +// optimizing summaries +methods { + function utils.SECONDS_PER_YEAR() external returns (uint256) envfree; + + // function ReserveLogic.cumulateToLiquidityIndex( + // DataTypes.ReserveData storage reserve, + // uint256 totalLiquidity, + // uint256 amount + //) internal returns (uint256) => cumulateToLiquidityIndexCVL(); + + function ValidationLogic.validateLiquidationCall( + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveData storage collateralReserve, + DataTypes.ReserveData storage debtReserve, + DataTypes.ValidateLiquidationCallParams memory params + ) internal => NONDET; + + /* function ValidationLogic.validateRepay( + DataTypes.ReserveCache memory reserveCache, + uint256 amountSent, + DataTypes.InterestRateMode interestRateMode, + address onBehalfOf, + uint256 stableDebt, + uint256 variableDebt + ) internal => NONDET; + */ + function ValidationLogic.validateWithdraw( + DataTypes.ReserveCache memory reserveCache, + uint256 amount, + uint256 userBalance + ) internal => NONDET; + + function ValidationLogic.validateFlashloanSimple( + DataTypes.ReserveData storage reserve, + uint256 amount + ) internal => NONDET; + + function ValidationLogic.validateAutomaticUseAsCollateral( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveConfigurationMap memory reserveConfig, + address aTokenAddress + ) internal returns (bool) => NONDET; + + function ReserveLogic._accrueToTreasury( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache + ) internal => _accrueToTreasuryCVL(); + + function MathUtils.calculateCompoundedInterest(uint256 rate,uint40 lastUpdateTimestamp, uint256 currentTimestamp) + internal returns (uint256)=> compoundedInterestCVL(rate, lastUpdateTimestamp, currentTimestamp); + + function MathUtils.calculateLinearInterest(uint256 rate, uint40 lastUpdateTimestamp) + internal returns (uint256) with (env e) => f_linearInterestCVL(e,rate,lastUpdateTimestamp); + + + function GenericLogic._getUserDebtInBaseCurrency( + address, + DataTypes.ReserveData storage, + uint256, + uint256 + ) internal returns (uint256) => NONDET /* difficulty 106 */; + + function GenericLogic.calculateUserAccountData( + mapping (address => DataTypes.ReserveData) storage, + mapping (uint256 => address) storage, + mapping (uint8 => DataTypes.EModeCategory) storage, + DataTypes.CalculateUserAccountDataParams memory + ) internal returns (uint256,uint256,uint256,uint256,uint256,bool) => NONDET /* difficulty 214 */; + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveData storage, + DataTypes.ReserveCache memory, + address, + address, + uint256, + uint256, + uint256, + address // IPriceOracleGetter + ) internal returns (uint256,uint256,uint256) => NONDET /* difficulty 90 */; + + function ValidationLogic.validateSupply( + DataTypes.ReserveCache memory, + DataTypes.ReserveData storage, + uint256, + address + ) internal => NONDET /* difficulty 52 */; + + function ValidationLogic.validateBorrow( + mapping (address => DataTypes.ReserveData) storage reservesData, + mapping (uint256 => address) storage reservesList, + mapping (uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.ValidateBorrowParams memory params + ) internal => NONDET; + /* difficulty 331 ; */ + + function ValidationLogic.validateHealthFactor( + mapping (address => DataTypes.ReserveData) storage, + mapping (uint256 => address) storage, + mapping (uint8 => DataTypes.EModeCategory) storage, + DataTypes.UserConfigurationMap memory, + address, + uint8, + uint256, + address + ) internal returns (uint256,bool) => NONDET /* difficulty 214 */; + + function ValidationLogic.validateHFAndLtv( + mapping (address => DataTypes.ReserveData) storage, + mapping (uint256 => address) storage, + mapping (uint8 => DataTypes.EModeCategory) storage, + DataTypes.UserConfigurationMap memory, + address, + address, + uint256, + address, + uint8 + ) internal => NONDET /* difficulty 216 */; + + + // xxxx + // monotonically increase stuff in the reserve + // updates lastUpdateTimestamp + // function ReserveLogic.updateState( + // DataTypes.ReserveData storage reserve, + // DataTypes.ReserveCache memory reserveCache + //) internal => NONDET; + +} // END METHODS BLOCK + + + +ghost linearInterestCVL(mathint,mathint,mathint) returns uint { + axiom forall mathint rate. forall mathint lastUpdateTimestamp. forall mathint currentTimestamp. + to_mathint(linearInterestCVL(rate, lastUpdateTimestamp, currentTimestamp)) == + RAY() + rate * (currentTimestamp - lastUpdateTimestamp) / (365*86400); + // the 365*86400 is SECONDS_PER_YEAR() as defined in MathUtils. +} +ghost compoundedInterestCVL(mathint,mathint,mathint) returns uint { + axiom forall mathint rate. forall mathint lastUpdateTimestamp. forall mathint currentTimestamp. + to_mathint(compoundedInterestCVL(rate, lastUpdateTimestamp, currentTimestamp)) >= + to_mathint(linearInterestCVL(rate, lastUpdateTimestamp, currentTimestamp)); + // RAY() + rate * (currentTimestamp - lastUpdateTimestamp) / (365*86400); + // the 365*86400 is SECONDS_PER_YEAR() as defined in MathUtils. +} + +ghost address ASSET; //The asset that current rule refers to. Should be assigned from within the rule. + +// The function _accrueToTreasury(...) only writes to the field accruedToTreasury. +function _accrueToTreasuryCVL() { + havoc currentContract._reserves[ASSET].accruedToTreasury; +} + +function f_linearInterestCVL(env e,uint256 rate,uint40 lastUpdateTimestamp) returns uint256 { + return linearInterestCVL(rate,lastUpdateTimestamp, e.block.timestamp); +} diff --git a/certora/solvency/specs/AUX/CVLMocks/AaveMath.spec b/certora/solvency/specs/AUX/CVLMocks/AaveMath.spec new file mode 100644 index 00000000..7d8a122e --- /dev/null +++ b/certora/solvency/specs/AUX/CVLMocks/AaveMath.spec @@ -0,0 +1,10 @@ +import "../Math/CVLMath.spec"; + +/* Math utils */ +function rayMulCVL(uint x, uint y) returns uint256 { + return mulDivUpAbstractPlus(x, y, RAY()); +} + +function rayDivCVL(uint x, uint y) returns uint256 { + return mulDivUpAbstractPlus(x, RAY(), y); +} diff --git a/certora/solvency/specs/AUX/CVLMocks/AaveMathCheck.spec b/certora/solvency/specs/AUX/CVLMocks/AaveMathCheck.spec new file mode 100644 index 00000000..84aefc1e --- /dev/null +++ b/certora/solvency/specs/AUX/CVLMocks/AaveMathCheck.spec @@ -0,0 +1,15 @@ +import "./AaveMath.spec"; + +rule rayMulCVLCorrectness(uint x, uint y) { + /* simluate rayMul */ + uint solRes = rayMulCVLPrecise(x, y); + uint cvlRes = rayMulCVL(x, y); + assert solRes == cvlRes; +} + +rule rayDivCVLCorrectness(uint x, uint y) { + /* simluate rayDiv */ + uint solRes = rayDivCVLPrecise(x, y); + uint cvlRes = rayDivCVL(x, y); + assert solRes == cvlRes; +} \ No newline at end of file diff --git a/certora/solvency/specs/AUX/CVLMocks/AddressProvider.spec b/certora/solvency/specs/AUX/CVLMocks/AddressProvider.spec new file mode 100644 index 00000000..34535bc7 --- /dev/null +++ b/certora/solvency/specs/AUX/CVLMocks/AddressProvider.spec @@ -0,0 +1,11 @@ +methods { + function _.getMarketId() external => NONDET; // expect string; + function _.getAddress(bytes32 id) external => NONDET; // expect address; + function _.getPool() external => NONDET; // expect address; + function _.getPoolConfigurator() external => NONDET; // expect address; + function _.getPriceOracle() external => NONDET; // expect address; + function _.getACLManager() external => NONDET; // expect address; + function _.getACLAdmin() external => NONDET; // expect address; + function _.getPriceOracleSentinel() external => NONDET; // expect address; + function _.getPoolDataProvider() external => NONDET; // expect address; +} \ No newline at end of file diff --git a/certora/solvency/specs/AUX/CVLMocks/aToken.spec b/certora/solvency/specs/AUX/CVLMocks/aToken.spec new file mode 100644 index 00000000..7fcafb38 --- /dev/null +++ b/certora/solvency/specs/AUX/CVLMocks/aToken.spec @@ -0,0 +1,267 @@ +// decide whether we want to summarize here for VariableDebtToken and StableDebtToken too, as interfaces are similar and so are some of the implementations. + +import "../ERC20/erc20cvlForAave.spec"; +import "./AaveMath.spec"; + +using PoolInstanceHarness as poolInstance; +//using PoolInstanceHarness as poolInstance; + +methods { + /* AToken methods */ + // no side effects: + function _.scaledTotalSupply() external => scaledTotalSupplyCVL(calledContract) expect uint256; + function _.scaledBalanceOf(address user) external => scaledBalanceOfCVL(calledContract, user) expect uint256; + function _.balanceOf(address user) external with (env e) => aTokenBalanceOfCVL(calledContract, user, e) expect uint256; + function _.totalSupply() external with (env e) => aTokenTotalSupplyCVL(calledContract, e) expect uint256; + + // addresses + function _.POOL() external => thePool expect address; + function _.RESERVE_TREASURY_ADDRESS() external => theTreasury expect address; + + // StableDebt only: + function _.getSupplyData() external => NONDET; // expect (uint256, uint256, uint256, uint40); + + // with side effects: + function _.transfer(address to, uint256 amount) external with (env e) => aTokenTransferCVL(calledContract, to, amount, e) expect bool; + function _.transferFrom(address from, address to, uint256 amount) external with (env e) => aTokenTransferFromCVL(calledContract, from, to, amount, e) expect bool; + + // matches for AToken, VariableDebtToken and StableDebtToken + function _.mint( // xxx note that VariableDebtToken expected to return bool, uint256; and StabledDebtToken expected to return bool, uint256, uint256; and aTokens are returning just bool + address caller, + address onBehalfOf, + uint256 amount, + uint256 index + ) external => aTokenMintCVL(calledContract, caller, onBehalfOf, amount, index) expect bool, uint, uint; + + // matches AToken only + function _.burn( + address from, + address receiverOfUnderlying, + uint256 amount, + uint256 index + ) external => aTokenBurnCVL(calledContract, from, receiverOfUnderlying, amount, index) expect void; + + function _.transferUnderlyingTo(address target, uint256 amount) external => aTokenTransferUnderlyingToCVL(calledContract, target, amount) expect void; + + function _.mintToTreasury(uint256 amount, uint256 index) external => aTokenMintToTreasuryCVL(calledContract, amount, index) expect void; + + function _.transferOnLiquidation(address from, address to, uint256 value) external with (env e) => aTokenTransferOnLiquidationCVL(calledContract, from, to, value, e) expect void; + + function _.handleRepayment(address user, address onBehalfOf, uint256 amount) external => aTokenHandleRepaymentCVL(calledContract, user, onBehalfOf, amount) expect void; + + function _.rescueTokens(address token, address to, uint256 amount) external with (env e) => aTokenRescueTokensCVL(calledContract, token, to, amount, e) expect void; + + // matches VariableDebtToken only + function _.burn(address from, uint256 amount, uint256 index) external => variableDebtBurnCVL(calledContract, from, amount, index) expect uint256; + + // matches StableDebtToken only + function _.burn(address from, uint256 amount) external => stableDebtBurnCVL(calledContract, from, amount) expect (uint256, uint256); + + // Side effects we don't care about + function _._setName(string memory) internal => NONDET; + function _._setSymbol(string memory) internal => NONDET; + // Getters with loops + // Pending Johannes' PR? + // function _.name() internal => nameCVL expect string; + // function _.symbol() internal => symbolCVL expect string; +} + +// Pending Johannes' PR? +// ghost string nameCVL; +// ghost string symbolCVL; + +// Pool address - same for all aTokens +// ASSUMES WE HAVE THE POOL IN THE SCENE +persistent ghost address thePool { + axiom thePool == poolInstance; +} + +// Treasury address - same for all aTokens +persistent ghost address theTreasury { + init_state axiom theTreasury == 0; +} + +/// aToken => scaledTotalSupply +// this is just totalSupplyByToken + +/// aToken => account => scaledBalance +// this is just balanceByToken + +/// aToken => underlying +/// We'd like to prove that aTokens never map to aTokens, e.g. +/// forall address aToken. aToken == 0 || aTokenToUnderlying[aToken] == 0 || aTokenToUnderlying[aTokenToUnderlying[aToken]] == 0 +persistent ghost mapping(address => address) aTokenToUnderlying { + init_state axiom forall address a. aTokenToUnderlying[a] == 0; +} + +// xxx can we use a sort instead? +definition VanillaERC20_token() returns mathint = 0; +definition AToken_token() returns mathint = 1; +definition VariableDebtToken_token() returns mathint = 2; +definition StableDebtToken_token() returns mathint = 3; + +persistent ghost mapping (address => mathint) tokenToSort { + axiom forall address a. 0 <= tokenToSort[a] && tokenToSort[a] <= 3; +} + +function scaledTotalSupplyCVL(address token) returns uint256 { + return require_uint256(totalSupplyByToken[token]); +} + +function scaledBalanceOfCVL(address token, address user) returns uint256 { + // return require_uint256(balanceByToken[token][user]); + return balanceByToken[token][user]; +} + +function indexForToken(address token, env e) returns uint256 { + uint index; + mathint tokenSort = tokenToSort[token]; + if (tokenSort == AToken_token()) { + index = poolInstance.getReserveNormalizedIncome(e, aTokenToUnderlying[token]); + } else if (tokenSort == VariableDebtToken_token()) { + index = poolInstance.getReserveNormalizedVariableDebt(e, aTokenToUnderlying[token]); + // } else if (tokenSort == StableDebtToken_token()) { + // seems disabled, so just return index=0 and then balanceOf/totalSupply will be 0... + // index = 0; + } else { + index = 0; + assert false, "unsupported token type"; + } + return index; +} + +// todo: adjust for stable debt token +function aTokenBalanceOfCVL(address token, address user, env e) returns uint256 { + uint storedBalance = balanceOfCVL(token, user); + if (aTokenToUnderlying[token] == 0) { + // not a properly initialized aToken, return the regular ERC20 balance + return storedBalance; + } + // hopefully this is the only place we actually call the pool + uint index = indexForToken(token, e); + uint ret = rayMulCVLPrecise(storedBalance, index); + return ret; +} + +persistent ghost bool PERFORM_INTERNAL_CHECK { + axiom PERFORM_INTERNAL_CHECK == true; +} + +// todo: adjust for stableDebtToken which has a completely different implementation +// and make sure to handle for variableDebtToken (same implementation based on underlying index) +function aTokenTotalSupplyCVL(address token, env e) returns uint256 { + uint storedTotalSupply = totalSupplyCVL(token); + if (aTokenToUnderlying[token] == 0) { + if (PERFORM_INTERNAL_CHECK) + assert false; // If we reach here - we have a bug in the spec ! + // not a properly initialized aToken, return the regular ERC20 totalSupply + return storedTotalSupply; + } + // hopefully this is the only place we actually call the pool + uint index = indexForToken(token, e); + uint ret = rayMulCVLPrecise(storedTotalSupply, index); + return ret; +} + +// xxx for VariableDebtToken and StableDebtToken, all transfer functions are disabled +// so need to have reverting summaries for this +function aTokenTransferCVL(address token, address to, uint256 amount, env e) returns bool { + aTokenTransferCVLInternal(token, e.msg.sender, to, amount, e); + return true; +} + +function aTokenTransferCVLInternal(address token, address from, address to, uint256 amount, env e) { + address underlying = aTokenToUnderlying[token]; + if (underlying == 0) { + // not a properly initialized aToken, use the regular ERC20 transfer + transferCVL(token, from, to, amount); + } else { + // based on AToken.sol + uint index = poolInstance.getReserveNormalizedIncome(e, underlying); + uint scaledAmount = rayDivCVLPrecise(amount, index); + transferCVL(token, from, to, scaledAmount); + // no call to POOL.finalizeTransfer. + } +} + +function aTokenTransferFromCVL(address token, address from, address to, uint256 amount, env e) returns bool { + address spender = e.msg.sender; + // copied from erc20cvl.spec: + if (allowanceByToken[token][from][spender] < amount) return false; + allowanceByToken[token][from][spender] = assert_uint256(allowanceByToken[token][from][spender] - amount); + // custom part: + aTokenTransferCVLInternal(token, from, to, amount, e); + return true; +} + +// mint in AToken: scaled erc20 minut + update user index. +// xxx mint in VariableDebtToken: decrease borrow allowance + same as AToken +// xxx mint in StableDebtToken: found no implementation? +function aTokenMintCVL(address token, address from, address to, uint amount, uint index) returns (bool, uint, uint) { + bool ret = balanceByToken[token][to] == 0; + + uint scaledAmount = rayDivCVLPrecise(amount, index); + balanceByToken[token][to] = require_uint256(balanceByToken[token][to] + scaledAmount); + totalSupplyByToken[token] = require_uint256(totalSupplyByToken[token] + scaledAmount); + + uint nondet; // for StableDebtToken + return (ret, totalSupplyByToken[token], nondet); +} + +// burn in AToken: scaled erc20 burn + update user index + in the underlying, transfer amount to the receiver +function aTokenBurnCVL(address token, address from, address receiverOfUnderlying, uint amount, uint index) { + // based on AToken.sol + uint scaledAmount = rayDivCVLPrecise(amount, index); // amount / index + balanceByToken[token][from] = require_uint256(balanceByToken[token][from] - scaledAmount); + totalSupplyByToken[token] = require_uint256(totalSupplyByToken[token] - scaledAmount); + if (token != receiverOfUnderlying) { + transferCVL(aTokenToUnderlying[token], from, receiverOfUnderlying, amount); + } + + // uint nondet; // for StableDebtToken + // return (require_uint256(totalSupplyByToken[token]), nondet); +} + +function aTokenTransferUnderlyingToCVL(address token, address target, uint amount) { + transferCVL(aTokenToUnderlying[token], token, target, amount); +} + +function aTokenMintToTreasuryCVL(address token, uint amount, uint index) { + // based on AToken.sol + if (amount == 0) { + return; + } + + aTokenMintCVL(token, thePool, theTreasury, amount, index); +} + +function aTokenTransferOnLiquidationCVL(address token, address from, address to, uint value, env e) { + aTokenTransferCVLInternal(token, from, to, value, e); +} + +function aTokenHandleRepaymentCVL(address token, address user, address onBehalfOf, uint amount) { + // In AToken.sol, does nothing. +} + +function aTokenRescueTokensCVL(address tokenCalled, address tokenToRescue, address to, uint256 amount, env e) { + require tokenToRescue != aTokenToUnderlying[tokenCalled]; + // if the tokenToRescue is an AToken, we should make sure to call the variant that checks + // if it's an AToken, and if it is, runs the right transfer function. This is okay since in + // the code of `rescueTokens`, we cast the specified token to an IERC20, so there are no + // internal-call shenaningans + // the env with which the transfer is called is where tokenCalled (our atoken) is the sender, but I'm not sure it matters + aTokenTransferCVLInternal(tokenToRescue, tokenCalled /* from */, to, amount, e); +} + +function variableDebtBurnCVL(address token, address from, uint amount, uint index) returns uint { + // based on VariableDebtToken.sol + aTokenBurnCVL(token, from, 0 /* receiver of underlying */, amount, index); + return require_uint256(totalSupplyByToken[token]); +} + +function stableDebtBurnCVL(address token, address from, uint amount) returns (uint, uint) { + // no implementation found? + uint nondet1; + uint nondet2; + return (nondet1, nondet2); +} diff --git a/certora/solvency/specs/AUX/CVLMocks/aTokenCheck.spec b/certora/solvency/specs/AUX/CVLMocks/aTokenCheck.spec new file mode 100644 index 00000000..14eccc24 --- /dev/null +++ b/certora/solvency/specs/AUX/CVLMocks/aTokenCheck.spec @@ -0,0 +1,176 @@ +import "./aToken.spec"; +import "./PoolSummarizationForTokens.spec"; +import "./IncentivesControllerForTokens.spec"; + +import "./tokenCheckBase.spec"; + +using ATokenInstance as aToken; +using Utilities as utils; + +methods { + // Note/Warning: Must _not summarize_ the super call to balanceOf! Only the top-level in AToken. Same with totalSupply. This will lead to 'empty' verification. + + // envfree declarations + function utils.nop() external envfree; + function aToken.scaledBalanceOf(address) external returns uint256 envfree; + function aToken.scaledTotalSupply() external returns uint256 envfree; + function aToken.allowance(address,address) external returns uint256 envfree; + // note that balanceOf and totalSupply are not envfree because the calls to the pool are in fact not envfree. +} + +function not_implemented() { + assert false, "Not implemented"; +} + +function dont_care() { + require false; // to be used for methods we don't model in the summary version anyway +} + +definition initialize_method_sig() returns uint32 = sig:initialize(address,address,address,address,uint8,string,string,bytes).selector; + +// xxx currently missing return value equivalence checks +function run_parametric_with_cvl_equivalent(method f, env e) { + // note there's a strong assumption the side effects of CVL and Solidity versions are disjoint + if (f.selector == sig:transfer(address,uint256).selector) { + address a1; + uint256 u1; + aTokenTransferCVL(aToken, a1, u1, e); + aToken.transfer(e, a1, u1); + } else if (f.selector == sig:transferFrom(address,address,uint256).selector) { + address a1; + address a2; + uint256 u1; + // must have the same allowance at the beginning to have the same effect + require allowanceByToken[aToken][a1][e.msg.sender] == aToken.allowance(e, a1, e.msg.sender); + aTokenTransferFromCVL(aToken, a1, a2, u1, e); + aToken.transferFrom(e, a1, a2, u1); + } else if (f.selector == sig:transferOnLiquidation(address,address,uint256).selector) { + address a1; + address a2; + uint256 u1; + aTokenTransferOnLiquidationCVL(aToken, a1, a2, u1, e); + aToken.transferOnLiquidation(e, a1, a2, u1); + } else if (f.selector == sig:transferUnderlyingTo(address,uint256).selector) { + address a1; + uint256 u1; + aTokenTransferUnderlyingToCVL(aToken, a1, u1); + aToken.transferUnderlyingTo(e, a1, u1); + } else if (f.selector == sig:permit(address,address,uint256,uint256,uint8,bytes32,bytes32).selector) { + address a1; + address a2; + uint256 u1; + uint256 u2; + uint8 u3; + bytes32 by1; + bytes32 by2; + permitCVL(aToken, a1, a2, u1, u2, u3, by1, by2); + aToken.permit(e, a1, a2, u1, u2, u3, by1, by2); + } else if (f.selector == sig:approve(address,uint256).selector) { + address a1; + uint256 u1; + approveCVL(aToken, e.msg.sender, a1, u1); + aToken.approve(e, a1, u1); + } else if (f.selector == sig:decreaseAllowance(address,uint256).selector) { + address a1; + uint256 u1; + decreaseAllowanceCVL(aToken, e.msg.sender, a1, u1); + aToken.decreaseAllowance(e, a1, u1); + } else if (f.selector == sig:increaseAllowance(address,uint256).selector) { + address a1; + uint256 u1; + increaseAllowanceCVL(aToken, e.msg.sender, a1, u1); + aToken.increaseAllowance(e, a1, u1); + } else if (f.selector == sig:mint(address,address,uint256,uint256).selector) { + address a1; + address a2; + uint256 u1; + uint256 u2; + aTokenMintCVL(aToken, a1, a2, u1, u2); + aToken.mint(e, a1, a2, u1, u2); + } else if (f.selector == sig:mintToTreasury(uint256,uint256).selector) { + uint256 u1; + uint256 u2; + aTokenMintToTreasuryCVL(aToken, u1, u2); + aToken.mintToTreasury(e, u1, u2); + } else if (f.selector == sig:burn(address,address,uint256,uint256).selector) { + address a1; + address a2; + uint256 u1; + uint256 u2; + aTokenBurnCVL(aToken, a1, a2, u1, u2); + aToken.burn(e, a1, a2, u1, u2); + } else if (f.selector == sig:handleRepayment(address,address,uint256).selector) { + // it's literally a no-op + address a1; + address a2; + uint256 u1; + aTokenHandleRepaymentCVL(aToken, a1, a2, u1); + aToken.handleRepayment(e, a1, a2, u1); + } else if (f.selector == initialize_method_sig()) { + // we're running all of our equivalence rules on an 'initialized' state of the AToken + dont_care(); + } else if (f.selector == sig:rescueTokens(address,address,uint256).selector) { + // there is no idempotency here (the summary will just simulate whatever is linked-to by rescueTokens, + // in case the rescued token is an AToken), so rules may fail. + // We will prove equivalence in an alternative way, see rescueTokenEquivalence + dont_care(); + } else if (f.selector == sig:setIncentivesController(address).selector) { + address a1; + // no CVL implementation, so nop + aToken.setIncentivesController(e, a1); + } else { + not_implemented(); + } +} + +rule rescueTokenEquivalence() { + env e; + address a1; + address a2; + uint256 u1; + storage init = lastStorage; + aTokenRescueTokensCVL(aToken, a1, a2, u1, e); + utils.nop(); // to properly set lastStorage, which is only updated in solidity calls + storage afterCVL = lastStorage; + aToken.rescueTokens(e, a1, a2, u1) at init; + storage afterSol = lastStorage; + assert afterCVL == afterSol; +} + +hook Sstore _underlyingAsset address newValue { + aTokenToUnderlying[aToken] = newValue; +} + +hook Sstore _treasury address newValue { + theTreasury = newValue; +} + +invariant aTokenUnderlyingMatchesGhost() + aToken._underlyingAsset == aTokenToUnderlying[aToken] + filtered { f -> f.contract == currentContract } + +invariant aTokenTreasuryMatchesGhost() + aToken._treasury == theTreasury + filtered { f -> f.contract == currentContract } + +use builtin rule sanity filtered { f -> f.contract == currentContract } + +function init_state_invariants() { + requireInvariant aTokensAreNotUnderlyings(); + requireInvariant aTokenUnderlyingMatchesGhost(); + requireInvariant aTokenTreasuryMatchesGhost(); + require currentATokenHasAnUnderlying(); // see definition for justification + require tokenToSort[aToken] == AToken_token(); +} + + +use rule balanceOfEquivalence; +use rule scaledBalanceOfEquivalence; +use rule totalSupplyEquivalence; +use rule scaledTotalSupplyEquivalence; +use rule allowanceEquivalence; +use rule listOtherViewFunctions; +use invariant aTokensAreNotUnderlyings; +use rule currentATokenHasAnUnderlyingAfterInitiailization; +use rule aTokenWithoutUnderlyingIsERC20AfterInitialization; + diff --git a/certora/solvency/specs/AUX/CVLMocks/generic.spec b/certora/solvency/specs/AUX/CVLMocks/generic.spec new file mode 100644 index 00000000..b6234ffd --- /dev/null +++ b/certora/solvency/specs/AUX/CVLMocks/generic.spec @@ -0,0 +1,88 @@ +/* +This rule find which functions are privileged. +A function is privileged if there is only one address that can call it. + +The rules finds this by finding which functions can be called by two different users. +*/ +rule privilegedOperation(method f, address privileged) { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + + storage initialStorage = lastStorage; + f@withrevert(e1, arg); // privileged succeeds executing candidate privileged operation. + bool firstSucceeded = !lastReverted; + + env e2; + calldataarg arg2; + require e2.msg.sender != privileged; + f@withrevert(e2, arg2) at initialStorage; // unprivileged + bool secondSucceeded = !lastReverted; + + assert !(firstSucceeded && secondSucceeded); +} + +rule timeoutChecker(method f) { + storage before = lastStorage; + env e; calldataarg arg; + f(e,arg); + assert before == lastStorage; +} + +/* +This rule find which functions that can be called, may fail due to someone else calling a function right before. + +This is n expensive rule - might fail on the demo site on big contracts +*/ +rule simpleFrontRunning(method f, address privileged) filtered { f-> !f.isView } { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + storage initialStorage = lastStorage; + f(e1, arg); + bool firstSucceeded = !lastReverted; + env e2; + calldataarg arg2; + require e2.msg.sender != e1.msg.sender; + f(e2, arg2) at initialStorage; + f@withrevert(e1, arg); + bool succeeded = !lastReverted; + assert succeeded; +} + +rule noRevert(method f) { + env e; + calldataarg arg; + require e.msg.value == 0; + f@withrevert(e, arg); + assert !lastReverted; +} + + +rule alwaysRevert(method f) { + env e; + calldataarg arg; + f@withrevert(e, arg); + assert lastReverted; +} + +use builtin rule sanity; +use builtin rule hasDelegateCalls; +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; + +/** + +// Integrate rules from generic.spec in importing specs like this: + +use builtin rule sanity filtered { f -> f.contract == currentContract } +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } + + **/ \ No newline at end of file diff --git a/certora/solvency/specs/AUX/ERC20/erc20cvlForAave.spec b/certora/solvency/specs/AUX/ERC20/erc20cvlForAave.spec new file mode 100644 index 00000000..a0277c23 --- /dev/null +++ b/certora/solvency/specs/AUX/ERC20/erc20cvlForAave.spec @@ -0,0 +1,104 @@ +methods { + // ERC20 standard + function _.name() external => NONDET; // can we use PER_CALLEE_CONSTANT? + function _.symbol() external => NONDET; // can we use PER_CALLEE_CONSTANT? + function _.decimals() external => PER_CALLEE_CONSTANT; + // function _.totalSupply() external => totalSupplyCVL(calledContract) expect uint256; // Aave specs will override + // function _.balanceOf(address a) external => balanceOfCVL(calledContract, a) expect uint256; // Aave specs will override + function _.allowance(address a, address b) external => allowanceCVL(calledContract, a, b) expect uint256; + function _.approve(address a, uint256 x) external with (env e) => approveCVL(calledContract, e.msg.sender, a, x) expect bool; + // function _.transfer(address a, uint256 x) external with (env e) => transferCVL(calledContract, e.msg.sender, a, x) expect bool; // Aave specs will override + // function _.transferFrom(address a, address b, uint256 x) external with (env e) => transferFromCVL(calledContract, e.msg.sender, a, b, x) expect bool; // Aave specs will override + + // increase/decrease allowance + function _.increaseAllowance(address spender, uint256 addedValue) external with (env e) => increaseAllowanceCVL(calledContract, e.msg.sender, spender, addedValue) expect bool; + function _.decreaseAllowance(address spender, uint256 subtractedValue) external with (env e) => decreaseAllowanceCVL(calledContract, e.msg.sender, spender, subtractedValue) expect bool; + + // Permit + // xxx unsound + function _.permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external => permitCVL(calledContract, owner, spender, value, deadline, v, r, s) expect void; + +} + + +/// CVL simple implementations of IERC20: +/// token => totalSupply +persistent ghost mapping(address => uint256) totalSupplyByToken { + init_state axiom forall address a. totalSupplyByToken[a]==0; +} + +/// token => account => balance +persistent ghost mapping(address => mapping(address => uint256)) balanceByToken { + init_state axiom forall address a. forall address b. balanceByToken[a][b]==0; +} +/// token => owner => spender => allowance +persistent ghost mapping(address => mapping(address => mapping(address => uint256))) allowanceByToken { + init_state axiom forall address a. forall address b. forall address c. allowanceByToken[a][b][c]==0; +} + +// function tokenBalanceOf(address token, address account) returns uint256 { +// return balanceByToken[token][account]; +// } + +function totalSupplyCVL(address token) returns uint256 { + return totalSupplyByToken[token]; +} + +function balanceOfCVL(address token, address a) returns uint256 { + return balanceByToken[token][a]; +} + +function allowanceCVL(address token, address a, address b) returns uint256 { + return allowanceByToken[token][a][b]; +} + +function approveCVL(address token, address approver, address spender, uint256 amount) returns bool { + allowanceByToken[token][approver][spender] = amount; + return true; +} + +function transferFromCVL(address token, address spender, address from, address to, uint256 amount) returns bool { + if (allowanceByToken[token][from][spender] < amount) return false; + allowanceByToken[token][from][spender] = assert_uint256(allowanceByToken[token][from][spender] - amount); + return transferCVL(token, from, to, amount); +} + +function transferCVL(address token, address from, address to, uint256 amount) returns bool { + //if(balanceByToken[token][from] < amount) return false; + require balanceByToken[token][from] >= amount; + balanceByToken[token][from] = assert_uint256(balanceByToken[token][from] - amount); + balanceByToken[token][to] = require_uint256(balanceByToken[token][to] + amount); // We neglect overflows. + return true; +} + +function increaseAllowanceCVL(address token, address owner, address spender, uint256 increasedAmount) returns bool { + uint256 amt = require_uint256(allowanceCVL(token, owner, spender) + increasedAmount); + return approveCVL(token, owner, spender, amt); +} + +function decreaseAllowanceCVL(address token, address owner, address spender, uint256 decreasedAmount) returns bool { + uint256 amt = require_uint256(allowanceCVL(token, owner, spender) - decreasedAmount); + return approveCVL(token, owner, spender, amt); +} + +function permitCVL( + address token, + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s +) { + // xxx not checking conditions + approveCVL(token, owner, spender, value); +} diff --git a/certora/solvency/specs/AUX/FlashLoanReceiver.spec b/certora/solvency/specs/AUX/FlashLoanReceiver.spec new file mode 100644 index 00000000..f85049f4 --- /dev/null +++ b/certora/solvency/specs/AUX/FlashLoanReceiver.spec @@ -0,0 +1,18 @@ +methods { + function _.executeOperation( + address[] assets, + uint256[] amounts, + uint256[] premiums, + address initiator, + bytes params + ) external => NONDET; // expect bool; + + // simple receiver + function _.executeOperation( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes params + ) external => NONDET; // expect bool; +} \ No newline at end of file diff --git a/certora/solvency/specs/AUX/Math/CVLMath.spec b/certora/solvency/specs/AUX/Math/CVLMath.spec new file mode 100644 index 00000000..39012c45 --- /dev/null +++ b/certora/solvency/specs/AUX/Math/CVLMath.spec @@ -0,0 +1,240 @@ +/****************************************** +----------- CVL Math Library -------------- +*******************************************/ + +// A restriction on the value of w = x * y / z +// The ratio between x (or y) and z is a rational number a/b or b/a. +// Important : do not set a = 0 or b = 0. +// Note: constRatio(x,y,z,a,b,w) <=> constRatio(x,y,z,b,a,w) +definition constRatio(uint256 x, uint256 y, uint256 z, + uint256 a, uint256 b, uint256 w) + returns bool = + ( a * x == b * z && to_mathint(w) == (b * y) / a ) || + ( b * x == a * z && to_mathint(w) == (a * y) / b ) || + ( a * y == b * z && to_mathint(w) == (b * x) / a ) || + ( b * y == a * z && to_mathint(w) == (a * x) / b ); + +// A restriction on the value of w = x * y / z +// The division quotient between x (or y) and z is an integer q or 1/q. +// Important : do not set q=0 +definition constQuotient(uint256 x, uint256 y, uint256 z, + uint256 q, uint256 w) + + returns bool = + ( to_mathint(x) == q * z && to_mathint(w) == q * y ) || + ( q * x == to_mathint(z) && to_mathint(w) == y / q ) || + ( to_mathint(y) == q * z && to_mathint(w) == q * x ) || + ( q * y == to_mathint(z) && to_mathint(w) == x / q ); + +/// Equivalent to the one above, but with implication +definition constQuotientImply(uint256 x, uint256 y, uint256 z, + uint256 q, uint256 w) + + returns bool = + ( to_mathint(x) == q * z => to_mathint(w) == q * y ) && + ( q * x == to_mathint(z) => to_mathint(w) == y / q ) && + ( to_mathint(y) == q * z => to_mathint(w) == q * x ) && + ( q * y == to_mathint(z) => to_mathint(w) == x / q ); + +definition ONE18() returns uint256 = 1000000000000000000; +definition RAY() returns uint256 = 10^27; + +definition _monotonicallyIncreasing(uint256 x, uint256 y, uint256 fx, uint256 fy) returns bool = + (x > y => fx >= fy); + +definition _monotonicallyDecreasing(uint256 x, uint256 y, uint256 fx, uint256 fy) returns bool = + (x > y => fx <= fy); + +definition abs(mathint x) returns mathint = + x >= 0 ? x : 0 - x; + +definition min(mathint x, mathint y) returns mathint = + x > y ? y : x; + +definition max(mathint x, mathint y) returns mathint = + x > y ? x : y; + +/// Returns whether y is equal to x up to error bound of 'err' (18 decs). +/// e.g. 10% relative error => err = 1e17 +definition relativeErrorBound(mathint x, mathint y, mathint err) returns bool = + (x != 0 + ? abs(x - y) * ONE18() <= abs(x) * err + : abs(y) <= err); + +/// Axiom for a weighted average of the form WA = (x * y) / (y + z) +/// This is valid as long as z + y > 0 => make certain of that condition in the use of this definition. +definition weightedAverage(mathint x, mathint y, mathint z, mathint WA) returns bool = + ((x > 0 && y > 0) => (WA >= 0 && WA <= x)) + && + ((x < 0 && y > 0) => (WA <= 0 && WA >= x)) + && + ((x > 0 && y < 0) => (WA <= 0 && WA - x <= 0)) + && + ((x < 0 && y < 0) => (WA >= 0 && WA + x <= 0)) + && + ((x == 0 || y == 0) => (WA == 0)); + + +function mulDivDownAbstract(uint256 x, uint256 y, uint256 z) returns uint256 { + require z !=0; + uint256 xy = require_uint256(x * y); + uint256 res; + mathint rem; + require z * res + rem == to_mathint(xy); + require rem < to_mathint(z); + return res; +} + +function mulDivDownAbstractPlus(uint256 x, uint256 y, uint256 z) returns uint256 { + uint256 res; + require z != 0; + uint256 xy = require_uint256(x * y); + uint256 fz = require_uint256(res * z); + + require xy >= fz; + require fz + z > to_mathint(xy); + return res; +} + +function mulDivUpAbstractPlus(uint256 x, uint256 y, uint256 z) returns uint256 { + uint256 res; + require z != 0; + uint256 xy = require_uint256(x * y); + uint256 fz = require_uint256(res * z); + require xy >= fz; + require fz + z > to_mathint(xy); + + if(xy == fz) { + return res; + } + return require_uint256(res + 1); +} + +function mulDownWad(uint256 x, uint256 y) returns uint256 { + return mulDivDownAbstractPlus(x, y, ONE18()); +} + +function mulUpWad(uint256 x, uint256 y) returns uint256 { + return mulDivUpAbstractPlus(x, y, ONE18()); +} + +function divDownWad(uint256 x, uint256 y) returns uint256 { + return mulDivDownAbstractPlus(x, ONE18(), y); +} + +function divUpWad(uint256 x, uint256 y) returns uint256 { + return mulDivUpAbstractPlus(x, ONE18(), y); +} + +function discreteQuotientMulDiv(uint256 x, uint256 y, uint256 z) returns uint256 +{ + uint256 res; + require z != 0 && noOverFlowMul(x, y); + // Discrete quotients: + require( + ((x ==0 || y ==0) && res == 0) || + (x == z && res == y) || + (y == z && res == x) || + constQuotient(x, y, z, 2, res) || // Division quotient is 1/2 or 2 + constQuotient(x, y, z, 5, res) || // Division quotient is 1/5 or 5 + constQuotient(x, y, z, 100, res) // Division quotient is 1/100 or 100 + ); + return res; +} + +function discreteRatioMulDiv(uint256 x, uint256 y, uint256 z) returns uint256 +{ + uint256 res; + require z != 0 && noOverFlowMul(x, y); + // Discrete ratios: + require( + ((x ==0 || y ==0) && res == 0) || + (x == z && res == y) || + (y == z && res == x) || + constRatio(x, y, z, 2, 1, res) || // f = 2*x or f = x/2 (same for y) + constRatio(x, y, z, 5, 1, res) || // f = 5*x or f = x/5 (same for y) + constRatio(x, y, z, 2, 3, res) || // f = 2*x/3 or f = 3*x/2 (same for y) + constRatio(x, y, z, 2, 7, res) // f = 2*x/7 or f = 7*x/2 (same for y) + ); + return res; +} + +function noOverFlowMul(uint256 x, uint256 y) returns bool +{ + return x * y <= max_uint; +} + +/// @doc Ghost power function that incorporates mathematical pure x^y axioms. +/// @warning Some of these axioms might be false, depending on the Solidity implementation +/// The user must bear in mind that equality-like axioms can be violated because of rounding errors. +ghost _ghostPow(uint256, uint256) returns uint256 { + /// x^0 = 1 + axiom forall uint256 x. _ghostPow(x, 0) == ONE18(); + /// 0^x = 1 + axiom forall uint256 y. _ghostPow(0, y) == 0; + /// x^1 = x + axiom forall uint256 x. _ghostPow(x, ONE18()) == x; + /// 1^y = 1 + axiom forall uint256 y. _ghostPow(ONE18(), y) == ONE18(); + + /// I. x > 1 && y1 > y2 => x^y1 > x^y2 + /// II. x < 1 && y1 > y2 => x^y1 < x^y2 + axiom forall uint256 x. forall uint256 y1. forall uint256 y2. + x >= ONE18() && y1 > y2 => _ghostPow(x, y1) >= _ghostPow(x, y2); + axiom forall uint256 x. forall uint256 y1. forall uint256 y2. + x < ONE18() && y1 > y2 => (_ghostPow(x, y1) <= _ghostPow(x, y2) && _ghostPow(x,y2) <= ONE18()); + axiom forall uint256 x. forall uint256 y. + x < ONE18() && y > ONE18() => (_ghostPow(x, y) <= x); + axiom forall uint256 x. forall uint256 y. + x < ONE18() && y <= ONE18() => (_ghostPow(x, y) >= x); + axiom forall uint256 x. forall uint256 y. + x >= ONE18() && y > ONE18() => (_ghostPow(x, y) >= x); + axiom forall uint256 x. forall uint256 y. + x >= ONE18() && y <= ONE18() => (_ghostPow(x, y) <= x); + /// x1 > x2 && y > 0 => x1^y > x2^y + axiom forall uint256 x1. forall uint256 x2. forall uint256 y. + x1 > x2 => _ghostPow(x1, y) >= _ghostPow(x2, y); + + /* Additional axioms - potentially unsafe + /// x^y * x^(1-y) == x -> 0.01% relative error + axiom forall uint256 x. forall uint256 y. forall uint256 z. + (0 <= y && y <= ONE18() && z + y == to_mathint(ONE18())) => + relativeErrorBound(_ghostPow(x, y) * _ghostPow(x, z), x * ONE18(), ONE18() / 10000); + + /// (x^y)^(1/y) == x -> 1% relative error + axiom forall uint256 x. forall uint256 y. forall uint256 z. + (0 <= y && y <= ONE18() && z * y == ONE18()*ONE18() ) => + relativeErrorBound(_ghostPow(_ghostPow(x, y), z), x, ONE18() / 100); + */ +} + +function CVLPow(uint256 x, uint256 y) returns uint256 { + if (y == 0) {return ONE18();} + if (x == 0) {return 0;} + return _ghostPow(x, y); +} + +function CVLSqrt(uint256 x) returns uint256 { + mathint SQRT; + require SQRT*SQRT <= to_mathint(x) && (SQRT + 1)*(SQRT + 1) > to_mathint(x); + return require_uint256(SQRT); +} + +// For Aave +function rayMulCVLPrecise(uint x, uint y) returns uint256 { + return require_uint256((x*y + RAY()/2) / RAY()); +// return rayMul(x,y); +// uint256 ret; +// require x*y + RAY()/2 <= ret*RAY() && ret*RAY() < x*y + 3*RAY()/2; +// return ret; +} + +function rayDivCVLPrecise(uint x, uint y) returns uint256 { + require y != 0; + return require_uint256((x*RAY() + y/2)/y); + //return rayDiv(x,y); + // require y != 0; + //uint256 ret; + //require x*RAY() + y/2 <= y*ret && y*ret < x*RAY() + y/2 + y; + //return ret; +} diff --git a/certora/solvency/specs/Aave/.ACLManager.spec b/certora/solvency/specs/Aave/.ACLManager.spec new file mode 100644 index 00000000..6777414f --- /dev/null +++ b/certora/solvency/specs/Aave/.ACLManager.spec @@ -0,0 +1,10 @@ +methods { + function _.isPoolAdmin(address admin) external => NONDET; // expect bool; + function _.isEmergencyAdmin(address admin) external => NONDET; // expect bool; + function _.isRiskAdmin(address admin) external => NONDET; // expect bool; + function _.isFlashBorrower(address borrower) external => NONDET; // expect bool; + function _.isBridge(address bridge) external => NONDET; // expect bool; + function _.isAssetListingAdmin(address admin) external => NONDET; // expect bool; + + function _.hasRole(bytes32 role, address account) external => NONDET; // expect bool; +} \ No newline at end of file diff --git a/certora/solvency/specs/Aave/.IncentivesControllerForTokens.spec b/certora/solvency/specs/Aave/.IncentivesControllerForTokens.spec new file mode 100644 index 00000000..2a5d6026 --- /dev/null +++ b/certora/solvency/specs/Aave/.IncentivesControllerForTokens.spec @@ -0,0 +1,4 @@ +methods { + // these summaries require additional rules to be safe + function _.handleAction(address user, uint256 totalSupply, uint256 userBalance) external => NONDET; // it is actually non-view, check no interaction with tokens XXX +} \ No newline at end of file diff --git a/certora/solvency/specs/Aave/.PoolSummarizationForTokens.spec b/certora/solvency/specs/Aave/.PoolSummarizationForTokens.spec new file mode 100644 index 00000000..acbef625 --- /dev/null +++ b/certora/solvency/specs/Aave/.PoolSummarizationForTokens.spec @@ -0,0 +1,44 @@ +// This is to be used in tandem with `PoolInstanceForAToken` and similar, +// as we build them. +methods { + function _.ADDRESSES_PROVIDER() external => NONDET; // expect address + + // Folding an internal function inside `getReserveNormalizedIncome` + // So that we could summarize it in both spec and contract calls + function _.getReserveNormalizedIncomeInt(address asset) internal with (env e) + => computeReserveNormalizedIncome(asset, e) expect uint256; + + function _.getReserveNormalizedIncome(address asset) external with (env e) + => computeReserveNormalizedIncome(asset, e) expect uint256; + + // Same trick for `getReserveNormalizedVariableDebt` + function _.getReserveNormalizedVariableDebtInt(address asset) internal with (env e) + => computeReserveNormalizedVariableDebt(asset, e) expect uint256; + + function _.getReserveNormalizedVariableDebt(address asset) external with (env e) + => computeReserveNormalizedVariableDebt(asset, e) expect uint256; + + // these summaries require additional rules to be safe + function _.finalizeTransfer( + address asset, + address from, + address to, + uint256 amount, + uint256 balanceFromBefore, + uint256 balanceToBefore + ) external => NONDET; // it is actually non-view, check no interaction with tokens XXX +} + +// Probably needs to model also last and current timestamps +ghost mapping(address => uint256) thePoolReservedNormalizedIncome; +function computeReserveNormalizedIncome(address underlyingAsset, env e) returns uint256 { + // xxx need to take timestamp into account + return thePoolReservedNormalizedIncome[underlyingAsset]; +} + +ghost mapping(address => uint256) thePoolReservedNormalizedVariableDebt; +function computeReserveNormalizedVariableDebt(address underlyingAsset, env e) returns uint256 { + // xxx need to take timestamp into account + return thePoolReservedNormalizedVariableDebt[underlyingAsset]; +} + diff --git a/certora/solvency/specs/Aave/.PriceOracle.spec b/certora/solvency/specs/Aave/.PriceOracle.spec new file mode 100644 index 00000000..b5658537 --- /dev/null +++ b/certora/solvency/specs/Aave/.PriceOracle.spec @@ -0,0 +1,3 @@ +methods { + function _.getAssetPrice(address asset) external => NONDET; // expect uint256; +} \ No newline at end of file diff --git a/certora/solvency/specs/Aave/.PriceOracleSentinel.spec b/certora/solvency/specs/Aave/.PriceOracleSentinel.spec new file mode 100644 index 00000000..47b0a175 --- /dev/null +++ b/certora/solvency/specs/Aave/.PriceOracleSentinel.spec @@ -0,0 +1,5 @@ +methods { + // no side effects + function _.isBorrowAllowed() external => NONDET; // expect bool; + function _.isLiquidationAllowed() external => NONDET; // expect bool; +} \ No newline at end of file diff --git a/certora/solvency/specs/Aave/.ReserveInterestRateStrategy.spec b/certora/solvency/specs/Aave/.ReserveInterestRateStrategy.spec new file mode 100644 index 00000000..5284cc33 --- /dev/null +++ b/certora/solvency/specs/Aave/.ReserveInterestRateStrategy.spec @@ -0,0 +1,24 @@ +methods { + // function _.calculateInterestRates( + // DataTypes.CalculateInterestRatesParams params + // ) external => calculateInterestRatesCVL(calledContract, params) expect (uint256, uint256, uint256); // marked view +} + +ghost mapping(mathint /* liquidityDelta */ => uint256) liquidityRateModel { + // monotone-decreasing, see [checkNextLiquidityRateChangeWhenLiquidityAddedOrTakenChangesLe] + axiom forall mathint n. forall mathint m. n >= m => liquidityRateModel[n] <= liquidityRateModel[m]; +} + +function calculateInterestRatesCVL( + address interestRateStrategy, // redundancy + DataTypes.CalculateInterestRatesParams params +) returns (uint256, uint256, uint256) { + uint256 liquidityRate = liquidityRateModel[params.liquidityAdded - params.liquidityTaken]; + uint256 stableBorrowRate; + uint256 variableBorrowRate; + + require (params.usingVirtualBalance && params.totalStableDebt + params.totalVariableDebt != 0) + => params.liquidityTaken <= require_uint256(params.virtualUnderlyingBalance + params.liquidityAdded); + + return (liquidityRate, stableBorrowRate, variableBorrowRate); +} diff --git a/certora/solvency/specs/Aave/.ReserveInterestRateStrategyCheck.spec b/certora/solvency/specs/Aave/.ReserveInterestRateStrategyCheck.spec new file mode 100644 index 00000000..157c77f9 --- /dev/null +++ b/certora/solvency/specs/Aave/.ReserveInterestRateStrategyCheck.spec @@ -0,0 +1,268 @@ +import "../Math/CVLMath.spec"; + +// current contract is assumed to be an IReserveInterestRateStrategy +methods { + function calculateInterestRates( + DataTypes.CalculateInterestRatesParams params + ) external; // view function + + // optimizations + function _.wadToRay(uint256 a) internal => wadToRayCVL(a) expect uint256; // this is optimized well actually + function _.rayMul(uint256 a, uint256 b) internal => rayMulCVLPrecise(a, b) expect uint256; // not optimized well by Prover + function _.rayDiv(uint256 a, uint256 b) internal => rayDivCVLPrecise(a, b) expect uint256; // seems to be optimized well by Prover + function _.percentMul(uint256 value, uint256 percentage) internal => percentMulPrecise(value, percentage) expect uint256; // to use require_uint256 instead of inline assembly +} + +definition WAD_RAY_RATIO() returns uint256 = /* 1e9 */ 1000000000; + +function wadToRayCVL(uint256 a) returns uint256 { + return require_uint256(a * WAD_RAY_RATIO()); +} + + +definition PERCENTAGE_FACTOR() returns uint256 = /* 1e4 */ 10000; +definition HALF_PERCENTAGE_FACTOR() returns uint256 = /* 0.5e4 */ 5000; + +function percentMulPrecise(uint256 value, uint256 percentage) returns uint256 { + return require_uint256((value*percentage + HALF_PERCENTAGE_FACTOR())/ PERCENTAGE_FACTOR()); +} + + +rule checkCalculateInterestRatesCVLSummary { + env e; + DataTypes.CalculateInterestRatesParams params; + calculateInterestRates(e, params); + assert (params.usingVirtualBalance && params.totalStableDebt + params.totalVariableDebt != 0) + => params.liquidityTaken <= assert_uint256(params.virtualUnderlyingBalance + params.liquidityAdded); +} + +// exploratory rule section +function checkNextLiquidityRateEffects( + DataTypes.CalculateInterestRatesParams params1, + DataTypes.CalculateInterestRatesParams params2 +) returns (uint256, uint256) { + env e; + + uint256 nextLiquidityRate1; + uint256 nextStableRate1; + uint256 nextVariableRate1; + nextLiquidityRate1, nextStableRate1, nextVariableRate1 = calculateInterestRates(e, params1); + uint256 nextLiquidityRate2; + uint256 nextStableRate2; + uint256 nextVariableRate2; + nextLiquidityRate2, nextStableRate2, nextVariableRate2 = calculateInterestRates(e, params2); + + return (nextLiquidityRate1, nextLiquidityRate2); +} + +function LeTotalVariableDebt( + DataTypes.CalculateInterestRatesParams params1, + DataTypes.CalculateInterestRatesParams params2 +) { + require + params1.unbacked == params2.unbacked + && params1.liquidityAdded == params2.liquidityAdded + && params1.liquidityTaken == params2.liquidityTaken + && params1.totalStableDebt == params2.totalStableDebt + && params1.totalVariableDebt <= params2.totalVariableDebt + && params1.averageStableBorrowRate == params2.averageStableBorrowRate + && params1.reserveFactor == params2.reserveFactor + && params1.reserve == params2.reserve + && params1.usingVirtualBalance == params2.usingVirtualBalance + && params1.virtualUnderlyingBalance == params2.virtualUnderlyingBalance + ; +} + +function GeTotalVariableDebt( + DataTypes.CalculateInterestRatesParams params1, + DataTypes.CalculateInterestRatesParams params2 +) { + require + params1.unbacked == params2.unbacked + && params1.liquidityAdded == params2.liquidityAdded + && params1.liquidityTaken == params2.liquidityTaken + && params1.totalStableDebt == params2.totalStableDebt + && params1.totalVariableDebt >= params2.totalVariableDebt + && params1.averageStableBorrowRate == params2.averageStableBorrowRate + && params1.reserveFactor == params2.reserveFactor + && params1.reserve == params2.reserve + && params1.usingVirtualBalance == params2.usingVirtualBalance + && params1.virtualUnderlyingBalance == params2.virtualUnderlyingBalance + ; +} + +function GeLiquidity( + DataTypes.CalculateInterestRatesParams params1, + DataTypes.CalculateInterestRatesParams params2 +) { + // if liquidity added is increased and liquidity taken is decreased then there's _more_ available liquidity + // which decreases the borrow rate, thus decreasing the liquidity rate + require + params1.unbacked == params2.unbacked + && params1.liquidityAdded >= params2.liquidityAdded + && params1.liquidityTaken <= params2.liquidityTaken + && params1.totalStableDebt == params2.totalStableDebt + && params1.totalVariableDebt == params2.totalVariableDebt + && params1.averageStableBorrowRate == params2.averageStableBorrowRate + && params1.reserveFactor == params2.reserveFactor + && params1.reserve == params2.reserve + && params1.usingVirtualBalance == params2.usingVirtualBalance + && params1.virtualUnderlyingBalance == params2.virtualUnderlyingBalance + ; +} + +// wrong +rule checkNextLiquidityRateChangeWhenTotalVariableDebtDecreasesGe() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + LeTotalVariableDebt(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 >= nextLiquidityRate2, "params1 resulted in >= nextLiquidityRate"; +} + +// wrong +rule checkNextLiquidityRateChangeWhenTotalVariableDebtDecreasesLe() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + LeTotalVariableDebt(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 <= nextLiquidityRate2, "params1 resulted in >= nextLiquidityRate"; +} + +// wrong +rule checkNextLiquidityRateChangeWhenTotalVariableDebtDecreasesEq() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + LeTotalVariableDebt(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 == nextLiquidityRate2, "params1 resulted in >= nextLiquidityRate"; +} + +// wrong +rule checkNextLiquidityRateChangeWhenTotalVariableDebtIncreasesGe() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + GeTotalVariableDebt(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 >= nextLiquidityRate2, "params1 resulted in >= nextLiquidityRate"; +} + +// wrong +rule checkNextLiquidityRateChangeWhenTotalVariableDebtIncreasesLe() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + GeTotalVariableDebt(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 <= nextLiquidityRate2, "params1 resulted in <= nextLiquidityRate"; +} + +// wrong +rule checkNextLiquidityRateChangeWhenLiquidityAddedOrTakenChangesGe() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + GeLiquidity(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 >= nextLiquidityRate2, "params1 resulted in >= nextLiquidityRate"; +} + +// see [GeLiquidity] - I'd expect this to be true - liquidity increased in params1 -> nextLiquidityRate1 is smaller +rule checkNextLiquidityRateChangeWhenLiquidityAddedOrTakenChangesLe() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + GeLiquidity(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 <= nextLiquidityRate2, "params1 resulted in <= nextLiquidityRate"; +} + +// wrong +rule checkNextLiquidityRateChangeWhenLiquidityAddedOrTakenChangesEq() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + GeLiquidity(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 == nextLiquidityRate2, "params1 resulted in == nextLiquidityRate"; +} + +// sanity +//use builtin rule sanity filtered { f -> f.contract == currentContract } + + + + + + +// wrong +rule checkNextLiquidityRateChangeWhenTotalVariableDebtIncreasesEq() { + DataTypes.CalculateInterestRatesParams params1; + DataTypes.CalculateInterestRatesParams params2; + GeTotalVariableDebt(params1, params2); + uint256 nextLiquidityRate1; + uint256 nextLiquidityRate2; + (nextLiquidityRate1, nextLiquidityRate2) = checkNextLiquidityRateEffects(params1, params2); + assert nextLiquidityRate1 >= nextLiquidityRate2, "params1 resulted in == nextLiquidityRate"; +} + + + +rule borrowRate_GEQ_liquidityRate(env e) { + DataTypes.CalculateInterestRatesParams params; + + require params.totalStableDebt == 0; + require params.averageStableBorrowRate == 0; + // require params.unbacked==0; + // require params.totalVariableDebt == 1; + //require params.virtualUnderlyingBalance == 1; + require params.reserveFactor == 500; // 5% + + uint256 liquidity_rate; + uint256 nextStableRate1; + uint256 variable_rate; + liquidity_rate, nextStableRate1, variable_rate = calculateInterestRates(e, params); + + // require get___vars_availableLiquidityPlusDebt() > 1; + + assert liquidity_rate <= variable_rate; +} + + +rule _getOverallBorrowRate_with_0_stable(env e) { + // uint256 totalStableDebt; + uint256 _totalVariableDebt; + uint256 _currentVariableBorrowRate; + uint256 _currentAverageStableBorrowRate; + + require 10^23 <= _currentVariableBorrowRate && _currentVariableBorrowRate <= 10^27; + require _totalVariableDebt * 10^9 >= 10^27 / 2; + + uint256 _ret_val = _getOverallBorrowRate(e, + 0, // no stable debt + _totalVariableDebt, + _currentVariableBorrowRate, + _currentAverageStableBorrowRate); + + assert _ret_val == _currentVariableBorrowRate; + +} + + + +/* +0x27abba297a2388bdb0599e976abf +0x27abba297a2389cfe04891b00000 +*/ diff --git a/certora/solvency/specs/Aave/.VariableDebtTokenCheck.spec b/certora/solvency/specs/Aave/.VariableDebtTokenCheck.spec new file mode 100644 index 00000000..88d657c7 --- /dev/null +++ b/certora/solvency/specs/Aave/.VariableDebtTokenCheck.spec @@ -0,0 +1,145 @@ +import "./aToken.spec"; +import "./PoolSummarizationForTokens.spec"; +import "./IncentivesControllerForTokens.spec"; + +import "../generic.spec"; +import "./tokenCheckBase.spec"; + +using VariableDebtTokenInstance as aToken; +using Utilities as utils; + +methods { + // envfree declarations + function utils.nop() external envfree; + function aToken.scaledBalanceOf(address) external returns uint256 envfree; + function aToken.scaledTotalSupply() external returns uint256 envfree; + function aToken.allowance(address,address) external returns uint256 envfree; + // note that balanceOf and totalSupply are not envfree because the calls to the pool are in fact not envfree, + // but our current envfree checks are not precise enough for this +} + +function not_implemented() { + assert false, "Not implemented"; +} + +function dont_care() { + require false; // to be used for methods we don't model in the summary version anyway +} + +definition initialize_method_sig() returns uint32 = sig:initialize(address,address,address,uint8,string,string,bytes).selector; + +// xxx currently missing return value equivalence checks +function run_parametric_with_cvl_equivalent(method f, env e) { + // note there's a strong assumption the side effects of CVL and Solidity versions are disjoint + if (f.selector == sig:transfer(address,uint256).selector) { + address a1; + uint256 u1; + aTokenTransferCVL(aToken, a1, u1, e); + aToken.transfer(e, a1, u1); + } else if (f.selector == sig:transferFrom(address,address,uint256).selector) { + address a1; + address a2; + uint256 u1; + // must have the same allowance at the beginning to have the same effect + require allowanceByToken[aToken][a1][e.msg.sender] == aToken.allowance(e, a1, e.msg.sender); + aTokenTransferFromCVL(aToken, a1, a2, u1, e); + aToken.transferFrom(e, a1, a2, u1); + } else if (f.selector == sig:approve(address,uint256).selector) { + address a1; + uint256 u1; + approveCVL(aToken, e.msg.sender, a1, u1); + aToken.approve(e, a1, u1); + } else if (f.selector == sig:decreaseAllowance(address,uint256).selector) { + address a1; + uint256 u1; + decreaseAllowanceCVL(aToken, e.msg.sender, a1, u1); + aToken.decreaseAllowance(e, a1, u1); + } else if (f.selector == sig:increaseAllowance(address,uint256).selector) { + address a1; + uint256 u1; + increaseAllowanceCVL(aToken, e.msg.sender, a1, u1); + aToken.increaseAllowance(e, a1, u1); + } else if (f.selector == sig:mint(address,address,uint256,uint256).selector) { + address a1; + address a2; + uint256 u1; + uint256 u2; + aTokenMintCVL(aToken, a1, a2, u1, u2); + aToken.mint(e, a1, a2, u1, u2); + } else if (f.selector == sig:burn(address,uint256,uint256).selector) { + address a1; + uint256 u1; + uint256 u2; + variableDebtBurnCVL(aToken, a1, u1, u2); + aToken.burn(e, a1, u1, u2); + } else if (f.selector == sig:setIncentivesController(address).selector) { + address a1; + // no CVL implementation, so nop + aToken.setIncentivesController(e, a1); + } else if (f.selector == initialize_method_sig()) { + // we're running all of our equivalence rules on an 'initialized' state of the AToken + dont_care(); + } else if (f.selector == sig:approveDelegation(address,uint256).selector) { + address a1; + uint256 u1; + // no CVL implementation, so nop + aToken.approveDelegation(e, a1, u1); // sol implementation to see if it's nop-equivalent for our purposes + } else if (f.selector == sig:delegationWithSig(address,address,uint256,uint256,uint8,bytes32,bytes32).selector) { + address a1; + address a2; + uint256 u1; + uint256 u2; + uint8 u3; + bytes32 b1; + bytes32 b2; + // no CVL implementation, so nop + aToken.delegationWithSig(e, a1, a2, u1, u2, u3, b1, b2); + } else { + not_implemented(); + } +} + +hook Sstore _underlyingAsset address newValue { + aTokenToUnderlying[aToken] = newValue; +} + +invariant aTokenUnderlyingMatchesGhost() + aToken._underlyingAsset == aTokenToUnderlying[aToken] + filtered { f -> f.contract == currentContract } + + +// some methods are not implemented, let's find them so we could omit them from other rules +definition unimplemented(method f) returns bool = + f.selector == sig:allowance(address,address).selector + || f.selector == sig:approve(address,uint256).selector + || f.selector == sig:decreaseAllowance(address,uint256).selector + || f.selector == sig:increaseAllowance(address,uint256).selector + || f.selector == sig:transfer(address,uint256).selector + || f.selector == sig:transferFrom(address,address,uint256).selector +; + +use rule alwaysRevert filtered { f -> + f.contract == currentContract + && unimplemented(f) +} + +use builtin rule sanity filtered { f -> f.contract == currentContract && !unimplemented(f) } + +function init_state_invariants() { + requireInvariant aTokensAreNotUnderlyings(); + requireInvariant aTokenUnderlyingMatchesGhost(); + require currentATokenHasAnUnderlying(); // see definition for justification + require tokenToSort[aToken] == VariableDebtToken_token(); + require forall address a. tokenToSort[a] < 3; // ignore stable tokens for now... xxx +} + + +use rule balanceOfEquivalence; +use rule scaledBalanceOfEquivalence; +use rule totalSupplyEquivalence; +use rule scaledTotalSupplyEquivalence; +use rule allowanceEquivalence; +use rule listOtherViewFunctions; +use invariant aTokensAreNotUnderlyings; +use rule currentATokenHasAnUnderlyingAfterInitiailization; +use rule aTokenWithoutUnderlyingIsERC20AfterInitialization; \ No newline at end of file diff --git a/certora/solvency/specs/Aave/.tokenCheckBase.spec b/certora/solvency/specs/Aave/.tokenCheckBase.spec new file mode 100644 index 00000000..cc17a1f9 --- /dev/null +++ b/certora/solvency/specs/Aave/.tokenCheckBase.spec @@ -0,0 +1,196 @@ +// Shared rules for AToken, VariableDebtToken, and StableDebtToken equivalence checks + +// balanceOf equivalence +rule balanceOfEquivalence(method f) +filtered { f -> f.contract == currentContract && !f.isView } +{ + env e; + init_state_invariants(); + address user; + // this is actually not enough to check the balanceOf getters, because + // of rounding that can cause divergence after updates. + // here, we must resort to make sure that the ghost state matches the + // low-level state of the AToken. So the proof is a bit more 'specific' + // per AToken, but that is okay. + // specifically: + require forall address a. to_mathint(aToken._userState[a].balance) == to_mathint(balanceByToken[aToken][a]); + uint _contractVal = aToken.balanceOf(e, user); + uint _specVal = aTokenBalanceOfCVL(aToken, user, e); + // given the low-level require, we can assert equivalence already here! + assert _contractVal == _specVal; + + run_parametric_with_cvl_equivalent(f, e); + + assert forall address a. to_mathint(aToken._userState[a].balance) == to_mathint(balanceByToken[aToken][a]); + + uint contractVal_ = aToken.balanceOf(e, user); + uint specVal_ = aTokenBalanceOfCVL(aToken, user, e); + assert contractVal_ == specVal_; +} + +rule scaledBalanceOfEquivalence(method f) +filtered { f -> f.contract == currentContract && !f.isView } +{ + env e; + init_state_invariants(); + address user; + uint _contractVal = aToken.scaledBalanceOf(user); + uint _specVal = scaledBalanceOfCVL(aToken, user); + // this is proven in balanceOfEquivalence + require forall address a. to_mathint(aToken._userState[a].balance) == to_mathint(balanceByToken[aToken][a]); + assert _contractVal == _specVal; + + run_parametric_with_cvl_equivalent(f, e); + + uint contractVal_ = aToken.scaledBalanceOf(user); + uint specVal_ = scaledBalanceOfCVL(aToken, user); + assert contractVal_ == specVal_; +} + +// totalSupply equivalence +rule totalSupplyEquivalence(method f) +filtered { f -> f.contract == currentContract && !f.isView } +{ + env e; + init_state_invariants(); + // this is actually not enough to check the totalSupply getters, because + // of rounding that can cause divergence after updates. + // here, we must resort to make sure that the ghost state matches the + // low-level state of the AToken. So the proof is a bit more 'specific' + // per AToken, but that is okay. + // specifically: + require to_mathint(aToken._totalSupply) == to_mathint(totalSupplyByToken[aToken]); + uint _contractVal = aToken.totalSupply(e); + uint _specVal = aTokenTotalSupplyCVL(aToken, e); + // given the low-level require, we can assert equivalence already here! + assert _contractVal == _specVal; + + run_parametric_with_cvl_equivalent(f, e); + + assert to_mathint(aToken._totalSupply) == to_mathint(totalSupplyByToken[aToken]); + + uint contractVal_ = aToken.totalSupply(e); + uint specVal_ = aTokenTotalSupplyCVL(aToken, e); + assert contractVal_ == specVal_; +} + +rule scaledTotalSupplyEquivalence(method f) +filtered { f -> f.contract == currentContract && !f.isView } +{ + env e; + init_state_invariants(); + // this is actually not enough to check the totalSupply getters, because + // of rounding that can cause divergence after updates. + // here, we must resort to make sure that the ghost state matches the + // low-level state of the AToken. So the proof is a bit more 'specific' + // per AToken, but that is okay. + // specifically: + require to_mathint(aToken._totalSupply) == to_mathint(totalSupplyByToken[aToken]); + + uint _contractVal = aToken.scaledTotalSupply(); + uint _specVal = scaledTotalSupplyCVL(aToken); + assert _contractVal == _specVal; + + run_parametric_with_cvl_equivalent(f, e); + + assert to_mathint(aToken._totalSupply) == to_mathint(totalSupplyByToken[aToken]); + + + uint contractVal_ = aToken.scaledTotalSupply(); + uint specVal_ = scaledTotalSupplyCVL(aToken); + assert contractVal_ == specVal_; +} + +// allowance equivalence +rule allowanceEquivalence(method f) +filtered { f -> f.contract == currentContract && !f.isView } +{ + env e; + init_state_invariants(); + address owner; + address spender; + uint _contractVal = aToken.allowance(owner, spender); + uint _specVal = allowanceCVL(aToken, owner, spender); + require _contractVal == _specVal; + + run_parametric_with_cvl_equivalent(f, e); + + uint contractVal_ = aToken.allowance(owner, spender); + uint specVal_ = allowanceCVL(aToken, owner, spender); + assert contractVal_ == specVal_; +} + +rule listOtherViewFunctions(method viewF) +filtered { viewF -> + viewF.contract == currentContract + && viewF.isView + && viewF.selector != sig:balanceOf(address).selector + && viewF.selector != sig:scaledBalanceOf(address).selector + && viewF.selector != sig:totalSupply().selector + && viewF.selector != sig:allowance(address,address).selector +} { + env e; + calldataarg arg; + viewF(e, arg); + assert true; +} + +// Expected failure in `initialize`, as there are no checks that we assign a legal underlying. (e.g. assigning an AToken's underlying as itself) +invariant aTokensAreNotUnderlyings() + forall address a. + a == 0 // nothing-token + || aTokenToUnderlying[a] == 0 // underlying + || aTokenToUnderlying[aTokenToUnderlying[a]] == 0 // aTokens map to underlyings which map to 0 + filtered { f -> f.contract == currentContract + // omit initialize function, we know initialization violates it + && f.selector != initialize_method_sig() + } + +// quasi-invariant, it must hold after initialize() was called on the AToken +definition currentATokenHasAnUnderlying() returns bool = + aTokenToUnderlying[aToken] != 0; + +rule currentATokenHasAnUnderlyingAfterInitiailization(method f) +filtered { f -> f.contract == currentContract && f.selector != initialize_method_sig() } +{ + require currentATokenHasAnUnderlying(); + env e; + calldataarg arg; + f(e, arg); + assert currentATokenHasAnUnderlying(); +} + +// quase-invariant. ERC20 tokens don't have an underlying, AToken/VarDebt/StableDebt must have an underlying. +// it must hold after initialize() was called. +// (though if it were possible, re-calling it can nullify the underlying) +definition aTokenWithoutUnderlyingIsERC20(address token) returns bool = + aTokenToUnderlying[token] == 0 <=> tokenToSort[token] == 0; + +rule aTokenWithoutUnderlyingIsERC20AfterInitialization(method f) +filtered { f -> f.contract == currentContract && f.selector != initialize_method_sig() } +{ + address token; + require aTokenWithoutUnderlyingIsERC20(token); + env e; + calldataarg arg; + f(e, arg); + assert aTokenWithoutUnderlyingIsERC20(token); +} + +// xxx all those initialize() failing rules could be prettified if we had an "initialized" +// predicate + +// use-me block +/** + +use rule balanceOfEquivalence; +use rule scaledBalanceOfEquivalence; +use rule totalSupplyEquivalence; +use rule scaledTotalSupplyEquivalence; +use rule allowanceEquivalence; +use rule listOtherViewFunctions; +use invariant aTokensAreNotUnderlyings; +use rule currentATokenHasAnUnderlyingAfterInitiailization; +use rule aTokenWithoutUnderlyingIsERC20AfterInitialization; + + */ \ No newline at end of file diff --git a/certora/solvency/specs/README.txt b/certora/solvency/specs/README.txt new file mode 100644 index 00000000..5d1e32f0 --- /dev/null +++ b/certora/solvency/specs/README.txt @@ -0,0 +1,31 @@ + +The property that we prove in this directory is the following solvency invariant (for an arbitrary asset): + (*) Atoken.totalSupply() <= VariableDebtToken.totalSupply() + virtual_balance + +Intuitively, the left hand side is the amount the the pool owes to its users, and the right hand +side is the amount it has (either in hands - the virtual_balance, or what people owe to it - +the VariableDebtToken.totalSupply()) (*) should be proved for the following case: +1. A function call: for example supply, withdraw, borrow, repay, repayWithATokens ... +2. Time passing (without any function being called). This is relevant because the indexes increase + with the time, hence the amounts that appear in (*) + +Note that: +1. The above isn't a real invariant. It can be violated due to rounding errors. What we really prove + is that the left-hand-side minus right-hand-side of (*) can't increase by more than the index + (in RAY units) after each function call. (it is either the liquidity-index or the variableBorrow-index + depending on the specific function call. + +2. The above is proved under the following assumptions: + a. The pool uses virtual accounting for the asset. + b. The asset uses only variable-debt interest (and not stable-debt). Moreover we assume that + StableDebtToken.totalSupply()==0. (Aave is going in that direction.) + c. RAY <= liquidity-index && RAY <= borrow-index. (this should be easy to prove) + d. Atoken.totalSupply() <= RAY. If this assumption is removed, we either get a timeout or an error. + (I suspect that the error is due to an imprecision RAY-calculation with such big numbers, but + haven't checked it yet.) + +3. We deal with each function in a seperate file. For some functions (repay, repayWithATokens) we + dedicate a sub-directory, because the proof is more involved and contains lemmas or case splits. + + + diff --git a/certora/solvency/specs/borrow.spec b/certora/solvency/specs/borrow.spec new file mode 100644 index 00000000..a74ea3b3 --- /dev/null +++ b/certora/solvency/specs/borrow.spec @@ -0,0 +1,75 @@ + +// aave imports +import "AUX/CVLMocks/aToken.spec"; +import "AUX/CVLMocks/AddressProvider.spec"; + + +import "common/optimizations.spec"; +import "common/functions.spec"; +import "common/validation_functions.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + + +/*===================================================================================== + Rule: solvency__borrow + Status: PASS + Link: https://prover.certora.com/output/66114/57d6a2730b3f4fd5ba9976a35f394324/?anonymousKey=743a5d838a12d3fd2dd4577c82d080a66a095d88 + =====================================================================================*/ +rule solvency__borrow(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + //THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a failure. + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + uint256 _amount; uint256 _interestRateMode; address onBehalfOf; uint16 referralCode; + require _interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + borrow(e, _asset, _amount, _interestRateMode, referralCode, onBehalfOf); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert __totSUP_aToken__ <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.variableBorrowIndex / RAY() ; +} diff --git a/certora/solvency/specs/common/functions.spec b/certora/solvency/specs/common/functions.spec new file mode 100644 index 00000000..701f67ab --- /dev/null +++ b/certora/solvency/specs/common/functions.spec @@ -0,0 +1,47 @@ + + +methods { + function _.ADDRESSES_PROVIDER() external => NONDET; // expect address + + function getReserveDataExtended(address) external returns (DataTypes.ReserveData memory) envfree; + function getReserveAddressById(uint16 id) external returns (address) envfree; + function getReservesList() external returns (address[]) envfree; + + function rayMul(uint256,uint256) external returns (uint256) envfree; + function rayDiv(uint256,uint256) external returns (uint256) envfree; +} + + + + +function isVirtualAccActive(uint256 data) returns bool { + uint mask = 0xEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + return (data & ~mask) != 0; +} + +function init_state() { + // based on aTokensAreNotUnderlyings + require forall address a. + a == 0 // nothing-token + || aTokenToUnderlying[a] == 0 // underlying + || aTokenToUnderlying[aTokenToUnderlying[a]] == 0 // aTokens map to underlyings which map to 0 + ; + // aTokens have the AToken sort, VariableDebtTokens have the VariableDebt sort, etc... + require forall address a. tokenToSort[currentContract._reserves[a].aTokenAddress] == AToken_token(); + require forall address a. tokenToSort[currentContract._reserves[a].variableDebtTokenAddress] == VariableDebtToken_token(); +} + +function tokens_addresses_limitations(address atoken, address variable, address asset) { + // require atoken==10; require variable==11; require asset==100; + // require weth!=10 && weth!=11 && weth!=12; + + require asset != 0; + require atoken != variable && atoken != asset; + require variable != asset; + // require weth != atoken && weth != variable && atoken != stb; + + // The asset that current rule deals with. It is used in summarization CVL-functions, and other places. + // See for example _accrueToTreasuryCVL(). + ASSET = asset; +} + diff --git a/certora/solvency/specs/common/optimizations.spec b/certora/solvency/specs/common/optimizations.spec new file mode 100644 index 00000000..84611123 --- /dev/null +++ b/certora/solvency/specs/common/optimizations.spec @@ -0,0 +1,60 @@ + + +// optimizing summaries +methods { + function ReserveLogic._accrueToTreasury( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache + ) internal => _accrueToTreasuryCVL(); + + function MathUtils.calculateCompoundedInterest( + uint256 rate,uint40 lastUpdateTimestamp, uint256 currentTimestamp + ) internal returns (uint256)=> compoundedInterestCVL(rate, lastUpdateTimestamp, currentTimestamp); + + function MathUtils.calculateLinearInterest(uint256 rate, uint40 lastUpdateTimestamp) + internal returns (uint256) with (env e) => f_linearInterestCVL(e,rate,lastUpdateTimestamp); + + function _.calculateInterestRates(DataTypes.CalculateInterestRatesParams params) external => NONDET; + + function GenericLogic._getUserDebtInBaseCurrency( + address, + DataTypes.ReserveData storage, + uint256, + uint256 + ) internal returns (uint256) => NONDET /* difficulty 106 */; + + function GenericLogic.calculateUserAccountData( + mapping (address => DataTypes.ReserveData) storage, + mapping (uint256 => address) storage, + mapping (uint8 => DataTypes.EModeCategory) storage, + DataTypes.CalculateUserAccountDataParams memory + ) internal returns (uint256,uint256,uint256,uint256,uint256,bool) => NONDET /* difficulty 214 */; + +} // END METHODS BLOCK + + + +persistent ghost linearInterestCVL(mathint,mathint,mathint) returns uint { + axiom forall mathint rate. forall mathint lastUpdateTimestamp. forall mathint currentTimestamp. + to_mathint(linearInterestCVL(rate, lastUpdateTimestamp, currentTimestamp)) == + RAY() + rate * (currentTimestamp - lastUpdateTimestamp) / (365*86400); + // the 365*86400 is SECONDS_PER_YEAR() as defined in MathUtils. +} +persistent ghost compoundedInterestCVL(mathint,mathint,mathint) returns uint { + axiom forall mathint rate. forall mathint lastUpdateTimestamp. forall mathint currentTimestamp. + to_mathint(compoundedInterestCVL(rate, lastUpdateTimestamp, currentTimestamp)) >= + to_mathint(linearInterestCVL(rate, lastUpdateTimestamp, currentTimestamp)); + // RAY() + rate * (currentTimestamp - lastUpdateTimestamp) / (365*86400); + // the 365*86400 is SECONDS_PER_YEAR() as defined in MathUtils. +} + +persistent ghost address ASSET; //The asset that current rule refers to. Should be assigned from within the rule. + +// The function _accrueToTreasury(...) only writes to the field accruedToTreasury. +function _accrueToTreasuryCVL() { + havoc currentContract._reserves[ASSET].accruedToTreasury; +} + +function f_linearInterestCVL(env e,uint256 rate,uint40 lastUpdateTimestamp) returns uint256 { + return linearInterestCVL(rate,lastUpdateTimestamp, e.block.timestamp); +} diff --git a/certora/solvency/specs/common/validation_functions.spec b/certora/solvency/specs/common/validation_functions.spec new file mode 100644 index 00000000..4659f30e --- /dev/null +++ b/certora/solvency/specs/common/validation_functions.spec @@ -0,0 +1,66 @@ + + + +// The validtion function are usually summarized to NONDET +methods { + function ValidationLogic.validateLiquidationCall( + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveData storage collateralReserve, + DataTypes.ReserveData storage debtReserve, + DataTypes.ValidateLiquidationCallParams memory params + ) internal => NONDET; + + function ValidationLogic.validateWithdraw( + DataTypes.ReserveCache memory reserveCache, + uint256 amount, + uint256 userBalance + ) internal => NONDET; + + function ValidationLogic.validateAutomaticUseAsCollateral( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveConfigurationMap memory reserveConfig, + address aTokenAddress + ) internal returns (bool) => NONDET; + + function ValidationLogic.validateSupply( + DataTypes.ReserveCache memory, + DataTypes.ReserveData storage, + uint256, + address + ) internal => NONDET; + + + function ValidationLogic.validateBorrow( + mapping (address => DataTypes.ReserveData) storage reservesData, + mapping (uint256 => address) storage reservesList, + mapping (uint8 => DataTypes.EModeCategory) storage eModeCategories, + DataTypes.ValidateBorrowParams memory params + ) internal => NONDET; + /* difficulty 331 ; */ + + function ValidationLogic.validateHealthFactor( + mapping (address => DataTypes.ReserveData) storage, + mapping (uint256 => address) storage, + mapping (uint8 => DataTypes.EModeCategory) storage, + DataTypes.UserConfigurationMap memory, + address, + uint8, + uint256, + address + ) internal returns (uint256,bool) => NONDET /* difficulty 214 */; + + function ValidationLogic.validateHFAndLtv( + mapping (address => DataTypes.ReserveData) storage, + mapping (uint256 => address) storage, + mapping (uint8 => DataTypes.EModeCategory) storage, + DataTypes.UserConfigurationMap memory, + address, + address, + uint256, + address, + uint8 + ) internal => NONDET /* difficulty 216 */; + +} diff --git a/certora/solvency/specs/cumulateToLiquidityIndexCVL-check.spec b/certora/solvency/specs/cumulateToLiquidityIndexCVL-check.spec new file mode 100644 index 00000000..0ba8c41f --- /dev/null +++ b/certora/solvency/specs/cumulateToLiquidityIndexCVL-check.spec @@ -0,0 +1,58 @@ + +// aave imports +import "AUX/CVLMocks/aToken.spec"; +import "AUX/CVLMocks/AddressProvider.spec"; + +import "common/optimizations.spec"; +import "common/functions.spec"; +import "common/validation_functions.spec"; + + +ghost mathint TOT_SUP_ATOKEN; + +methods { + // function _.rayMul(uint256 a, uint256 b) internal => rayMulCVLPrecise(a, b) expect uint256; // not optimized well by Prover + //function _.rayDiv(uint256 a, uint256 b) internal => rayDivCVLPrecise(a, b) expect uint256; // seems to be optimized well by Prover +} + + +/*===================================================================================== + Rule: check_cumulateToLiquidityIndexCVL + In the proof of solvency__flashLoanSimple we summarize the function + cumulateToLiquidityIndex(...) with its CVL's counterpart: cumulateToLiquidityIndexCVL(). + Here we check that out summarization is indeed correct. + + Status: PASS + Link: https://prover.certora.com/output/66114/398efa550ab44487bb5c958c588d9de9/?anonymousKey=9dfc18bf71c7a3d4074449a2319c2ebf558e5911 + =====================================================================================*/ +rule check_cumulateToLiquidityIndexCVL(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + require (_atoken != 0); require (_asset != 0); + uint256 __scaled = scaledTotalSupplyCVL(_atoken); + require aTokenToUnderlying[_atoken]==_asset; + + // INDEX + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + require (__liqInd_before == assert_uint256(currentContract._reserves[_asset].liquidityIndex)); + require 10^27 <= __liqInd_before; // && __liqInd_before <= 100*10^27; + + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + TOT_SUP_ATOKEN = __totSUP_aToken; + + require __totSUP_aToken <= 10^27; + + // THE FUNCTION CALL + uint256 totalLiquidity; uint256 __amount; + require __totSUP_aToken <= to_mathint(totalLiquidity) && totalLiquidity <= 10^27; + require to_mathint(__amount) <= __totSUP_aToken; + uint256 __new_index = cumulateToLiquidityIndex(e,_asset, totalLiquidity, __amount); // ******* + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + + assert to_mathint(rayMulCVLPrecise(__new_index, __scaled)) <= + TOT_SUP_ATOKEN + __amount + 2*__liqInd_before/10^27; + +} diff --git a/certora/solvency/specs/flashloan.spec b/certora/solvency/specs/flashloan.spec new file mode 100644 index 00000000..6eafcf72 --- /dev/null +++ b/certora/solvency/specs/flashloan.spec @@ -0,0 +1,129 @@ + +// aave imports +import "AUX/CVLMocks/aToken.spec"; +import "AUX/CVLMocks/AddressProvider.spec"; +import "AUX/FlashLoanReceiver.spec"; + +import "common/optimizations.spec"; +import "common/functions.spec"; +import "common/validation_functions.spec"; + +/*================================================================================================ + See the README.txt file in the solvency/ directory. + + Note that we rely on the summarization of cumulateToLiquidityIndex, that states that the + liquidity-index isn't increased too much. + + We prove that out summarization is indeed sound in the file cumulateToLiquidityIndexCVL-check.spec. + ================================================================================================*/ + +methods { + function ReserveLogic.cumulateToLiquidityIndex(DataTypes.ReserveData storage reserve, + uint256 totalLiquidity, + uint256 amount + ) internal returns uint256 with (env e) + => cumulateToLiquidityIndexCVL(e, totalLiquidity, amount); + + function ValidationLogic.validateFlashloanSimple( + DataTypes.ReserveData storage reserve, + uint256 amount + ) internal => validateFlashloanSimpleCVL(amount); +} + +ghost uint256 AMOUNT; +ghost address ATOKEN; +ghost uint128 VB; +ghost uint256 LIQUIDITY_IND_BEFORE; +ghost mathint TOT_SUP_ATOKEN; +ghost mathint TOT_SUP_DEBT; +ghost uint256 DELTA; + + +function cumulateToLiquidityIndexCVL(env e, uint256 totalLiq, uint256 amount) returns uint256 { + uint256 __liqInd_before = getReserveNormalizedIncome(e, ASSET); + assert (__liqInd_before == assert_uint256(currentContract._reserves[ASSET].liquidityIndex)); + assert to_mathint(amount) <= TOT_SUP_ATOKEN; + assert TOT_SUP_ATOKEN <= to_mathint(totalLiq); + + uint256 scaled = scaledTotalSupplyCVL(ATOKEN); + + + uint256 new_index; + require to_mathint(rayMulCVLPrecise(new_index, scaled)) <= TOT_SUP_ATOKEN + amount + 2*__liqInd_before/10^27; + + havoc currentContract._reserves[ASSET].liquidityIndex; + require currentContract._reserves[ASSET].liquidityIndex==require_uint128(new_index); + + return new_index; +} + +function validateFlashloanSimpleCVL(uint256 amount) { + require to_mathint(amount) <= TOT_SUP_ATOKEN; +} + + +/*===================================================================================== + Rule: solvency__flashLoanSimple + + Status: PASS + Link: https://prover.certora.com/output/66114/0245f79ede1044c787a1c6b834495d65/?anonymousKey=ce44cd008eb5a1e797acb2e2d9fb93f89485d2bb + =====================================================================================*/ +rule solvency__flashLoanSimple(env e, address _asset) { + init_state(); + + require FLASHLOAN_PREMIUM_TOTAL(e) <= 10000; + require FLASHLOAN_PREMIUM_TO_PROTOCOL(e) <= 10000; + + address _atoken = currentContract._reserves[_asset].aTokenAddress; ATOKEN = _atoken; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + require forall address a. balanceByToken[_atoken][a] <= totalSupplyByToken[_atoken]; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); LIQUIDITY_IND_BEFORE = __liqInd_before; + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + require __liqInd_before >= RAY() && __dbtInd_before >= RAY(); + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + TOT_SUP_ATOKEN = __totSUP_aToken; + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + TOT_SUP_DEBT = __totSUP_debt; + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + require to_mathint(__totSUP_aToken) <= to_mathint(__virtual_bal) + __totSUP_debt + DELTA; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + + // THE FUNCTION CALL + address receiverAddress; uint256 _amount; bytes params; uint16 referralCode; + AMOUNT = _amount; + VB = __virtual_bal; + flashLoanSimple(e, receiverAddress, _asset, _amount, params, referralCode); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == require_uint128(__dbtInd_before)); + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + uint256 __dbtInd_after = getReserveNormalizedVariableDebt(e, _asset); + + mathint __totSUP_aToken__; mathint __totSUP_debt__; + __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= to_mathint(__virtual_bal__) + __totSUP_debt__ + DELTA + + 2*__liqInd_before / RAY(); +} + diff --git a/certora/solvency/specs/liquidationCall/COLasset-common.spec b/certora/solvency/specs/liquidationCall/COLasset-common.spec new file mode 100644 index 00000000..7971617e --- /dev/null +++ b/certora/solvency/specs/liquidationCall/COLasset-common.spec @@ -0,0 +1,78 @@ + + +persistent ghost address _DBT_asset; persistent ghost address _DBT_atoken; persistent ghost address _DBT_debt; +persistent ghost address _COL_asset; persistent ghost address _COL_atoken; persistent ghost address _COL_debt; + + +/*================================================================================================ + Summarizations + ================================================================================================*/ +methods { + function IsolationModeLogic.updateIsolatedDebtIfIsolated( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveCache memory reserveCache, + uint256 repayAmount + ) internal => updateIsolatedDebtIfIsolatedCVL(); + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal returns (uint256,uint256,uint256,uint256) => + _calculateAvailableCollateralToLiquidateCVL(); +} + + +function _calculateAvailableCollateralToLiquidateCVL() returns (uint256,uint256,uint256,uint256) { + uint256 a; uint256 b; uint256 c; uint256 d; // require c==0; + return (a,b,c,d); +} + + +// The function updateIsolatedDebtIfIsolated(...) only writes to the field isolationModeTotalDebt. +function updateIsolatedDebtIfIsolatedCVL() { + address asset; + havoc currentContract._reserves[asset].isolationModeTotalDebt; +} + + + + +/*================================================================================================ + General Function + ================================================================================================*/ + +function tokens_addresses_limitations_LQD(address asset, address atoken, address debt, + address asset2, address atoken2, address debt2 + ) { + require asset==100; require atoken==10; require debt==11; + require asset2==200; require atoken2==20; require debt2==21; + + //require _DBT_asset!=0; require _COL_asset!=0; + //require _DBT_asset != _COL_asset; + //require _DBT_atoken != _COL_atoken; + //require _DBT_debt != _COL_debt; +} + + +function configuration() { + init_state(); + + _DBT_atoken = currentContract._reserves[_DBT_asset].aTokenAddress; + _COL_atoken = currentContract._reserves[_COL_asset].aTokenAddress; + _DBT_debt = currentContract._reserves[_DBT_asset].variableDebtTokenAddress; + _COL_debt = currentContract._reserves[_COL_asset].variableDebtTokenAddress; + tokens_addresses_limitations_LQD(_DBT_asset, _DBT_atoken, _DBT_debt, + _COL_asset, _COL_atoken, _COL_debt); + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} + diff --git a/certora/solvency/specs/liquidationCall/COLasset-lemma.spec b/certora/solvency/specs/liquidationCall/COLasset-lemma.spec new file mode 100644 index 00000000..203f8f21 --- /dev/null +++ b/certora/solvency/specs/liquidationCall/COLasset-lemma.spec @@ -0,0 +1,179 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; +import "DBTasset-common.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + +persistent ghost bool INSIDE_liquidationCall; +persistent ghost bool INSIDE_burnBadDebt; + +persistent ghost uint256 _DBT_liqIND; persistent ghost uint256 _DBT_dbtIND; +persistent ghost uint256 _COL_liqIND; persistent ghost uint256 _COL_dbtIND; + + + + +/*================================================================================================ + Summarizations + ================================================================================================*/ +methods { + //TEMPORARY !!! we remove the following + /* function LiquidationLogic._burnBadDebt( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + uint256 reservesCount, + address user + ) internal => NONDET;*/ + + function ReserveLogic.getNormalizedIncome_hook(uint256 ret_val, address aTokenAddress) + internal => getNormalizedIncome_hook_CVL(ret_val, aTokenAddress); + + function ReserveLogic.getNormalizedDebt_hook(uint256 ret_val, address aTokenAddress) + internal => getNormalizedDebt_hook_CVL(ret_val, aTokenAddress); + + function ReserveLogic._updateIndexes_hook(DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache) + internal => _updateIndexes_hook_CVL(reserveCache); +} + +function getNormalizedIncome_hook_CVL(uint256 ret_val, address aTokenAddress) { + assert INSIDE_liquidationCall && !INSIDE_burnBadDebt => aTokenAddress==_COL_atoken; + assert INSIDE_liquidationCall && !INSIDE_burnBadDebt => ret_val==_COL_liqIND; +} + +function getNormalizedDebt_hook_CVL(uint256 ret_val, address aTokenAddress) {} + +function _updateIndexes_hook_CVL(DataTypes.ReserveCache reserveCache) { + assert (!INSIDE_burnBadDebt && reserveCache.aTokenAddress == _COL_atoken) => currentContract._reserves[_COL_asset].liquidityIndex==_COL_liqIND; + assert (!INSIDE_burnBadDebt && reserveCache.aTokenAddress == _COL_atoken) => currentContract._reserves[_COL_asset].variableBorrowIndex==_COL_dbtIND; +} + + + +/*================================================================================================ + Summarizations of HOOKS function + ================================================================================================*/ +methods { + function LiquidationLogic.get_userCollateralBalance() + internal returns(uint256) => NONDET; + + function LiquidationLogic.HOOK_burnCollateralATokens_after_updateState() + internal => HOOK_burnCollateralATokens_after_updateState_CVL(); + + function LiquidationLogic.HOOK_liquidation_before_burnBadDebt() + internal with (env e) => HOOK_liquidation_before_burnBadDebt_CVL(e); + + function LiquidationLogic.HOOK_liquidation_after_burnBadDebt() + internal with (env e) => HOOK_liquidation_after_burnBadDebt_CVL(e); +} + +// This is immediately after the call to updateState for the COL token +function HOOK_burnCollateralATokens_after_updateState_CVL() { + assert currentContract._reserves[_COL_asset].liquidityIndex == _COL_liqIND; + assert currentContract._reserves[_COL_asset].variableBorrowIndex == _COL_dbtIND; +} + +persistent ghost uint256 COL_liqIND_INTR1; +persistent ghost uint256 COL_dbtIND_INTR1; +function HOOK_liquidation_before_burnBadDebt_CVL(env e) { + INSIDE_liquidationCall = false; + + COL_liqIND_INTR1 = getReserveNormalizedIncome(e, _COL_asset); + assert COL_liqIND_INTR1 == _COL_liqIND; + + COL_dbtIND_INTR1 = getReserveNormalizedVariableDebt(e, _COL_asset); + assert COL_dbtIND_INTR1 == _COL_dbtIND; + + INSIDE_burnBadDebt = true; + INSIDE_liquidationCall = true; +} + +persistent ghost uint256 COL_liqIND_INTR2; +persistent ghost uint256 COL_dbtIND_INTR2; +function HOOK_liquidation_after_burnBadDebt_CVL(env e) { + INSIDE_burnBadDebt = false; + INSIDE_liquidationCall = false; + + COL_liqIND_INTR2 = getReserveNormalizedIncome(e, _COL_asset); + assert COL_liqIND_INTR2 == COL_liqIND_INTR1; + + COL_dbtIND_INTR2 = getReserveNormalizedVariableDebt(e, _COL_asset); + assert COL_dbtIND_INTR2 == COL_dbtIND_INTR1; + + INSIDE_liquidationCall = true; +} + + + +/* +function tokens_addresses_limitations_LQD(address asset, address atoken, address debt, + address asset2, address atoken2, address debt2 + ) { + require asset==100; require atoken==10; require debt==11; + require asset2==200; require atoken2==20; require debt2==21; +} + + +function configuration() { + init_state(); + + _DBT_atoken = currentContract._reserves[_DBT_asset].aTokenAddress; + _COL_atoken = currentContract._reserves[_COL_asset].aTokenAddress; + _DBT_debt = currentContract._reserves[_DBT_asset].variableDebtTokenAddress; + _COL_debt = currentContract._reserves[_COL_asset].variableDebtTokenAddress; + tokens_addresses_limitations_LQD(_DBT_asset, _DBT_atoken, _DBT_debt, + _COL_asset, _COL_atoken, _COL_debt); + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} +*/ + +/*===================================================================================== + Rule: same_indexes__liquidationCall + =====================================================================================*/ +rule same_indexes__liquidationCall(env e) { + INSIDE_liquidationCall = false; + INSIDE_burnBadDebt = false; + configuration(); + + _DBT_liqIND = getReserveNormalizedIncome(e, _DBT_asset); + _COL_liqIND = getReserveNormalizedIncome(e, _COL_asset); + + _DBT_dbtIND = getReserveNormalizedVariableDebt(e, _DBT_asset); + _COL_dbtIND = getReserveNormalizedVariableDebt(e, _COL_asset); + + DataTypes.ReserveData reserve = getReserveDataExtended(_DBT_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_DBT_debt)!=0; // We prove that if ==0 then the call to + // liquidationCall reverts (see DBTasset-totSUP0.spec) + require scaledTotalSupplyCVL(_COL_debt)!=0; // We treat the case where ==0 in the files COLasset-totSUP0... + + // THE FUNCTION CALL + address user; uint256 debtToCover; + bool receiveAToken = true; + + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, user, debtToCover, receiveAToken); + INSIDE_liquidationCall = false; + + uint256 __COL_liqIND_after = getReserveNormalizedIncome(e, _COL_asset); + assert __COL_liqIND_after == _COL_liqIND; + + uint256 __COL_dbtIND_after = getReserveNormalizedVariableDebt(e, _COL_asset); + assert __COL_dbtIND_after == _COL_dbtIND; +} + + diff --git a/certora/solvency/specs/liquidationCall/COLasset-main.spec b/certora/solvency/specs/liquidationCall/COLasset-main.spec new file mode 100644 index 00000000..891de2dc --- /dev/null +++ b/certora/solvency/specs/liquidationCall/COLasset-main.spec @@ -0,0 +1,319 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + +persistent ghost bool INSIDE_liquidationCall; +persistent ghost bool INSIDE_burnBadDebt; + +persistent ghost address _DBT_asset; persistent ghost address _DBT_atoken; persistent ghost address _DBT_debt; + +persistent ghost address _COL_asset; persistent ghost address _COL_atoken; persistent ghost address _COL_debt; +persistent ghost uint256 _COL_liqIND {axiom _COL_liqIND >= 10^27;} +persistent ghost uint256 _COL_dbtIND {axiom _COL_dbtIND >= 10^27;} + + +persistent ghost address USER; +persistent ghost mathint DELTA; +persistent ghost mathint ORIG_totSUP_aToken; +persistent ghost mathint ORIG_totSUP_debt; +persistent ghost uint128 ORIG_VB; +persistent ghost uint128 ORIG_deficit; + +persistent ghost mathint INTR_totSUP_aToken; +persistent ghost mathint INTR_totSUP_debt; +persistent ghost uint128 INTR_VB; +persistent ghost uint128 INTR_deficit; + +persistent ghost mathint INTR2_totSUP_aToken; +persistent ghost mathint INTR2_totSUP_debt; +persistent ghost uint128 INTR2_VB; +persistent ghost uint128 INTR2_deficit; + +persistent ghost mathint FINAL_totSUP_aToken; +persistent ghost mathint FINAL_totSUP_debt; +persistent ghost uint128 FINAL_VB; +persistent ghost uint128 FINAL_deficit; + + +methods { + function IsolationModeLogic.updateIsolatedDebtIfIsolated( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveCache memory reserveCache, + uint256 repayAmount + ) internal => updateIsolatedDebtIfIsolatedCVL(); + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal returns (uint256,uint256,uint256,uint256) => + _calculateAvailableCollateralToLiquidateCVL(); + + function LiquidationLogic._burnBadDebt( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + uint256 reservesCount, + address user + ) internal with (env e) => _burnBadDebt_CVL(e); +} + +function _burnBadDebt_CVL(env e) { + INSIDE_liquidationCall = false; + + mathint curr_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + mathint curr_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + uint128 curr_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + uint128 curr_deficit = getReserveDataExtended(_COL_asset).deficit; + + if (curr_totSUP_aToken<=curr_VB + curr_totSUP_debt + curr_deficit + DELTA + _COL_liqIND / RAY()) { + havoc_all(e); + mathint after_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + mathint after_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + uint128 after_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + uint128 after_deficit = getReserveDataExtended(_COL_asset).deficit; + + require + after_totSUP_aToken <= + after_VB + after_totSUP_debt + after_deficit + DELTA + + _COL_liqIND / RAY() + _COL_dbtIND / RAY() + ; + } + INSIDE_liquidationCall = true; +} + +// This is immediately after the call to updateState for the COL token +function _calculateAvailableCollateralToLiquidateCVL() returns (uint256,uint256,uint256,uint256) { + uint256 a; uint256 b; uint256 c; uint256 d; + return (a,b,c,d); +} + + +// The function updateIsolatedDebtIfIsolated(...) only writes to the field isolationModeTotalDebt. +function updateIsolatedDebtIfIsolatedCVL() { + address asset; + havoc currentContract._reserves[asset].isolationModeTotalDebt; +} + + +/*============================================================================================== + Summarizations + ==============================================================================================*/ +methods { + function ReserveLogic.getNormalizedIncome(DataTypes.ReserveData storage reserve) + internal returns (uint256) => _COL_liqIND; + + function ReserveLogic.getNormalizedDebt(DataTypes.ReserveData storage reserve) + internal returns (uint256) => getNormalizedDebt_CVL(); +} + +function getNormalizedDebt_CVL() returns uint256 { + if (INSIDE_liquidationCall) { + uint256 any_index; + return any_index; + } + else + return _COL_liqIND; +} + + +/*================================================================================================ + Summarizations of HOOKS function + ================================================================================================*/ +methods { + function LiquidationLogic.HOOK_liquidation_before_burnCollateralATokens(uint256 actualCollateralToLiquidate) + internal with (env e) => HOOK_liquidation_before_burnCollateralATokens_CVL(e, actualCollateralToLiquidate); + + function LiquidationLogic.HOOK_liquidation_after_burnCollateralATokens(uint256 actualCollateralToLiquidate) + internal with (env e) => HOOK_liquidation_after_burnCollateralATokens_CVL(e, actualCollateralToLiquidate); + + function LiquidationLogic.HOOK_burnCollateralATokens_after_updateState() + internal => HOOK_burnCollateralATokens_after_updateState_CVL(); + + function LiquidationLogic.HOOK_liquidation_before_burnBadDebt() + internal with (env e) => HOOK_liquidation_before_burnBadDebt_CVL(e); + + function LiquidationLogic.HOOK_liquidation_after_burnBadDebt() + internal with (env e) => HOOK_liquidation_after_burnBadDebt_CVL(e); +} + +function HOOK_liquidation_before_burnCollateralATokens_CVL(env e, uint256 actualCollateralToLiquidate) { + INSIDE_liquidationCall = false; + + mathint curr_totSUP_aToken; curr_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + mathint curr_totSUP_debt; curr_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + uint128 curr_VB; curr_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + uint128 curr_deficit; curr_deficit = getReserveDataExtended(_COL_asset).deficit; + + assert ORIG_totSUP_aToken == curr_totSUP_aToken; + assert ORIG_totSUP_debt == curr_totSUP_debt; + assert ORIG_VB == curr_VB; + assert ORIG_deficit == curr_deficit; + + assert ORIG_totSUP_aToken <= ORIG_VB + ORIG_totSUP_debt + ORIG_deficit + DELTA; + + uint256 scaled = totalSupplyByToken[_COL_atoken]; uint256 IND = _COL_liqIND; + assert + to_mathint(rayMulCVLPrecise( require_uint256(scaled - rayDivCVLPrecise(actualCollateralToLiquidate,IND)), IND) ) + <= + rayMulCVLPrecise(scaled,IND) - actualCollateralToLiquidate + IND/RAY(); + + INSIDE_liquidationCall = true; +} + +function HOOK_liquidation_after_burnCollateralATokens_CVL(env e, uint256 actualCollateralToLiquidate) { + INSIDE_liquidationCall = false; + + INTR_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + INTR_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + INTR_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + INTR_deficit = getReserveDataExtended(_COL_asset).deficit; + + assert INTR_totSUP_debt == ORIG_totSUP_debt; + assert INTR_deficit == ORIG_deficit; + + assert INTR_VB == ORIG_VB - actualCollateralToLiquidate; + assert INTR_totSUP_aToken <= ORIG_totSUP_aToken - actualCollateralToLiquidate + _COL_liqIND / RAY() ; + + assert INTR_totSUP_aToken <= INTR_VB + INTR_totSUP_debt + INTR_deficit + DELTA + _COL_liqIND / RAY(); + + INSIDE_liquidationCall = true; +} + +function HOOK_burnCollateralATokens_after_updateState_CVL() { + require currentContract._reserves[_COL_asset].liquidityIndex == _COL_liqIND; + require currentContract._reserves[_COL_asset].variableBorrowIndex == _COL_dbtIND; +} + +function HOOK_liquidation_before_burnBadDebt_CVL(env e) { + INSIDE_liquidationCall = false; + + INTR2_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + INTR2_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + INTR2_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + INTR2_deficit = getReserveDataExtended(_COL_asset).deficit; + + assert INTR2_totSUP_aToken <= INTR2_VB + INTR2_totSUP_debt + INTR2_deficit + DELTA + _COL_liqIND / RAY(); + + INSIDE_liquidationCall = true; + INSIDE_burnBadDebt = true; +} + +function HOOK_liquidation_after_burnBadDebt_CVL(env e) { + INSIDE_liquidationCall = false; + INSIDE_burnBadDebt = false; + + FINAL_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + FINAL_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + FINAL_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + FINAL_deficit = getReserveDataExtended(_COL_asset).deficit; + + assert + FINAL_totSUP_aToken <= FINAL_VB + FINAL_totSUP_debt + FINAL_deficit + DELTA + + _COL_liqIND / RAY() + _COL_dbtIND / RAY(); + + INSIDE_liquidationCall = true; +} + + + + + + + + +function tokens_addresses_limitations_LQD(address asset, address atoken, address debt, + address asset2, address atoken2, address debt2 + ) { + //require asset==100; require atoken==10; require debt==11; + //require asset2==200; require atoken2==20; require debt2==21; + + require _DBT_asset!=0; require _COL_asset!=0; + + require _DBT_asset!=_COL_asset; + require _DBT_atoken != _COL_atoken; + require _DBT_debt != _COL_debt; +} + + +function configuration() { + init_state(); + + _DBT_atoken = currentContract._reserves[_DBT_asset].aTokenAddress; + _COL_atoken = currentContract._reserves[_COL_asset].aTokenAddress; + _DBT_debt = currentContract._reserves[_DBT_asset].variableDebtTokenAddress; + _COL_debt = currentContract._reserves[_COL_asset].variableDebtTokenAddress; + tokens_addresses_limitations_LQD(_DBT_asset, _DBT_atoken, _DBT_debt, + _COL_asset, _COL_atoken, _COL_debt); + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} + +/*===================================================================================== + Rule: solvency__liquidationCall + =====================================================================================*/ +rule solvency__liquidationCall_COLasset(env e) { + INSIDE_liquidationCall = false; + INSIDE_burnBadDebt = false; + configuration(); + + DataTypes.ReserveData reserve = getReserveDataExtended(_COL_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + ORIG_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + ORIG_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + ORIG_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + ORIG_deficit = getReserveDataExtended(_COL_asset).deficit; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + require ORIG_totSUP_aToken <= ORIG_VB + ORIG_totSUP_debt + ORIG_deficit + DELTA; + + require ORIG_totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_COL_debt)!=0; + require exists_debt; + + // THE FUNCTION CALL + uint256 _debtToCover; bool _receiveAToken; + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, USER, _debtToCover, _receiveAToken); + INSIDE_liquidationCall = false; + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_COL_asset); + + mathint final_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + mathint final_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + uint128 final_VB = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + uint128 final_deficit = getReserveDataExtended(_COL_asset).deficit; + + //THE ASSERTION + assert + final_totSUP_aToken <= final_VB + final_totSUP_debt + final_deficit + DELTA + + getReserveNormalizedVariableDebt(e, _COL_asset) / RAY() + + getReserveNormalizedIncome(e, _COL_asset) / RAY() + ; +} + diff --git a/certora/solvency/specs/liquidationCall/COLasset-totSUP0-lemma.spec b/certora/solvency/specs/liquidationCall/COLasset-totSUP0-lemma.spec new file mode 100644 index 00000000..0881f3ae --- /dev/null +++ b/certora/solvency/specs/liquidationCall/COLasset-totSUP0-lemma.spec @@ -0,0 +1,167 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + +methods { + function ReserveLogic.getNormalizedIncome_hook(uint256 ret_val, address aTokenAddress) + internal => getNormalizedIncome_hook_CVL(ret_val, aTokenAddress); + + // function ReserveLogic.getNormalizedDebt_hook(uint256 ret_val, address aTokenAddress) + // internal => getNormalizedDebt_hook_CVL(ret_val, aTokenAddress); + + //function ReserveLogic._updateIndexes_hook(DataTypes.ReserveData storage reserve, + // DataTypes.ReserveCache memory reserveCache) + // internal => _updateIndexes_hook_CVL(reserveCache); + + // function LiquidationLogic.HOOK_liquidation_after_updateState_DBT() + // internal => HOOK_liquidation_after_updateState_DBT_CVL(); + + function LiquidationLogic.HOOK_liquidation_after_burnCollateralATokens(uint256 actualCollateralToLiquidate) + internal => HOOK_liquidation_after_burnCollateralATokens_CVL(); + + + function IsolationModeLogic.updateIsolatedDebtIfIsolated( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveCache memory reserveCache, + uint256 repayAmount + ) internal => updateIsolatedDebtIfIsolatedCVL(); + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal returns (uint256,uint256,uint256,uint256) => + _calculateAvailableCollateralToLiquidateCVL(); +} + +function getNormalizedIncome_hook_CVL(uint256 ret_val, address aTokenAddress) { + assert INSIDE_liquidationCall => aTokenAddress==_COL_atoken; + assert INSIDE_liquidationCall => ret_val==_COL_liqIND; +} + +function getNormalizedDebt_hook_CVL(uint256 ret_val, address aTokenAddress) { + assert INSIDE_liquidationCall => aTokenAddress!=_COL_atoken; + + // assert INSIDE_liquidationCall => aTokenAddress==_DBT_atoken; + //assert INSIDE_liquidationCall => ret_val==_DBT_dbtIND; +} + +function _updateIndexes_hook_CVL(DataTypes.ReserveCache reserveCache) { + //assert reserveCache.aTokenAddress == _DBT_atoken => currentContract._reserves[_DBT_asset].liquidityIndex==_DBT_liqIND; + //assert reserveCache.aTokenAddress == _DBT_atoken => currentContract._reserves[_DBT_asset].variableBorrowIndex==_DBT_dbtIND; + + assert reserveCache.aTokenAddress == _COL_atoken => currentContract._reserves[_COL_asset].liquidityIndex==_COL_liqIND; + // assert reserveCache.aTokenAddress == _COL_atoken => currentContract._reserves[_COL_asset].variableBorrowIndex==_COL_dbtIND; +} + +// This is immediately after the call to updateState for the DBT token +//function HOOK_liquidation_after_updateState_DBT_CVL() { +// assert currentContract._reserves[_DBT_asset].liquidityIndex == _DBT_liqIND; +// assert currentContract._reserves[_DBT_asset].variableBorrowIndex == _DBT_dbtIND; +//} + +// This is immediately after the call to updateState for the COL token +function HOOK_liquidation_after_burnCollateralATokens_CVL() { + assert currentContract._reserves[_COL_asset].liquidityIndex == _COL_liqIND; + // assert currentContract._reserves[_COL_asset].variableBorrowIndex == _COL_dbtIND; +} + + +persistent ghost bool INSIDE_liquidationCall; + +persistent ghost address _DBT_asset; persistent ghost address _DBT_atoken; persistent ghost address _DBT_debt; +persistent ghost uint256 _DBT_liqIND; persistent ghost uint256 _DBT_dbtIND; + +persistent ghost address _COL_asset; persistent ghost address _COL_atoken; persistent ghost address _COL_debt; +persistent ghost uint256 _COL_liqIND; persistent ghost uint256 _COL_dbtIND; + + +function _calculateAvailableCollateralToLiquidateCVL() returns (uint256,uint256,uint256,uint256) { + uint256 a; uint256 b; uint256 c; uint256 d; // require c==0; + return (a,b,c,d); +} + + +// The function updateIsolatedDebtIfIsolated(...) only writes to the field isolationModeTotalDebt. +function updateIsolatedDebtIfIsolatedCVL() { + address asset; + havoc currentContract._reserves[asset].isolationModeTotalDebt; +} + + +function tokens_addresses_limitations_LQD(address asset, address atoken, address debt, + address asset2, address atoken2, address debt2 + ) { + require asset==100; require atoken==10; require debt==11; + require asset2==200; require atoken2==20; require debt2==21; +} + + +function configuration() { + init_state(); + + _DBT_atoken = currentContract._reserves[_DBT_asset].aTokenAddress; + _COL_atoken = currentContract._reserves[_COL_asset].aTokenAddress; + _DBT_debt = currentContract._reserves[_DBT_asset].variableDebtTokenAddress; + _COL_debt = currentContract._reserves[_COL_asset].variableDebtTokenAddress; + tokens_addresses_limitations_LQD(_DBT_asset, _DBT_atoken, _DBT_debt, + _COL_asset, _COL_atoken, _COL_debt); + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} + + + +/*===================================================================================== + Rule: same_indexes__liquidationCall + =====================================================================================*/ +rule same_indexes__liquidationCall(env e) { + INSIDE_liquidationCall = false; + configuration(); + + _DBT_liqIND = getReserveNormalizedIncome(e, _DBT_asset); + _COL_liqIND = getReserveNormalizedIncome(e, _COL_asset); + + _DBT_dbtIND = getReserveNormalizedVariableDebt(e, _DBT_asset); + _COL_dbtIND = getReserveNormalizedVariableDebt(e, _COL_asset); + + DataTypes.ReserveData reserve = getReserveDataExtended(_DBT_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_DBT_debt)!=0; + //require scaledTotalSupplyCVL(_COL_debt)!=0; + + // THE FUNCTION CALL + address user; uint256 debtToCover; bool receiveAToken; + + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, user, debtToCover, receiveAToken); + INSIDE_liquidationCall = false; + + uint256 __COL_liqIND_after = getReserveNormalizedIncome(e, _COL_asset); + assert __COL_liqIND_after == _COL_liqIND; + + // uint256 __COL_dbtIND_after = getReserveNormalizedVariableDebt(e, _COL_asset); + //assert __COL_dbtIND_after == _COL_dbtIND; +} + + diff --git a/certora/solvency/specs/liquidationCall/COLasset-totSUP0.spec b/certora/solvency/specs/liquidationCall/COLasset-totSUP0.spec new file mode 100644 index 00000000..7a5ab609 --- /dev/null +++ b/certora/solvency/specs/liquidationCall/COLasset-totSUP0.spec @@ -0,0 +1,146 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + +methods { + function ReserveLogic.getNormalizedIncome(DataTypes.ReserveData storage reserve) + internal returns (uint256) => _COL_liqIND; + + function LiquidationLogic.HOOK_liquidation_after_burnCollateralATokens(uint256 actualCollateralToLiquidate) + internal => HOOK_liquidation_after_burnCollateralATokens_CVL(); + + + function IsolationModeLogic.updateIsolatedDebtIfIsolated( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveCache memory reserveCache, + uint256 repayAmount + ) internal => updateIsolatedDebtIfIsolatedCVL(); + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal returns (uint256,uint256,uint256,uint256) => + _calculateAvailableCollateralToLiquidateCVL(); +} + +// This is immediately after the call to updateState for the COL token +function HOOK_liquidation_after_burnCollateralATokens_CVL() { + require currentContract._reserves[_COL_asset].liquidityIndex == _COL_liqIND; + // require currentContract._reserves[_COL_asset].variableBorrowIndex == _COL_dbtIND; +} + +function _calculateAvailableCollateralToLiquidateCVL() returns (uint256,uint256,uint256,uint256) { + uint256 a; uint256 b; uint256 c; uint256 d; // require c==0; + return (a,b,c,d); +} + +// The function updateIsolatedDebtIfIsolated(...) only writes to the field isolationModeTotalDebt. +function updateIsolatedDebtIfIsolatedCVL() { + address asset; + havoc currentContract._reserves[asset].isolationModeTotalDebt; +} + + +persistent ghost bool INSIDE_liquidationCall; + +persistent ghost address _DBT_asset; persistent ghost address _DBT_atoken; persistent ghost address _DBT_debt; +persistent ghost uint256 _DBT_liqIND; +persistent ghost uint256 _DBT_dbtIND; + +persistent ghost address _COL_asset; persistent ghost address _COL_atoken; persistent ghost address _COL_debt; +persistent ghost uint256 _COL_liqIND {axiom _COL_liqIND >= 10^27;} +persistent ghost uint256 _COL_dbtIND; + + + +function tokens_addresses_limitations_LQD(address asset, address atoken, address debt, + address asset2, address atoken2, address debt2 + ) { + require asset==100; require atoken==10; require debt==11; + require asset2==200; require atoken2==20; require debt2==21; +} + + +function configuration() { + init_state(); + + _DBT_atoken = currentContract._reserves[_DBT_asset].aTokenAddress; + _COL_atoken = currentContract._reserves[_COL_asset].aTokenAddress; + _DBT_debt = currentContract._reserves[_DBT_asset].variableDebtTokenAddress; + _COL_debt = currentContract._reserves[_COL_asset].variableDebtTokenAddress; + tokens_addresses_limitations_LQD(_DBT_asset, _DBT_atoken, _DBT_debt, + _COL_asset, _COL_atoken, _COL_debt); + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} + + + +/*===================================================================================== + Rule: solvency__liquidationCall_totDebt_of_COLasset_EQ_0 + =====================================================================================*/ +rule solvency__liquidationCall_totDebt_of_COLasset_EQ_0(env e) { + INSIDE_liquidationCall = false; + configuration(); + + DataTypes.ReserveData reserve = getReserveDataExtended(_COL_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + //uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal //+ __totSUP_debt + CONST; + ; + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + require scaledTotalSupplyCVL(_COL_debt)==0; + assert __totSUP_debt==0; + + // THE FUNCTION CALL + require _COL_asset != _DBT_asset; + address _user; uint256 _debtToCover; bool _receiveAToken; + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, _user, _debtToCover, _receiveAToken); + INSIDE_liquidationCall = false; + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_COL_asset); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_COL_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_COL_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_COL_asset).virtualUnderlyingBalance; + + assert __totSUP_debt__==0; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ /*+ __totSUP_debt__*/ // + CONST + + reserve2.liquidityIndex / RAY() + ; +} + + + + + + diff --git a/certora/solvency/specs/liquidationCall/DBTasset-common.spec b/certora/solvency/specs/liquidationCall/DBTasset-common.spec new file mode 100644 index 00000000..e5c90e0b --- /dev/null +++ b/certora/solvency/specs/liquidationCall/DBTasset-common.spec @@ -0,0 +1,74 @@ + + +persistent ghost address _DBT_asset; persistent ghost address _DBT_atoken; persistent ghost address _DBT_debt; +persistent ghost address _COL_asset; persistent ghost address _COL_atoken; persistent ghost address _COL_debt; + + +/*================================================================================================ + Summarizations + ================================================================================================*/ +methods { + function IsolationModeLogic.updateIsolatedDebtIfIsolated( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveCache memory reserveCache, + uint256 repayAmount + ) internal => updateIsolatedDebtIfIsolatedCVL(); + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal returns (uint256,uint256,uint256,uint256) => + _calculateAvailableCollateralToLiquidateCVL(); +} + + +/*================================================================================================ + Functions for summarizations + ================================================================================================*/ +function _calculateAvailableCollateralToLiquidateCVL() returns (uint256,uint256,uint256,uint256) { + uint256 a; uint256 b; uint256 c; uint256 d; + return (a,b,c,d); +} + +// The function updateIsolatedDebtIfIsolated(...) only writes to the field isolationModeTotalDebt. +function updateIsolatedDebtIfIsolatedCVL() { + address asset; + havoc currentContract._reserves[asset].isolationModeTotalDebt; +} + + + +/*================================================================================================ + General Function + ================================================================================================*/ +function tokens_addresses_limitations_LQD() { + // _DBT_asset=100; _DBT_atoken=10; _DBT_debt=11; + // _COL_asset=200; _COL_atoken=20; _COL_debt=21; + + require _DBT_asset!=0; require _COL_asset!=0; + + require _DBT_asset!=_COL_asset; + require _DBT_atoken != _COL_atoken; + require _DBT_debt != _COL_debt; +} + +function configuration() { + init_state(); + tokens_addresses_limitations_LQD(); + + require currentContract._reserves[_DBT_asset].aTokenAddress == _DBT_atoken; + require currentContract._reserves[_COL_asset].aTokenAddress == _COL_atoken; + require currentContract._reserves[_DBT_asset].variableDebtTokenAddress == _DBT_debt; + require currentContract._reserves[_COL_asset].variableDebtTokenAddress == _COL_debt; + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} diff --git a/certora/solvency/specs/liquidationCall/DBTasset-lemma.spec b/certora/solvency/specs/liquidationCall/DBTasset-lemma.spec new file mode 100644 index 00000000..d0d946f8 --- /dev/null +++ b/certora/solvency/specs/liquidationCall/DBTasset-lemma.spec @@ -0,0 +1,92 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; +import "DBTasset-common.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + +persistent ghost bool INSIDE_liquidationCall; +persistent ghost bool INSIDE_burnBadDebt; + +persistent ghost uint256 _DBT_liqIND; persistent ghost uint256 _DBT_dbtIND; + + + +/*================================================================================================ + Summarizations of HOOKS function + ================================================================================================*/ +methods { + function ReserveLogic.getNormalizedIncome_hook(uint256 ret_val, address aTokenAddress) + internal => getNormalizedIncome_hook_CVL(ret_val, aTokenAddress); + + function ReserveLogic.getNormalizedDebt_hook(uint256 ret_val, address aTokenAddress) + internal => getNormalizedDebt_hook_CVL(ret_val, aTokenAddress); + + function LiquidationLogic.HOOK_liquidation_after_updateState_DBT() + internal => HOOK_liquidation_after_updateState_DBT_CVL(); + + function LiquidationLogic.HOOK_liquidation_before_burnBadDebt() + internal with (env e) => HOOK_liquidation_before_burnBadDebt_CVL(e); + + function LiquidationLogic.HOOK_liquidation_after_burnBadDebt() + internal with (env e) => HOOK_liquidation_after_burnBadDebt_CVL(e); +} + + +function getNormalizedIncome_hook_CVL(uint256 ret_val, address aTokenAddress) {} + +function getNormalizedDebt_hook_CVL(uint256 ret_val, address aTokenAddress) { + assert (INSIDE_liquidationCall && !INSIDE_burnBadDebt) => aTokenAddress==_DBT_atoken; + assert (INSIDE_liquidationCall && !INSIDE_burnBadDebt) => ret_val==_DBT_dbtIND; +} + + +// This is immediately after the call to updateState for the DBT token +function HOOK_liquidation_after_updateState_DBT_CVL() { + assert currentContract._reserves[_DBT_asset].liquidityIndex == _DBT_liqIND; + assert currentContract._reserves[_DBT_asset].variableBorrowIndex == _DBT_dbtIND; +} + +function HOOK_liquidation_before_burnBadDebt_CVL(env e) {INSIDE_burnBadDebt = true;} +function HOOK_liquidation_after_burnBadDebt_CVL(env e) {INSIDE_burnBadDebt = false;} + + + +/*===================================================================================== + Rule: same_indexes__liquidationCall + =====================================================================================*/ +rule same_indexes__liquidationCall(env e) { + INSIDE_liquidationCall = false; + INSIDE_burnBadDebt = false; + configuration(); + + _DBT_liqIND = getReserveNormalizedIncome(e, _DBT_asset); + _DBT_dbtIND = getReserveNormalizedVariableDebt(e, _DBT_asset); + + DataTypes.ReserveData reserve = getReserveDataExtended(_DBT_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_DBT_debt)!=0; + + // THE FUNCTION CALL + address user; uint256 debtToCover; bool receiveAToken; + + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, user, debtToCover, receiveAToken); + INSIDE_liquidationCall = false; + + uint256 __DBT_liqIND_after = getReserveNormalizedIncome(e, _DBT_asset); + assert __DBT_liqIND_after == _DBT_liqIND; + + uint256 __DBT_dbtIND_after = getReserveNormalizedVariableDebt(e, _DBT_asset); + assert __DBT_dbtIND_after == _DBT_dbtIND; +} diff --git a/certora/solvency/specs/liquidationCall/DBTasset-main.spec b/certora/solvency/specs/liquidationCall/DBTasset-main.spec new file mode 100644 index 00000000..16f8a24e --- /dev/null +++ b/certora/solvency/specs/liquidationCall/DBTasset-main.spec @@ -0,0 +1,301 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; +import "DBTasset-common.spec"; + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + +persistent ghost bool INSIDE_liquidationCall; +persistent ghost bool INSIDE_burnBadDebt; + +persistent ghost bool HASNoCollateralLeft; +persistent ghost uint256 ACTUAL; +persistent ghost uint256 USER_RESERVE_DEBT; + +persistent ghost uint256 _DBT_liqIND {axiom _DBT_liqIND >= 10^27;} +persistent ghost uint256 _DBT_dbtIND {axiom _DBT_dbtIND >= 10^27;} + + +persistent ghost mathint DELTA; +persistent ghost mathint ORIG_totSUP_aToken; +persistent ghost mathint ORIG_totSUP_debt; +persistent ghost uint128 ORIG_VB; +persistent ghost uint128 ORIG_deficit; + +persistent ghost mathint INTR_totSUP_aToken; +persistent ghost mathint INTR_totSUP_debt; +persistent ghost uint128 INTR_VB; +persistent ghost uint128 INTR_deficit; + +persistent ghost mathint INTR2_totSUP_aToken; +persistent ghost mathint INTR2_totSUP_debt; +persistent ghost uint128 INTR2_VB; +persistent ghost uint128 INTR2_deficit; + +persistent ghost mathint INTR3_totSUP_aToken; +persistent ghost mathint INTR3_totSUP_debt; +persistent ghost uint128 INTR3_VB; +persistent ghost uint128 INTR3_deficit; + + +/*================================================================================================ + Summarizations + ================================================================================================*/ +methods { + function ReserveLogic.getNormalizedIncome(DataTypes.ReserveData storage reserve) + internal returns (uint256) => getNormalizedIncome_CVL(); + + function ReserveLogic.getNormalizedDebt(DataTypes.ReserveData storage reserve) + internal returns (uint256) => getNormalizedDebt_CVL(); + + function LiquidationLogic._burnBadDebt( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + uint256 reservesCount, + address user + ) internal with (env e) => _burnBadDebt_CVL(e); +} + +function getNormalizedIncome_CVL() returns uint256 { + if (INSIDE_liquidationCall) { + uint256 col_index; + return col_index; + } + else + return _DBT_liqIND; +} + +function getNormalizedDebt_CVL() returns uint256 { + if (!INSIDE_burnBadDebt) + return _DBT_dbtIND; + else { + uint256 val; + return val; + } +} + +function _burnBadDebt_CVL(env e) { + INSIDE_liquidationCall = false; + + mathint curr_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + mathint curr_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + uint128 curr_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + uint128 curr_deficit = getReserveDataExtended(_DBT_asset).deficit; + + // ert INTR2_totSUP_aToken <= INTR2_VB + INTR2_totSUP_debt + INTR2_deficit + _DBT_dbtIND / RAY() + DELTA; + if (curr_totSUP_aToken <= curr_VB + curr_totSUP_debt + curr_deficit + DELTA + _DBT_dbtIND / RAY()) { + havoc_all(e); + mathint after_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + mathint after_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + uint128 after_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + uint128 after_deficit = getReserveDataExtended(_DBT_asset).deficit; + + require + after_totSUP_aToken <= + after_VB + after_totSUP_debt + after_deficit + DELTA + + _DBT_dbtIND / RAY() + _DBT_dbtIND / RAY() + ; + + require + getReserveDataExtended(_DBT_asset).variableBorrowIndex == getNormalizedDebt_CVL(); + + require + getReserveDataExtended(_DBT_asset).liquidityIndex == _DBT_liqIND; + } + INSIDE_liquidationCall = true; +} + + +/*================================================================================================ + Summarizations of HOOKS function + ================================================================================================*/ +methods { + function LiquidationLogic.HOOK_liquidation_after_updateState_DBT() + internal => HOOK_liquidation_after_updateState_DBT_CVL(); + + function LiquidationLogic.HOOK_liquidation_before_burnDebtTokens(bool hasNoCollateralLeft) + internal with (env e) => HOOK_liquidation_before_burnDebtTokens_CVL(e, hasNoCollateralLeft); + + function LiquidationLogic.HOOK_liquidation_after_burnDebtTokens + (bool hasNoCollateralLeft, uint256 actualDebtToLiquidate, uint256 userReserveDebt) internal with (env e) => + HOOK_liquidation_after_burnDebtTokens_CVL(e, hasNoCollateralLeft, actualDebtToLiquidate, userReserveDebt); + + function LiquidationLogic.HOOK_liquidation_before_burnBadDebt() + internal with (env e) => HOOK_liquidation_before_burnBadDebt_CVL(e); + + function LiquidationLogic.HOOK_burnBadDebt_inside_loop(address reserveAddress) + internal with (env e) => HOOK_burnBadDebt_inside_loop_CVL(e, reserveAddress); + + function LiquidationLogic.HOOK_liquidation_after_burnBadDebt() + internal with (env e) => HOOK_liquidation_after_burnBadDebt_CVL(e); +} + + +// This is immediately after the call to updateState for the DBT token +function HOOK_liquidation_after_updateState_DBT_CVL() { + require currentContract._reserves[_DBT_asset].liquidityIndex == _DBT_liqIND; + require currentContract._reserves[_DBT_asset].variableBorrowIndex == _DBT_dbtIND; +} + +function HOOK_liquidation_before_burnDebtTokens_CVL(env e, bool hasNoCollateralLeft) { + INSIDE_liquidationCall = false; + + mathint curr_totSUP_aToken; curr_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + mathint curr_totSUP_debt; curr_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + uint128 curr_VB; curr_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + uint128 curr_deficit; curr_deficit = getReserveDataExtended(_DBT_asset).deficit; + + assert ORIG_totSUP_aToken == curr_totSUP_aToken; + assert ORIG_totSUP_debt == curr_totSUP_debt; + assert ORIG_VB == curr_VB; + assert ORIG_deficit == curr_deficit; + + assert ORIG_totSUP_aToken <= ORIG_VB + ORIG_totSUP_debt + ORIG_deficit + DELTA; + + INSIDE_liquidationCall = true; +} + +function HOOK_liquidation_after_burnDebtTokens_CVL(env e, bool hasNoCollateralLeft, + uint256 actualDebtToLiquidate, uint256 userReserveDebt) { + INSIDE_liquidationCall = false; + HASNoCollateralLeft = hasNoCollateralLeft; + ACTUAL = actualDebtToLiquidate; + USER_RESERVE_DEBT = userReserveDebt; + + INTR_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + INTR_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + INTR_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + INTR_deficit = getReserveDataExtended(_DBT_asset).deficit; + + + assert ORIG_totSUP_aToken == INTR_totSUP_aToken; + assert to_mathint(INTR_VB) == ORIG_VB + actualDebtToLiquidate; + assert !hasNoCollateralLeft => + INTR_totSUP_debt >= ORIG_totSUP_debt - actualDebtToLiquidate - _DBT_dbtIND / RAY(); + + assert hasNoCollateralLeft => + INTR_totSUP_debt >= ORIG_totSUP_debt - userReserveDebt - _DBT_dbtIND / RAY(); + assert hasNoCollateralLeft => + to_mathint(INTR_deficit) == ORIG_deficit + (userReserveDebt - actualDebtToLiquidate); + + // THE MAIN ASSERTION + assert INTR_totSUP_aToken <= INTR_VB + INTR_totSUP_debt + INTR_deficit + _DBT_dbtIND / RAY() + DELTA; + + INSIDE_liquidationCall = true; +} + + +function HOOK_liquidation_before_burnBadDebt_CVL(env e) { + INSIDE_liquidationCall = false; + + INTR2_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + INTR2_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + INTR2_deficit = getReserveDataExtended(_DBT_asset).deficit; + INTR2_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + + assert INTR2_totSUP_aToken == INTR_totSUP_aToken; + assert INTR2_totSUP_debt == INTR_totSUP_debt; + assert INTR2_deficit == INTR_deficit; + assert INTR2_VB == INTR_VB; + + // THE MAIN ASSERTION + assert INTR2_totSUP_aToken <= INTR2_VB + INTR2_totSUP_debt + INTR2_deficit + _DBT_dbtIND / RAY() + DELTA; + + INSIDE_burnBadDebt = true; + INSIDE_liquidationCall = true; +} + + +function HOOK_burnBadDebt_inside_loop_CVL(env e, address reserveAddress) { + // assert reserveAddress != _DBT_asset; + //require reserveAddress != _DBT_asset; +} + +function HOOK_liquidation_after_burnBadDebt_CVL(env e) { + INSIDE_burnBadDebt = false; + INSIDE_liquidationCall = false; + + INTR3_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + INTR3_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + INTR3_deficit = getReserveDataExtended(_DBT_asset).deficit; + INTR3_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + + assert INTR3_totSUP_aToken == INTR_totSUP_aToken; + assert INTR3_totSUP_debt == INTR_totSUP_debt; + assert INTR3_deficit == INTR_deficit; + assert INTR3_VB == INTR_VB; + + // THE MAIN ASSERTION + assert INTR3_totSUP_aToken <= INTR3_VB + INTR3_totSUP_debt + INTR3_deficit + _DBT_dbtIND / RAY() + DELTA; + + INSIDE_liquidationCall = true; +} + + + +/*===================================================================================== + Rule: solvency__liquidationCall + =====================================================================================*/ +rule solvency__liquidationCall_DBTasset(env e) { + INSIDE_liquidationCall = false; + INSIDE_burnBadDebt = false; + configuration(); + + DataTypes.ReserveData reserve = getReserveDataExtended(_DBT_asset); + require getReserveAddressById(reserve.id)==_DBT_asset; + require reserve.id!=0 => getReserveAddressById(0) != _DBT_asset; + //_reservesList[reserve.id] = _DBT_asset; + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + ORIG_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + ORIG_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + ORIG_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + ORIG_deficit = getReserveDataExtended(_DBT_asset).deficit; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + require ORIG_totSUP_aToken <= ORIG_VB + ORIG_totSUP_debt + ORIG_deficit + DELTA; + + require ORIG_totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_DBT_debt)!=0; + require exists_debt; + + // THE FUNCTION CALL + address user; uint256 debtToCover; bool receiveAToken; + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, user, debtToCover, receiveAToken); + INSIDE_liquidationCall = false; + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_DBT_asset); + + mathint FINAL_totSUP_aToken; FINAL_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_DBT_atoken, e)); + mathint FINAL_totSUP_debt; FINAL_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_DBT_debt, e)); + uint128 FINAL_VB = getReserveDataExtended(_DBT_asset).virtualUnderlyingBalance; + uint128 FINAL_deficit = getReserveDataExtended(_DBT_asset).deficit; + + assert FINAL_totSUP_aToken == INTR_totSUP_aToken; + assert FINAL_totSUP_debt == INTR_totSUP_debt; + assert FINAL_deficit == INTR_deficit; + assert FINAL_VB == INTR_VB; + + + //THE ASSERTION + assert + FINAL_totSUP_aToken <= FINAL_VB + FINAL_totSUP_debt + FINAL_deficit + DELTA + + 2*reserve2.variableBorrowIndex / RAY() + ; + +} + diff --git a/certora/solvency/specs/liquidationCall/DBTasset-totSUP0.spec b/certora/solvency/specs/liquidationCall/DBTasset-totSUP0.spec new file mode 100644 index 00000000..58b52585 --- /dev/null +++ b/certora/solvency/specs/liquidationCall/DBTasset-totSUP0.spec @@ -0,0 +1,50 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +//import "../common/validation_functions.spec"; +import "DBTasset-common.spec"; + + + +methods { + function LiquidationLogic.HOOK_liquidation_before_validateLiquidationCall(uint256 userTotalDebt) + internal => HOOK_liquidation_before_validateLiquidationCall_CVL(userTotalDebt); +} + +function HOOK_liquidation_before_validateLiquidationCall_CVL(uint256 userTotalDebt) { + assert userTotalDebt==0; +} + + + +/*===================================================================================== + Rule: liquidationCall_must_revert_if_totDebt_of_DBTasset_EQ_0 + =====================================================================================*/ +rule liquidationCall_must_revert_if_totDebt_of_DBTasset_EQ_0(env e) { + init_state(); + + configuration(); + + DataTypes.ReserveData reserve = getReserveDataExtended(_DBT_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // BASIC ASSUMPTION FOR THE RULE + // require isVirtualAccActive(reserve.configuration.data); + + require scaledTotalSupplyCVL(_DBT_debt)==0; + + // THE FUNCTION CALL + address user; uint256 debtToCover; bool receiveAToken; + // TODO: prove the following basic erc20 property + require aTokenBalanceOfCVL(_DBT_debt,user,e) <= scaledTotalSupplyCVL(_DBT_debt); + liquidationCall@withrevert(e, _COL_asset, _DBT_asset, user, debtToCover, receiveAToken); + + assert lastReverted; +} + + + diff --git a/certora/solvency/specs/liquidationCall/burnBadDebt-assetINloop.spec b/certora/solvency/specs/liquidationCall/burnBadDebt-assetINloop.spec new file mode 100644 index 00000000..e07b7e5f --- /dev/null +++ b/certora/solvency/specs/liquidationCall/burnBadDebt-assetINloop.spec @@ -0,0 +1,146 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + +/*================================================================================================ + Rules in the file: + PASS: https://prover.certora.com/output/66114/3e82c00d6ccd433988bedffbf910c656/?anonymousKey=aaca4f1c23e82f92a0dbfac22998c6a8511f886e + ================================================================================================*/ + + +persistent ghost address ATOKEN; persistent ghost address DEBT; + +persistent ghost mathint DELTA; +persistent ghost uint256 AMOUNT; + +persistent ghost mathint ORIG_totSUP_aToken; +persistent ghost mathint ORIG_totSUP_debt; +persistent ghost uint128 ORIG_VB; +persistent ghost uint128 ORIG_deficit; + +persistent ghost mathint INTR1_totSUP_aToken; +persistent ghost mathint INTR1_totSUP_debt; +persistent ghost uint128 INTR1_VB; +persistent ghost uint128 INTR1_deficit; + +persistent ghost mathint INTR2_totSUP_aToken; +persistent ghost mathint INTR2_totSUP_debt; +persistent ghost uint128 INTR2_VB; +persistent ghost uint128 INTR2_deficit; + + + +methods { + function LiquidationLogic.HOOK_burnBadDebt_inside_loop(address reserveAddress) + internal with (env e) => HOOK_burnBadDebt_inside_loop_CVL(e, reserveAddress); + + function LiquidationLogic.HOOK_burnBadDebt_before_burnDebtTokens(address reserveAddress, uint256 amount) + internal with (env e) => HOOK_burnBadDebt_before_burnDebtTokens_CVL(e,reserveAddress,amount); + + function LiquidationLogic.HOOK_burnBadDebt_after_burnDebtTokens(address reserveAddress) + internal with (env e) => HOOK_burnBadDebt_after_burnDebtTokens_CVL(e,reserveAddress); +} + +function HOOK_burnBadDebt_inside_loop_CVL(env e, address reserveAddress) { + assert reserveAddress == ASSET; +} + +function HOOK_burnBadDebt_before_burnDebtTokens_CVL(env e, address reserveAddress, uint256 amount) { + AMOUNT = amount; + INTR1_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + INTR1_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + INTR1_VB = getReserveDataExtended(ASSET).virtualUnderlyingBalance; + INTR1_deficit = getReserveDataExtended(ASSET).deficit; + + assert INTR1_totSUP_aToken == ORIG_totSUP_aToken; + assert INTR1_totSUP_debt == ORIG_totSUP_debt; + assert INTR1_VB == ORIG_VB; + assert INTR1_deficit == ORIG_deficit; +} + +function HOOK_burnBadDebt_after_burnDebtTokens_CVL(env e, address reserveAddress) { + INTR2_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + INTR2_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + INTR2_VB = getReserveDataExtended(ASSET).virtualUnderlyingBalance; + INTR2_deficit = getReserveDataExtended(ASSET).deficit; + + assert INTR2_totSUP_aToken == INTR1_totSUP_aToken; + assert INTR2_VB == INTR1_VB; + assert INTR2_deficit == INTR1_deficit + AMOUNT; + assert INTR2_totSUP_debt >= INTR1_totSUP_debt - AMOUNT - getReserveDataExtended(ASSET).variableBorrowIndex / RAY(); + + assert INTR2_totSUP_aToken <= INTR2_VB + INTR2_totSUP_debt + INTR2_deficit + DELTA + + getReserveDataExtended(ASSET).variableBorrowIndex / RAY() ; +} + + + +function configuration(address asset) { + init_state(); + + // Different assets have different Debt tokens. + require forall address a1. forall address a2. + a1!=a2 => currentContract._reserves[a1].variableDebtTokenAddress != currentContract._reserves[a2].variableDebtTokenAddress; + + ATOKEN = currentContract._reserves[asset].aTokenAddress; + DEBT = currentContract._reserves[asset].variableDebtTokenAddress; + tokens_addresses_limitations(ATOKEN,DEBT,asset); // this call makes (among other things ASSET==asset) + + require aTokenToUnderlying[ATOKEN]==asset; require aTokenToUnderlying[DEBT]==asset; +} + + + + + +rule solvency__burnBadDebt(env e, address _asset) { + configuration(_asset); + + DataTypes.ReserveData reserve = getReserveDataExtended(ASSET); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + uint256 __liqInd_before = getReserveNormalizedIncome(e, ASSET); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, ASSET); + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + ORIG_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + ORIG_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + ORIG_VB = getReserveDataExtended(ASSET).virtualUnderlyingBalance; + ORIG_deficit = getReserveDataExtended(ASSET).deficit; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + // We assume that the ASSET is processed in the loop (of _burnBadDebt). The opposite is considered in the + // file burnBadDebt-assetNOTINloop.spec + require currentContract._reservesCount >= 1 && getReservesList()[0]==ASSET; + + //THE MAIN REQUIREMENT + require ORIG_totSUP_aToken <= ORIG_VB + ORIG_totSUP_debt + ORIG_deficit + DELTA; + + require ORIG_totSUP_aToken <= 10^27; // Without this requirement we get a failure. + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + address user; _burnBadDebt_WRP(e, user); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(ASSET); + mathint FINAL_totSUP_aToken; FINAL_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + mathint FINAL_totSUP_debt; FINAL_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + uint128 FINAL_VB = getReserveDataExtended(ASSET).virtualUnderlyingBalance; + uint128 FINAL_deficit = getReserveDataExtended(ASSET).deficit; + + //THE ASSERTION + assert FINAL_totSUP_aToken <= FINAL_VB + FINAL_totSUP_debt + FINAL_deficit + DELTA + + getReserveNormalizedVariableDebt(e, ASSET) / RAY(); +} + diff --git a/certora/solvency/specs/liquidationCall/burnBadDebt-assetNOTINloop.spec b/certora/solvency/specs/liquidationCall/burnBadDebt-assetNOTINloop.spec new file mode 100644 index 00000000..a46828e1 --- /dev/null +++ b/certora/solvency/specs/liquidationCall/burnBadDebt-assetNOTINloop.spec @@ -0,0 +1,145 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + +/*================================================================================================ + Rules in the file: + PASS: https://prover.certora.com/output/66114/cd7e0f9d2fc44e16a1f2c80918ec0fba/?anonymousKey=04d6f84469e7d81aea57a662f69e2f9bf8fae266 + ================================================================================================*/ + + +persistent ghost address ATOKEN; persistent ghost address DEBT; + +persistent ghost mathint DELTA; + +persistent ghost mathint ORIG_totSUP_aToken; +persistent ghost mathint ORIG_totSUP_debt; +persistent ghost uint128 ORIG_VB; +persistent ghost uint128 ORIG_deficit; + +persistent ghost mathint INTR1_totSUP_aToken; +persistent ghost mathint INTR1_totSUP_debt; +persistent ghost uint128 INTR1_VB; +persistent ghost uint128 INTR1_deficit; + +persistent ghost mathint INTR2_totSUP_aToken; +persistent ghost mathint INTR2_totSUP_debt; +persistent ghost uint128 INTR2_VB; +persistent ghost uint128 INTR2_deficit; + + + +methods { + function LiquidationLogic.HOOK_burnBadDebt_inside_loop(address reserveAddress) + internal with (env e) => HOOK_burnBadDebt_inside_loop_CVL(e, reserveAddress); + + function LiquidationLogic.HOOK_burnBadDebt_before_burnDebtTokens(address reserveAddress, uint256 amount) + internal with (env e) => HOOK_burnBadDebt_before_burnDebtTokens_CVL(e,reserveAddress,amount); + + function LiquidationLogic.HOOK_burnBadDebt_after_burnDebtTokens(address reserveAddress) + internal with (env e) => HOOK_burnBadDebt_after_burnDebtTokens_CVL(e,reserveAddress); +} + +function HOOK_burnBadDebt_inside_loop_CVL(env e, address reserveAddress) { + assert reserveAddress != ASSET; +} + +function HOOK_burnBadDebt_before_burnDebtTokens_CVL(env e, address reserveAddress, uint256 amount) { + INTR1_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + INTR1_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + INTR1_VB = getReserveDataExtended(ASSET).virtualUnderlyingBalance; + INTR1_deficit = getReserveDataExtended(ASSET).deficit; + + assert INTR1_totSUP_aToken == ORIG_totSUP_aToken; + assert INTR1_totSUP_debt == ORIG_totSUP_debt; + assert INTR1_VB == ORIG_VB; + assert INTR1_deficit == ORIG_deficit; +} + +function HOOK_burnBadDebt_after_burnDebtTokens_CVL(env e, address reserveAddress) { + INTR2_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + INTR2_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + INTR2_VB = getReserveDataExtended(ASSET).virtualUnderlyingBalance; + INTR2_deficit = getReserveDataExtended(ASSET).deficit; + + assert INTR2_totSUP_aToken == INTR1_totSUP_aToken; + assert INTR2_totSUP_debt == INTR1_totSUP_debt; + assert INTR2_VB == INTR1_VB; + assert INTR2_deficit == INTR1_deficit; +} + + +function configuration(address asset) { + init_state(); + + // Different assets have different Debt tokens. + require forall address a1. forall address a2. + a1!=a2 => currentContract._reserves[a1].variableDebtTokenAddress != currentContract._reserves[a2].variableDebtTokenAddress; + + ATOKEN = currentContract._reserves[asset].aTokenAddress; + DEBT = currentContract._reserves[asset].variableDebtTokenAddress; + tokens_addresses_limitations(ATOKEN,DEBT,asset); // this call makes (among other things ASSET==asset) + + require aTokenToUnderlying[ATOKEN]==asset; require aTokenToUnderlying[DEBT]==asset; +} + + + + +rule solvency__burnBadDebt(env e, address _asset) { + configuration(_asset); + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + ORIG_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + ORIG_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + ORIG_VB = getReserveDataExtended(_asset).virtualUnderlyingBalance; + ORIG_deficit = getReserveDataExtended(_asset).deficit; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + // We assume that the ASSET is NOT processed in the loop (of _burnBadDebt). The opposite is considered in the + // file burnBadDebt-assetINloop.spec + require currentContract._reservesCount < 1 || getReservesList()[0]!=ASSET; + + //THE MAIN REQUIREMENT + require ORIG_totSUP_aToken <= ORIG_VB + ORIG_totSUP_debt + ORIG_deficit + DELTA; + + require ORIG_totSUP_aToken <= 10^27; // Without this requirement we get a failure. + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + address user; _burnBadDebt_WRP(e, user); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + mathint FINAL_totSUP_aToken; FINAL_totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + mathint FINAL_totSUP_debt; FINAL_totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + uint128 FINAL_VB = getReserveDataExtended(_asset).virtualUnderlyingBalance; + uint128 FINAL_deficit = getReserveDataExtended(_asset).deficit; + + assert FINAL_totSUP_aToken==ORIG_totSUP_aToken; + assert FINAL_totSUP_debt==ORIG_totSUP_debt; + assert FINAL_VB==ORIG_VB; + assert FINAL_deficit==ORIG_deficit; + + //THE ASSERTION + assert + FINAL_totSUP_aToken <= FINAL_VB + FINAL_totSUP_debt + FINAL_deficit + DELTA + + getReserveNormalizedVariableDebt(e, ASSET) / RAY(); +} + diff --git a/certora/solvency/specs/liquidationCall/sanity.spec b/certora/solvency/specs/liquidationCall/sanity.spec new file mode 100644 index 00000000..8319fec9 --- /dev/null +++ b/certora/solvency/specs/liquidationCall/sanity.spec @@ -0,0 +1,126 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + +persistent ghost bool INSIDE_liquidationCall; +persistent ghost bool INSIDE_burnBadDebt; + +persistent ghost address _DBT_asset; persistent ghost address _DBT_atoken; persistent ghost address _DBT_debt; + +persistent ghost address _COL_asset; persistent ghost address _COL_atoken; persistent ghost address _COL_debt; +persistent ghost uint256 _COL_liqIND {axiom _COL_liqIND >= 10^27;} +persistent ghost uint256 _COL_dbtIND {axiom _COL_dbtIND >= 10^27;} + + +methods { + //TEMPORARY !!! we remove the following + // function LiquidationLogic._burnBadDebt( + // mapping(address => DataTypes.ReserveData) storage reservesData, + // mapping(uint256 => address) storage reservesList, + // DataTypes.UserConfigurationMap storage userConfig, + // uint256 reservesCount, + // address user + //) internal => NONDET; + + function ReserveLogic.getNormalizedIncome(DataTypes.ReserveData storage reserve) + internal returns (uint256) => _COL_liqIND; + + function ReserveLogic.getNormalizedDebt(DataTypes.ReserveData storage reserve) + internal returns (uint256) => getNormalizedDebt_CVL(); + + function IsolationModeLogic.updateIsolatedDebtIfIsolated( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ReserveCache memory reserveCache, + uint256 repayAmount + ) internal => updateIsolatedDebtIfIsolatedCVL(); + + function LiquidationLogic._calculateAvailableCollateralToLiquidate( + DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, + uint256 collateralAssetPrice, + uint256 collateralAssetUnit, + uint256 debtAssetPrice, + uint256 debtAssetUnit, + uint256 debtToCover, + uint256 userCollateralBalance, + uint256 liquidationBonus + ) internal returns (uint256,uint256,uint256,uint256) => + _calculateAvailableCollateralToLiquidateCVL(); +} + + +function getNormalizedDebt_CVL() returns uint256 { + if (INSIDE_liquidationCall) { + uint256 dbt_index; + return dbt_index; + } + else + return _COL_liqIND; +} + + +function _calculateAvailableCollateralToLiquidateCVL() returns (uint256,uint256,uint256,uint256) { + uint256 a; uint256 b; uint256 c; uint256 d; // require c==0; + return (a,b,c,d); +} + + +// The function updateIsolatedDebtIfIsolated(...) only writes to the field isolationModeTotalDebt. +function updateIsolatedDebtIfIsolatedCVL() { + address asset; + havoc currentContract._reserves[asset].isolationModeTotalDebt; +} + + + +function tokens_addresses_limitations_LQD(address asset, address atoken, address debt, + address asset2, address atoken2, address debt2 + ) { + require asset==100; require atoken==10; require debt==11; + require asset2==200; require atoken2==20; require debt2==21; + +} + + +function configuration() { + init_state(); + + _DBT_atoken = currentContract._reserves[_DBT_asset].aTokenAddress; + _COL_atoken = currentContract._reserves[_COL_asset].aTokenAddress; + _DBT_debt = currentContract._reserves[_DBT_asset].variableDebtTokenAddress; + _COL_debt = currentContract._reserves[_COL_asset].variableDebtTokenAddress; + tokens_addresses_limitations_LQD(_DBT_asset, _DBT_atoken, _DBT_debt, + _COL_asset, _COL_atoken, _COL_debt); + + require aTokenToUnderlying[_DBT_atoken]==_DBT_asset; require aTokenToUnderlying[_DBT_debt]==_DBT_asset; + require aTokenToUnderlying[_COL_atoken]==_COL_asset; require aTokenToUnderlying[_COL_debt]==_COL_asset; +} + +/*===================================================================================== + Rule: solvency__liquidationCall + =====================================================================================*/ +rule sanity__liquidationCall(env e) { + INSIDE_liquidationCall = false; + configuration(); + + // THE FUNCTION CALL + address _user; uint256 _debtToCover; bool _receiveAToken; + INSIDE_liquidationCall = true; + liquidationCall(e, _COL_asset, _DBT_asset, _user, _debtToCover, _receiveAToken); + INSIDE_liquidationCall = false; + + satisfy true; +} + diff --git a/certora/solvency/specs/repay/repay-NONindexSUMM.spec b/certora/solvency/specs/repay/repay-NONindexSUMM.spec new file mode 100644 index 00000000..c8852041 --- /dev/null +++ b/certora/solvency/specs/repay/repay-NONindexSUMM.spec @@ -0,0 +1,135 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + +/*================================================================================================ + See the top comment at the file repay-indexSUMM.spec (this directory). + ================================================================================================*/ + + +/*===================================================================================== + Rule: same_indexes__repay + + The rule is the following lemma (assumed in the file repay-indexSUMM.spec. + + a: The return value of getNormalizedIncome(...) is the same before and after the function call + and: + b: If the scaled-total-supply of the variable-debt isn't 0, then the return value of getNormalizedDebt(...) + is the same before and after the function call. + Note that getNormalizedIncome(...) return the liquidity index, while getNormalizedDebt(...) returns + the variable-debt index. + + Status: PASS + Link: https://prover.certora.com/output/66114/17b4d0b1734948029433e1ab174ec431/?anonymousKey=35f254e9d99941b9760eda33d00434141edba7e2 + + =====================================================================================*/ +rule same_indexes__repay(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getNormalizedDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + bool exists_debt = scaledTotalSupplyCVL(_debt)!=0; + require exists_debt; + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repay(e, _asset, _amount, interestRateMode, onBehalfOf); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + uint128 __liqInd_afterS = reserve2.liquidityIndex; + uint128 __dbtInd_afterS = reserve2.variableBorrowIndex; + uint256 __liqInd_after = getNormalizedIncome(e, _asset); + uint256 __dbtInd_after = getNormalizedDebt(e, _asset); + + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + + assert __liqInd_after == __liqInd_before; + assert assert_uint256(__liqInd_afterS) == __liqInd_after; + + assert __dbtInd_after == __dbtInd_before; + assert assert_uint256(__dbtInd_afterS) == __dbtInd_after; +} + + + + + + +/*===================================================================================== + Rule: solvency__repay_totDbt_EQ_0 + Details: We prove that if scaled-total-supply of the variable-debt is 0, then repay reverts. + Status: PASS + Link: https://prover.certora.com/output/66114/17b4d0b1734948029433e1ab174ec431/?anonymousKey=35f254e9d99941b9760eda33d00434141edba7e2 + =====================================================================================*/ +rule solvency__repay_totDbt_EQ_0(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + require forall address a. balanceByToken[_debt][a] <= totalSupplyByToken[_debt]; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_debt)!=0; + require !exists_debt; + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repay@withrevert(e, _asset, _amount, interestRateMode, onBehalfOf); + assert lastReverted; +} + + diff --git a/certora/solvency/specs/repay/repay-indexSUMM.spec b/certora/solvency/specs/repay/repay-indexSUMM.spec new file mode 100644 index 00000000..0172a6dd --- /dev/null +++ b/certora/solvency/specs/repay/repay-indexSUMM.spec @@ -0,0 +1,98 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory. + + For the case of the repay functions getNormalizedIncome(...), getNormalizedDebt(...) to return + specific values (see below in the method block). We also assume that after the repay function + is finished, that values will be the same as the values in the storage: + reserve-of-the-asset.liquidityIndex, and reserve-of-the-asset.variableBorrowIndex. + (This indeed holds under the assumption that the scaled-total-supply of the variable-debt isn't 0.) + + We prove that it is indeed the case, and we prove it in the file repay-NONindexSUMM.spec (in this + directory.) We also take care there of the non interesting case where scaled-total-supply of the + variable-debt == 0. + ================================================================================================*/ + +methods { + function ValidationLogic.validateRepay( + DataTypes.ReserveCache memory reserveCache, + uint256 amountSent, + DataTypes.InterestRateMode interestRateMode, + address onBehalfOf, + uint256 debt + ) internal => NONDET; + + function ReserveLogic.getNormalizedIncome(DataTypes.ReserveData storage reserve) + internal returns (uint256) => LIQUIDITY_INDEX; + + function ReserveLogic.getNormalizedDebt(DataTypes.ReserveData storage reserve) + internal returns (uint256) => DEBT_INDEX; +} + +ghost uint256 LIQUIDITY_INDEX {axiom LIQUIDITY_INDEX >= 10^27;} +ghost uint256 DEBT_INDEX {axiom DEBT_INDEX >= 10^27;} + + +/*===================================================================================== + Rule: solvency__repay + Status: PASS + Link: https://prover.certora.com/output/66114/c3bf7d127763413a885fcae19671d662/?anonymousKey=f3f236570d484eeb903cab1cd345d20beb5f664b + =====================================================================================*/ +rule solvency__repay(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_debt)!=0; + require exists_debt; + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repay(e, _asset, _amount, interestRateMode, onBehalfOf); + + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + require assert_uint256(reserve2.liquidityIndex) == LIQUIDITY_INDEX; + require assert_uint256(reserve2.variableBorrowIndex) == DEBT_INDEX; + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.variableBorrowIndex / RAY() ; +} + diff --git a/certora/solvency/specs/repayWithATokens/repayWithATokens-HOOKS.spec b/certora/solvency/specs/repayWithATokens/repayWithATokens-HOOKS.spec new file mode 100644 index 00000000..753f2e74 --- /dev/null +++ b/certora/solvency/specs/repayWithATokens/repayWithATokens-HOOKS.spec @@ -0,0 +1,201 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + +/*================================================================================================ + See the README.txt file in the solvency/ directory + + In order to deal with time-outs, we helped the prover by adding several "hooks" inside the function + executeRepay. In these hooks (see their summarizations below) we added several asserts, hence + the prover proves them, and later uses them as requirement for later asserts. + We run with --multi_asset_check. + ================================================================================================*/ + + + +methods { + function _.rayMul(uint256 a, uint256 b) internal => rayMulCVLPrecise(a, b) expect uint256; // not optimized well by Prover + function _.rayDiv(uint256 a, uint256 b) internal => rayDivCVLPrecise(a, b) expect uint256; // seems to be optimized well by Prover + + function ValidationLogic.validateRepay( + DataTypes.ReserveCache memory reserveCache, + uint256 amountSent, + DataTypes.InterestRateMode interestRateMode, + address onBehalfOf, + uint256 debt + ) internal => NONDET; + + function BorrowLogic.repay_hook_1(DataTypes.ReserveCache memory reserveCache) + internal => repay_hook_1_CVL(reserveCache); + + function BorrowLogic.repay_hook_2(DataTypes.ReserveCache memory reserveCache) + internal with (env e) => repay_hook_2_CVL(e, reserveCache); + + function BorrowLogic.repay_hook_3(DataTypes.ReserveCache memory reserveCache, uint256 paybackAmount) + internal with (env e) => repay_hook_3_CVL(e, reserveCache, paybackAmount); + + function BorrowLogic.repay_hook_4(DataTypes.ReserveCache memory reserveCache) + internal with (env e) => repay_hook_4_CVL(e, reserveCache); + + //function BorrowLogic.dummy_get_amount() internal returns(uint256) => NONDET; + + function _.havoc_all_dummy() external => HAVOC_ALL; +} + +function repay_hook_1_CVL(DataTypes.ReserveCache res) { + assert currentContract._reserves[ASSET].liquidityIndex == require_uint128(LIQUIDITY_INDEX); + assert res.nextLiquidityIndex == LIQUIDITY_INDEX; + + assert currentContract._reserves[ASSET].variableBorrowIndex == require_uint128(DEBT_INDEX); + assert res.nextVariableBorrowIndex == DEBT_INDEX; +} + +function repay_hook_2_CVL(env e, DataTypes.ReserveCache res) { + assert res.nextLiquidityIndex==LIQUIDITY_INDEX; + assert res.nextVariableBorrowIndex == DEBT_INDEX; + assert getReserveNormalizedIncome(e, ASSET) == LIQUIDITY_INDEX; + assert getReserveNormalizedVariableDebt(e, ASSET) == DEBT_INDEX; + + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + + // THE MAIN ASSERTION + assert to_mathint(__totSUP_aToken) <= __totSUP_debt + DELTA; +} + +function repay_hook_3_CVL(env e, DataTypes.ReserveCache res, uint256 paybackAmount) { + assert res.nextLiquidityIndex==LIQUIDITY_INDEX; + assert res.nextVariableBorrowIndex == DEBT_INDEX; + assert getReserveNormalizedIncome(e, ASSET) == LIQUIDITY_INDEX; + assert getReserveNormalizedVariableDebt(e, ASSET) == DEBT_INDEX; + assert currentContract._reserves[ASSET].lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert assert_uint256(currentContract._reserves[ASSET].liquidityIndex) == LIQUIDITY_INDEX; + assert assert_uint256(currentContract._reserves[ASSET].variableBorrowIndex) == DEBT_INDEX; + assert VB == currentContract._reserves[ASSET].virtualUnderlyingBalance; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + + // THE MAIN ASSERTION + assert to_mathint(__totSUP_aToken) - paybackAmount <= __totSUP_debt + DEBT_INDEX/RAY() + DELTA; + havoc_all(e); + + // The following requirements are all safe because we've just proved them before the havoc_all. + mathint __totSUP_aToken2; __totSUP_aToken2 = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + mathint __totSUP_debt2; __totSUP_debt2 = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + require res.nextLiquidityIndex==LIQUIDITY_INDEX; + require res.nextVariableBorrowIndex == DEBT_INDEX; + require getReserveNormalizedIncome(e, ASSET) == LIQUIDITY_INDEX; + require getReserveNormalizedVariableDebt(e, ASSET) == DEBT_INDEX; + require currentContract._reserves[ASSET].lastUpdateTimestamp == assert_uint40(e.block.timestamp); + require assert_uint256(currentContract._reserves[ASSET].liquidityIndex) == LIQUIDITY_INDEX; + require assert_uint256(currentContract._reserves[ASSET].variableBorrowIndex) == DEBT_INDEX; + require VB == currentContract._reserves[ASSET].virtualUnderlyingBalance; + + + uint256 scaled = totalSupplyByToken[ATOKEN]; uint256 IND = LIQUIDITY_INDEX; + assert + to_mathint(rayMulCVLPrecise( require_uint256(scaled - rayDivCVLPrecise(paybackAmount,IND)), IND) ) + <= + rayMulCVLPrecise(scaled,IND) - paybackAmount + IND/RAY(); +} + +function repay_hook_4_CVL(env e, DataTypes.ReserveCache res) { + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(ATOKEN, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(DEBT, e)); + + // THE MAIN ASSERTION + assert to_mathint(__totSUP_aToken) <= __totSUP_debt + DEBT_INDEX/RAY() + + LIQUIDITY_INDEX/RAY() + DELTA; +} + +persistent ghost address ATOKEN; //The current atoken in use. Should be assigned from within the rule. +persistent ghost address DEBT; //The current debt-token in use. Should be assigned from within the rule. +persistent ghost uint256 DELTA; +persistent ghost uint128 VB; // the virtual balance. should not be changed in repayWithATokens. + +function _updateIndexesCVL(DataTypes.ReserveCache res) { + havoc currentContract._reserves[ASSET].liquidityIndex;// assuming + require currentContract._reserves[ASSET].liquidityIndex == require_uint128(LIQUIDITY_INDEX); + havoc currentContract._reserves[ASSET].variableBorrowIndex; // assuming + require currentContract._reserves[ASSET].variableBorrowIndex == require_uint128(DEBT_INDEX); + + require res.nextLiquidityIndex == LIQUIDITY_INDEX; + require res.nextVariableBorrowIndex == DEBT_INDEX; +} + + +persistent ghost uint256 LIQUIDITY_INDEX {axiom LIQUIDITY_INDEX >= 10^27;} +persistent ghost uint256 DEBT_INDEX {axiom DEBT_INDEX >= 10^27;} + + +/*===================================================================================== + Rule: solvency__repayWithATokens + Status: PASS + Link: https://prover.certora.com/output/66114/4f2ccd151350485cbc13ae8b6f090592/?anonymousKey=269ecbf902dcd6fa81c201aeed0c7371a605828f + =====================================================================================*/ +rule solvency__repayWithATokens(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; ATOKEN = _atoken; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; DEBT = _debt; + //address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + //require aTokenToUnderlying[_stb]==_asset; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + VB = __virtual_bal; + + // INDEXES + LIQUIDITY_INDEX = getReserveNormalizedIncome(e, _asset); + DEBT_INDEX = getReserveNormalizedVariableDebt(e, _asset); + require RAY() <= LIQUIDITY_INDEX && RAY() <= DEBT_INDEX ; + + + // BASIC ASSUMPTION FOR THE RULE + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + //require scaledTotalSupplyCVL(_stb)==0; + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + // Note: + // 1. DELTA: the gap which the invariant can be break upto. This is a ghost variable + // 2. We dropped that virtual-balance from the inequality because we prove it stays + // unchanged during the function repayWithATokens(...). + require to_mathint(__totSUP_aToken) <= __totSUP_debt + DELTA; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_debt)!=0; + require exists_debt; + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repayWithATokens(e, _asset, _amount, interestRateMode); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // The virtual-balance stays unchanged + assert __virtual_bal__==__virtual_bal; + + // THE MAIN ASSERTION + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert to_mathint(__totSUP_aToken__) <= __totSUP_debt__ + DELTA + + reserve2.liquidityIndex / RAY() + reserve2.variableBorrowIndex / RAY(); +} + diff --git a/certora/solvency/specs/repayWithATokens/repayWithATokens-noHOOKS.spec b/certora/solvency/specs/repayWithATokens/repayWithATokens-noHOOKS.spec new file mode 100644 index 00000000..6a42f361 --- /dev/null +++ b/certora/solvency/specs/repayWithATokens/repayWithATokens-noHOOKS.spec @@ -0,0 +1,70 @@ + +// aave imports +import "../AUX/CVLMocks/aToken.spec"; +import "../AUX/CVLMocks/AddressProvider.spec"; + +import "../common/optimizations.spec"; +import "../common/functions.spec"; +import "../common/validation_functions.spec"; + + + + + + +/*===================================================================================== + Rule: solvency__repayWithATokens_totDbt_EQ_0 + We prove that if scaled-total-supply of the variable-debt is 0, then repayWithATokens reverts. + + Status: PASS + Link: https://prover.certora.com/output/66114/384d747b8ad540bca06387fe00d0a7bf/?anonymousKey=c15ce8ec3bd0ade0b0fa77042d725c28174e137d + =====================================================================================*/ +rule solvency__repayWithATokens_totDbt_EQ_0(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require forall address a. balanceByToken[_debt][a] <= totalSupplyByToken[_debt]; + require forall address a. balanceByToken[_atoken][a] <= totalSupplyByToken[_atoken]; + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + bool exists_debt = scaledTotalSupplyCVL(_debt)!=0; + require !exists_debt; + + + // THE FUNCTION CALL + uint256 _amount; uint256 interestRateMode; address onBehalfOf; uint16 referralCode; + assert balanceByToken[_debt][onBehalfOf]==0; // This should help the prover + require interestRateMode == assert_uint256(DataTypes.InterestRateMode.VARIABLE); + repayWithATokens@withrevert(e, _asset, _amount, interestRateMode); + assert lastReverted; +} + + diff --git a/certora/solvency/specs/supply.spec b/certora/solvency/specs/supply.spec new file mode 100644 index 00000000..a7d0fe51 --- /dev/null +++ b/certora/solvency/specs/supply.spec @@ -0,0 +1,72 @@ + +// aave imports +import "AUX/CVLMocks/aToken.spec"; +import "AUX/CVLMocks/AddressProvider.spec"; + +import "common/optimizations.spec"; +import "common/functions.spec"; +import "common/validation_functions.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + + + +/*===================================================================================== + Rule: solvency__supply + + Status: PASS + Link: https://prover.certora.com/output/66114/702a83a589334a8a815c18dc79629c0d/?anonymousKey=4d139c38ab10b1a9ea6625410f2de43960e38b12 + =====================================================================================*/ +rule solvency__supply(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + require RAY() <= __liqInd_before && RAY() <= __dbtInd_before ; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get timeout/error + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + uint256 _amount; address onBehalfOf; uint16 referralCode; + supply(e, _asset, _amount, onBehalfOf, referralCode); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.liquidityIndex/RAY(); +} diff --git a/certora/solvency/specs/time_passing.spec b/certora/solvency/specs/time_passing.spec new file mode 100644 index 00000000..344b7944 --- /dev/null +++ b/certora/solvency/specs/time_passing.spec @@ -0,0 +1,116 @@ + +// aave imports +import "AUX/CVLMocks/aToken.spec"; +import "AUX/CVLMocks/AddressProvider.spec"; + + +import "common/optimizations.spec"; +import "common/functions.spec"; +import "common/validation_functions.spec"; + + + + + + +/*===================================================================================== + Rule: solvency__time_passing + + Status: + 1. Currently , in order to avoid time-out the are too many restrictions (see under + "// RESTRICTIONS" below. + 2. There is a big assumption that is not proved. See under "// ASSMPTION" and the explanation + above it. + =====================================================================================*/ +rule solvency__time_passing(env e1, env e2, address _asset) { + init_state(); + require require_uint40(e1.block.timestamp) <= require_uint40(e2.block.timestamp); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + address _stb = currentContract._reserves[_asset].stableDebtTokenAddress; + //tokens_addresses_limitations(_atoken,_debt,_stb,_asset); + require _atoken==10; require _debt==11; require _stb==12; require _asset==100; + // require weth!=10 && weth!=11 && weth!=12; + ASSET = _asset; + + require forall address a. balanceByToken[_debt][a] <= totalSupplyByToken[_debt]; + require forall address a. balanceByToken[_atoken][a] <= totalSupplyByToken[_atoken]; + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; require aTokenToUnderlying[_stb]==_asset; + + DataTypes.ReserveData reserve1 = getReserveDataExtended(_asset); + require reserve1.lastUpdateTimestamp == require_uint40(e1.block.timestamp); + + // INDEXES REQUIREMENTS + uint128 __liqInd_beforeS = reserve1.liquidityIndex; + uint128 __dbtInd_beforeS = reserve1.variableBorrowIndex; + require 10^27<=__liqInd_beforeS && 10^27<=__dbtInd_beforeS; + // the following 3 line are redundant because we assume lastUpdateTimestamp == e1.block.timestamp, but hopfully + // it helps the prover. + uint256 __liqInd_before = getReserveNormalizedIncome(e1, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e1, _asset); + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + + // BASIC ASSUMPTION FOR THE RULE + require scaledTotalSupplyCVL(_stb)==0; + + uint256 __totSUP_aToken; __totSUP_aToken = aTokenTotalSupplyCVL(_atoken, e1); + uint256 __totSUP_debt; __totSUP_debt = aTokenTotalSupplyCVL(_debt, e1); + // uint256 supply_usage_ratio = rayDivCVLPrecise(__totSUP_debt,__totSUP_aToken); + + // Here we need to require some property about the relation between the liq-rate and borrow-rate. + // The property should follow from the function DefaultReserveInterestRateStrategyV2.calculateInterestRates + // and it should be something like liq-rate <= borrow-rate * supply-usage-ratio + // where supply-usage-ratio is total-debt / (VB + total-debt + unbacked) + // require reserve1.currentLiquidityRate <= reserve1.currentVariableBorrowRate; + // require assert_uint256(reserve1.currentLiquidityRate) <= rayMulCVLPrecise(reserve1.currentVariableBorrowRate, supply_usage_ratio); + + // ASSUMPTION + require rayMulCVLPrecise(reserve1.currentLiquidityRate, __totSUP_aToken) <= + rayMulCVLPrecise(reserve1.currentVariableBorrowRate, __totSUP_debt); + + + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE MAIN REQUIREMENT + uint256 CONST; + // require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + require __totSUP_aToken <= __totSUP_debt; + + + // RESTRICTIONS + require __totSUP_aToken <= 10^27; + require __totSUP_debt <= 10^27; + require e1.block.timestamp==0; + require e2.block.timestamp==365*86400; + + // require __liqInd_before ==10^27 && __dbtInd_before == 10^27; + require reserve1.liquidityIndex == 10^27 && reserve1.variableBorrowIndex==10^27; + //require reserve1.currentLiquidityRate == reserve1.currentVariableBorrowRate; + //require reserve1.currentLiquidityRate == 10^27; + require totalSupplyCVL(_debt)==0; + + + // FUNCTION CALL: NONE !!! + + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + uint256 __liqInd_after = getReserveNormalizedIncome(e2, _asset); + uint256 __dbtInd_after = getReserveNormalizedVariableDebt(e2, _asset); + + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e2)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e2)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + assert __virtual_bal__ == __virtual_bal; + + //THE ASSERTION + //assert to_mathint(__totSUP_aToken__) <= __virtual_bal__ + __totSUP_debt__ + CONST + assert __totSUP_aToken__ <= __totSUP_debt__ + /* + some rounding error */; +} + + + + diff --git a/certora/solvency/specs/withdraw.spec b/certora/solvency/specs/withdraw.spec new file mode 100644 index 00000000..4585c1e1 --- /dev/null +++ b/certora/solvency/specs/withdraw.spec @@ -0,0 +1,76 @@ + +// aave imports +import "AUX/CVLMocks/aToken.spec"; +import "AUX/CVLMocks/AddressProvider.spec"; + +import "common/optimizations.spec"; +import "common/functions.spec"; +import "common/validation_functions.spec"; + + +/*================================================================================================ + See the README.txt file in the solvency/ directory + ================================================================================================*/ + + + +/*===================================================================================== + Rule: solvency__withdraw + + Status: PASS + Link: https://prover.certora.com/output/66114/f78383017cfa4b7ead6530b0b6e3a87b/?anonymousKey=58440ef19dd97ec9a5d7ba978481a77673e3f133 + =====================================================================================*/ +rule solvency__withdraw(env e, address _asset) { + init_state(); + + address _atoken = currentContract._reserves[_asset].aTokenAddress; + address _debt = currentContract._reserves[_asset].variableDebtTokenAddress; + tokens_addresses_limitations(_atoken,_debt,_asset); + + require aTokenToUnderlying[_atoken]==_asset; require aTokenToUnderlying[_debt]==_asset; + + DataTypes.ReserveData reserve = getReserveDataExtended(_asset); + require reserve.lastUpdateTimestamp <= require_uint40(e.block.timestamp); + + // INDEXES REQUIREMENTS + uint256 __liqInd_before = getReserveNormalizedIncome(e, _asset); + uint256 __dbtInd_before = getReserveNormalizedVariableDebt(e, _asset); + uint128 __liqInd_beforeS = reserve.liquidityIndex; + uint128 __dbtInd_beforeS = reserve.variableBorrowIndex; + require RAY()<=__liqInd_before && RAY()<=__dbtInd_before; + require assert_uint128(RAY()) <= __liqInd_beforeS && assert_uint128(RAY()) <= __dbtInd_beforeS; + + mathint __totSUP_aToken; __totSUP_aToken = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt; __totSUP_debt = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + // BASIC ASSUMPTION FOR THE RULE + require isVirtualAccActive(reserve.configuration.data); + + // THE MAIN REQUIREMENT + uint256 CONST; + require to_mathint(__totSUP_aToken) <= __virtual_bal + __totSUP_debt + CONST; + + require __totSUP_aToken <= 10^27; // Without this requirement we get a timeout + // I believe it's due inaccure RAY-calculations. + + // THE FUNCTION CALL + uint256 _amount; address _to; + withdraw(e, _asset, _amount, _to); + + DataTypes.ReserveData reserve2 = getReserveDataExtended(_asset); + assert reserve2.lastUpdateTimestamp == assert_uint40(e.block.timestamp); + assert reserve2.liquidityIndex == assert_uint128(__liqInd_before); + assert __totSUP_debt != 0 => (reserve2.variableBorrowIndex == assert_uint128(__dbtInd_before)); + assert getReserveNormalizedIncome(e, _asset) == __liqInd_before; + assert __totSUP_debt != 0 => (getReserveNormalizedVariableDebt(e, _asset) == __dbtInd_before); + + mathint __totSUP_aToken__; __totSUP_aToken__ = to_mathint(aTokenTotalSupplyCVL(_atoken, e)); + mathint __totSUP_debt__; __totSUP_debt__ = to_mathint(aTokenTotalSupplyCVL(_debt, e)); + uint128 __virtual_bal__ = getReserveDataExtended(_asset).virtualUnderlyingBalance; + + //THE ASSERTION + assert __totSUP_aToken__ <= __virtual_bal__ + __totSUP_debt__ + CONST + + reserve2.liquidityIndex / RAY(); +} +