From 1c480e11ff751829ef9e47a72a4e47232434c940 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 6 Dec 2023 13:20:29 -0500 Subject: [PATCH] [SC-288] Add pause support and external authorized contract (#5) * add in pause support * add wards * add pause spells and rename to execute() to be more generic * readme and doc fixing * fix readme * add integration tests for pause; add interface for execute-once spells and fix pragma * add multisig integration test * formating and naming fixes * more accurate prod env for integration test of spell * dont need extra role auth --- README.md | 20 +- src/SparkLendFreezerMom.sol | 54 +- src/interfaces/IExecuteOnceSpell.sol | 16 + src/interfaces/ISparkLendFreezerMom.sol | 82 ++- ...ergencySpell_SparkLend_FreezeAllAssets.sol | 13 +- ...gencySpell_SparkLend_FreezeSingleAsset.sol | 13 +- ...mergencySpell_SparkLend_PauseAllAssets.sol | 23 + ...rgencySpell_SparkLend_PauseSingleAsset.sol | 25 + test/IntegrationTests.t.sol | 535 +++++++++++++----- test/Mocks.sol | 2 + test/SparkLendFreezerMom.t.sol | 245 +++++++- 11 files changed, 846 insertions(+), 182 deletions(-) create mode 100644 src/interfaces/IExecuteOnceSpell.sol create mode 100644 src/spells/EmergencySpell_SparkLend_PauseAllAssets.sol create mode 100644 src/spells/EmergencySpell_SparkLend_PauseSingleAsset.sol diff --git a/README.md b/README.md index e6c7110..8d81894 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,26 @@ ## Overview -This repo contains three contracts: +This repo contains five contracts: ### `src/SparkLendFreezerMom.sol` -A contract that will have `RISK_ADMIN_ROLE` in SparkLend, and has two functions: -- `freezeAllMarkets`: Freezes all markets in SparkLend, callable by the `hat` in the Chief or by the PauseProxy in MakerDAO. -- `freezeMarket`: Freezes a single market in SparkLend, callable by the `hat` in the Chief or by the PauseProxy in MakerDAO. +A contract that will have `RISK_ADMIN_ROLE` & `EMERGENCY_ADMIN_ROLE` in SparkLend, and has four functions: +- `freezeAllMarkets`: Freezes all markets in SparkLend, callable by the `hat` in the Chief, an authorized contract (ward) or by the PauseProxy in MakerDAO. +- `freezeMarket`: Freezes a single market in SparkLend, callable by the `hat` in the Chief, an authorized contract (ward) or by the PauseProxy in MakerDAO. +- `pauseAllMarkets`: Pauses all markets in SparkLend, callable by the `hat` in the Chief, an authorized contract (ward) or by the PauseProxy in MakerDAO. +- `pauseMarket`: Pauses a single market in SparkLend, callable by the `hat` in the Chief, an authorized contract (ward) or by the PauseProxy in MakerDAO. ### `src/spells/EmergencySpell_SparkLend_FreezeAllAssets.sol` -A spell that can be set as the `hat` in the Chief to freeze all markets in SparkLend by calling `freezeAllMarkets` in `SparkLendFreezerMom`. +A spell that can be set as the `hat` in the Chief to freeze all markets in SparkLend by calling `freezeAllMarkets(true)` in `SparkLendFreezerMom`. ### `src/spells/EmergencySpell_SparkLend_FreezeSingleAsset.sol` -A spell that can be set as the `hat` in the Chief to freeze all markets in SparkLend by calling `freezeMarket` in `SparkLendFreezerMom`. A separate spell is needed for each market, with the reserve being declared in the constructor. +A spell that can be set as the `hat` in the Chief to freeze a specific market in SparkLend by calling `freezeMarket(reserve, true)` in `SparkLendFreezerMom`. A separate spell is needed for each market, with the reserve being declared in the constructor. + +### `src/spells/EmergencySpell_SparkLend_PauseAllAssets.sol` +A spell that can be set as the `hat` in the Chief to pauses all markets in SparkLend by calling `pauseAllMarkets(true)` in `SparkLendFreezerMom`. + +### `src/spells/EmergencySpell_SparkLend_PauseSingleAsset.sol` +A spell that can be set as the `hat` in the Chief to pause a specific market in SparkLend by calling `pauseMarket(reserve, true)` in `SparkLendFreezerMom`. A separate spell is needed for each market, with the reserve being declared in the constructor. ## Testing To run the tests, run `forge test`. diff --git a/src/SparkLendFreezerMom.sol b/src/SparkLendFreezerMom.sol index 6b6afd5..12e89a3 100644 --- a/src/SparkLendFreezerMom.sol +++ b/src/SparkLendFreezerMom.sol @@ -3,18 +3,13 @@ pragma solidity ^0.8.13; import { ISparkLendFreezerMom } from "src/interfaces/ISparkLendFreezerMom.sol"; +import { IPool } from "aave-v3-core/contracts/interfaces/IPool.sol"; +import { IPoolConfigurator } from "aave-v3-core/contracts/interfaces/IPoolConfigurator.sol"; + interface AuthorityLike { function canCall(address src, address dst, bytes4 sig) external view returns (bool); } -interface PoolConfiguratorLike { - function setReserveFreeze(address asset, bool freeze) external; -} - -interface PoolLike { - function getReservesList() external view returns (address[] memory); -} - contract SparkLendFreezerMom is ISparkLendFreezerMom { /**********************************************************************************************/ @@ -26,6 +21,8 @@ contract SparkLendFreezerMom is ISparkLendFreezerMom { address public override authority; address public override owner; + + mapping(address => uint256) public override wards; constructor(address poolConfigurator_, address pool_) { poolConfigurator = poolConfigurator_; @@ -64,23 +61,48 @@ contract SparkLendFreezerMom is ISparkLendFreezerMom { owner = owner_; } + function rely(address usr) external override onlyOwner { + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external override onlyOwner { + wards[usr] = 0; + emit Deny(usr); + } + /**********************************************************************************************/ /*** Auth Functions ***/ /**********************************************************************************************/ - function freezeAllMarkets() external override auth { - address[] memory reserves = PoolLike(pool).getReservesList(); + function freezeAllMarkets(bool freeze) external override auth { + address[] memory reserves = IPool(pool).getReservesList(); + + for (uint256 i = 0; i < reserves.length; i++) { + address reserve = reserves[i]; + IPoolConfigurator(poolConfigurator).setReserveFreeze(reserve, freeze); + emit FreezeMarket(reserve, freeze); + } + } + + function freezeMarket(address reserve, bool freeze) external override auth { + IPoolConfigurator(poolConfigurator).setReserveFreeze(reserve, freeze); + emit FreezeMarket(reserve, freeze); + } + + function pauseAllMarkets(bool pause) external override auth { + address[] memory reserves = IPool(pool).getReservesList(); for (uint256 i = 0; i < reserves.length; i++) { address reserve = reserves[i]; - PoolConfiguratorLike(poolConfigurator).setReserveFreeze(reserve, true); - emit FreezeMarket(reserve); + IPoolConfigurator(poolConfigurator).setReservePause(reserve, pause); + emit PauseMarket(reserve, pause); } } - function freezeMarket(address reserve) external override auth { - PoolConfiguratorLike(poolConfigurator).setReserveFreeze(reserve, true); - emit FreezeMarket(reserve); + function pauseMarket(address reserve, bool pause) external override auth { + IPoolConfigurator(poolConfigurator).setReservePause(reserve, pause); + emit PauseMarket(reserve, pause); } /**********************************************************************************************/ @@ -90,7 +112,7 @@ contract SparkLendFreezerMom is ISparkLendFreezerMom { function isAuthorized(address src, bytes4 sig) internal view returns (bool) { if (src == address(this)) { return true; - } else if (src == owner) { + } else if (src == owner || wards[src] == 1) { return true; } else if (authority == address(0)) { return false; diff --git a/src/interfaces/IExecuteOnceSpell.sol b/src/interfaces/IExecuteOnceSpell.sol new file mode 100644 index 0000000..be2c03a --- /dev/null +++ b/src/interfaces/IExecuteOnceSpell.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +interface IExecuteOnceSpell { + + /** + * @dev Returns true if the spell has been executed. + */ + function executed() external view returns (bool); + + /** + * @dev Executes the spell. Can only be called once. + */ + function execute() external; + +} diff --git a/src/interfaces/ISparkLendFreezerMom.sol b/src/interfaces/ISparkLendFreezerMom.sol index 1ed3c54..d2c0db9 100644 --- a/src/interfaces/ISparkLendFreezerMom.sol +++ b/src/interfaces/ISparkLendFreezerMom.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.0; +pragma solidity >=0.8.0; interface ISparkLendFreezerMom { @@ -11,8 +11,17 @@ interface ISparkLendFreezerMom { * @dev Event to log the freezing of a given market in SparkLend. * @dev NOTE: This event will fire even if the market is already frozen. * @param reserve The address of the market reserve. + * @param freeze A boolean indicating whether the market is frozen or unfrozen. */ - event FreezeMarket(address indexed reserve); + event FreezeMarket(address indexed reserve, bool freeze); + + /** + * @dev Event to log the pausing of a given market in SparkLend. + * @dev NOTE: This event will fire even if the market is already paused. + * @param reserve The address of the market reserve. + * @param pause A boolean indicating whether the market is paused or unpaused. + */ + event PauseMarket(address indexed reserve, bool pause); /** * @dev Event to log the setting of a new owner. @@ -28,6 +37,18 @@ interface ISparkLendFreezerMom { */ event SetAuthority(address indexed oldAuthority, address indexed newAuthority); + /** + * @dev Authorize a contract to trigger this mom. + * @param usr The address to authorize. + */ + event Rely(address indexed usr); + + /** + * @dev Deauthorize a contract to trigger this mom. + * @param usr The address to deauthorize. + */ + event Deny(address indexed usr); + /**********************************************************************************************/ /*** Storage Variables ***/ /**********************************************************************************************/ @@ -56,6 +77,12 @@ interface ISparkLendFreezerMom { */ function owner() external view returns (address); + /** + * @dev Returns if an address is authorized to trigger this mom (or not). + * @return 1 if authorized, 0 if not. + */ + function wards(address usr) external view returns (uint256); + /**********************************************************************************************/ /*** Owner Functions ***/ /**********************************************************************************************/ @@ -72,25 +99,58 @@ interface ISparkLendFreezerMom { */ function setOwner(address owner) external; + /** + * @dev Authorize a contract to trigger this mom. + * @param usr The address to authorize. + */ + function rely(address usr) external; + + /** + * @dev Deauthorize a contract to trigger this mom. + * @param usr The address to deauthorize. + */ + function deny(address usr) external; + /**********************************************************************************************/ /*** Auth Functions ***/ /**********************************************************************************************/ /** * @dev Function to freeze a specified market. Permissioned using the isAuthorized function - * which allows the owner, the freezer contract itself, or the `hat` in the Chief - * to call the function. Note that the `authority` in this contract is assumed to be - * the Chief in the MakerDAO protocol. + * which allows the owner, a ward, the freezer contract itself, or the `hat` in the + * Chief to call the function. Note that the `authority` in this contract is assumed to + * be the Chief in the MakerDAO protocol. * @param reserve The address of the market to freeze. + * @param freeze A boolean indicating whether to freeze or unfreeze the market. + */ + function freezeMarket(address reserve, bool freeze) external; + + /** + * @dev Function to freeze all markets. Permissioned using the isAuthorized function + * which allows the owner, a ward, the freezer contract itself, or the `hat` in the + * Chief to call the function. Note that the `authority` in this contract is assumed to + * be the Chief in the MakerDAO protocol. + * @param freeze A boolean indicating whether to freeze or unfreeze the market. + */ + function freezeAllMarkets(bool freeze) external; + + /** + * @dev Function to pause a specified market. Permissioned using the isAuthorized function + * which allows the owner, a ward, the freezer contract itself, or the `hat` in the + * Chief to call the function. Note that the `authority` in this contract is assumed to + * be the Chief in the MakerDAO protocol. + * @param reserve The address of the market to pause. + * @param pause A boolean indicating whether to pause or unpause the market. */ - function freezeMarket(address reserve) external; + function pauseMarket(address reserve, bool pause) external; /** - * @dev Function to freeze all markets. Permissioned using the isAuthorized function - * which allows the owner, the freezer contract itself, or the `hat` in the Chief - * to call the function. Note that the `authority` in this contract is assumed to be - * the Chief in the MakerDAO protocol. + * @dev Function to pause all markets. Permissioned using the isAuthorized function + * which allows the owner, a ward, the freezer contract itself, or the `hat` in the + * Chief to call the function. Note that the `authority` in this contract is assumed to + * be the Chief in the MakerDAO protocol. + * @param pause A boolean indicating whether to pause or unpause the market. */ - function freezeAllMarkets() external; + function pauseAllMarkets(bool pause) external; } diff --git a/src/spells/EmergencySpell_SparkLend_FreezeAllAssets.sol b/src/spells/EmergencySpell_SparkLend_FreezeAllAssets.sol index c75534d..b2d2f1a 100644 --- a/src/spells/EmergencySpell_SparkLend_FreezeAllAssets.sol +++ b/src/spells/EmergencySpell_SparkLend_FreezeAllAssets.sol @@ -1,22 +1,23 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; +import { IExecuteOnceSpell } from "src/interfaces/IExecuteOnceSpell.sol"; import { ISparkLendFreezerMom } from "src/interfaces/ISparkLendFreezerMom.sol"; -contract EmergencySpell_SparkLend_FreezeAllAssets { +contract EmergencySpell_SparkLend_FreezeAllAssets is IExecuteOnceSpell { address public immutable sparkLendFreezerMom; - bool public executed; + bool public override executed; - constructor(address sparklendFreezerMom_) { - sparkLendFreezerMom = sparklendFreezerMom_; + constructor(address sparkLendFreezerMom_) { + sparkLendFreezerMom = sparkLendFreezerMom_; } - function freeze() external { + function execute() external override { require(!executed, "FreezeAllAssetsSpell/already-executed"); executed = true; - ISparkLendFreezerMom(sparkLendFreezerMom).freezeAllMarkets(); + ISparkLendFreezerMom(sparkLendFreezerMom).freezeAllMarkets(true); } } diff --git a/src/spells/EmergencySpell_SparkLend_FreezeSingleAsset.sol b/src/spells/EmergencySpell_SparkLend_FreezeSingleAsset.sol index 6381290..74a99ae 100644 --- a/src/spells/EmergencySpell_SparkLend_FreezeSingleAsset.sol +++ b/src/spells/EmergencySpell_SparkLend_FreezeSingleAsset.sol @@ -1,24 +1,25 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; +import { IExecuteOnceSpell } from "src/interfaces/IExecuteOnceSpell.sol"; import { ISparkLendFreezerMom } from "src/interfaces/ISparkLendFreezerMom.sol"; -contract EmergencySpell_SparkLend_FreezeSingleAsset { +contract EmergencySpell_SparkLend_FreezeSingleAsset is IExecuteOnceSpell { address public immutable sparkLendFreezerMom; address public immutable reserve; - bool public executed; + bool public override executed; - constructor(address sparklendFreezerMom_, address reserve_) { - sparkLendFreezerMom = sparklendFreezerMom_; + constructor(address sparkLendFreezerMom_, address reserve_) { + sparkLendFreezerMom = sparkLendFreezerMom_; reserve = reserve_; } - function freeze() external { + function execute() external override { require(!executed, "FreezeSingleAssetSpell/already-executed"); executed = true; - ISparkLendFreezerMom(sparkLendFreezerMom).freezeMarket(reserve); + ISparkLendFreezerMom(sparkLendFreezerMom).freezeMarket(reserve, true); } } diff --git a/src/spells/EmergencySpell_SparkLend_PauseAllAssets.sol b/src/spells/EmergencySpell_SparkLend_PauseAllAssets.sol new file mode 100644 index 0000000..3449267 --- /dev/null +++ b/src/spells/EmergencySpell_SparkLend_PauseAllAssets.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { IExecuteOnceSpell } from "src/interfaces/IExecuteOnceSpell.sol"; +import { ISparkLendFreezerMom } from "src/interfaces/ISparkLendFreezerMom.sol"; + +contract EmergencySpell_SparkLend_PauseAllAssets is IExecuteOnceSpell { + + address public immutable sparkLendFreezerMom; + + bool public override executed; + + constructor(address sparkLendFreezerMom_) { + sparkLendFreezerMom = sparkLendFreezerMom_; + } + + function execute() external override { + require(!executed, "PauseAllAssetsSpell/already-executed"); + executed = true; + ISparkLendFreezerMom(sparkLendFreezerMom).pauseAllMarkets(true); + } + +} diff --git a/src/spells/EmergencySpell_SparkLend_PauseSingleAsset.sol b/src/spells/EmergencySpell_SparkLend_PauseSingleAsset.sol new file mode 100644 index 0000000..d19d295 --- /dev/null +++ b/src/spells/EmergencySpell_SparkLend_PauseSingleAsset.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { IExecuteOnceSpell } from "src/interfaces/IExecuteOnceSpell.sol"; +import { ISparkLendFreezerMom } from "src/interfaces/ISparkLendFreezerMom.sol"; + +contract EmergencySpell_SparkLend_PauseSingleAsset is IExecuteOnceSpell { + + address public immutable sparkLendFreezerMom; + address public immutable reserve; + + bool public override executed; + + constructor(address sparkLendFreezerMom_, address reserve_) { + sparkLendFreezerMom = sparkLendFreezerMom_; + reserve = reserve_; + } + + function execute() external override { + require(!executed, "PauseSingleAssetSpell/already-executed"); + executed = true; + ISparkLendFreezerMom(sparkLendFreezerMom).pauseMarket(reserve, true); + } + +} diff --git a/test/IntegrationTests.t.sol b/test/IntegrationTests.t.sol index 249aea8..b89eb97 100644 --- a/test/IntegrationTests.t.sol +++ b/test/IntegrationTests.t.sol @@ -6,6 +6,7 @@ import "forge-std/Test.sol"; import { SafeERC20 } from "lib/erc20-helpers/src/SafeERC20.sol"; import { IERC20 } from "lib/erc20-helpers/src/interfaces/IERC20.sol"; +import { IExecuteOnceSpell } from "src/interfaces/IExecuteOnceSpell.sol"; import { SparkLendFreezerMom } from "src/SparkLendFreezerMom.sol"; import { EmergencySpell_SparkLend_FreezeSingleAsset as FreezeSingleAssetSpell } @@ -14,6 +15,12 @@ import { EmergencySpell_SparkLend_FreezeSingleAsset as FreezeSingleAssetSpell } import { EmergencySpell_SparkLend_FreezeAllAssets as FreezeAllAssetsSpell } from "src/spells/EmergencySpell_SparkLend_FreezeAllAssets.sol"; +import { EmergencySpell_SparkLend_PauseSingleAsset as PauseSingleAssetSpell } + from "src/spells/EmergencySpell_SparkLend_PauseSingleAsset.sol"; + +import { EmergencySpell_SparkLend_PauseAllAssets as PauseAllAssetsSpell } + from "src/spells/EmergencySpell_SparkLend_PauseAllAssets.sol"; + import { IACLManager } from "lib/aave-v3-core/contracts/interfaces/IACLManager.sol"; import { IPoolConfigurator } from "lib/aave-v3-core/contracts/interfaces/IPoolConfigurator.sol"; import { IPoolDataProvider } from "lib/aave-v3-core/contracts/interfaces/IPoolDataProvider.sol"; @@ -37,6 +44,7 @@ contract IntegrationTestsBase is Test { address mkrWhale = makeAddr("mkrWhale"); address randomUser = makeAddr("randomUser"); + address multisig = makeAddr("multisig"); IAuthorityLike authority = IAuthorityLike(AUTHORITY); IACLManager aclManager = IACLManager(ACL_MANAGER); @@ -44,42 +52,22 @@ contract IntegrationTestsBase is Test { IPoolConfigurator poolConfig = IPoolConfigurator(POOL_CONFIG); IPoolDataProvider dataProvider = IPoolDataProvider(DATA_PROVIDER); - SparkLendFreezerMom freezer; + SparkLendFreezerMom freezerMom; function setUp() public virtual { - vm.createSelectFork(getChain('mainnet').rpcUrl, 18_621_350); + vm.createSelectFork(getChain('mainnet').rpcUrl, 18_706_418); - freezer = new SparkLendFreezerMom(POOL_CONFIG, POOL); + freezerMom = new SparkLendFreezerMom(POOL_CONFIG, POOL); - freezer.setAuthority(AUTHORITY); - freezer.setOwner(PAUSE_PROXY); - } - - function _vote(address spell) internal { - uint256 amount = 1_000_000 ether; - - deal(MKR, mkrWhale, amount); - - vm.startPrank(mkrWhale); - IERC20(MKR).approve(AUTHORITY, amount); - authority.lock(amount); - - address[] memory slate = new address[](1); - slate[0] = spell; - authority.vote(slate); - - vm.roll(block.number + 1); - - authority.lift(spell); - - vm.stopPrank(); - - assertTrue(authority.hat() == spell); + freezerMom.setAuthority(AUTHORITY); + freezerMom.setOwner(PAUSE_PROXY); + vm.prank(PAUSE_PROXY); + freezerMom.rely(multisig); } // NOTE: For all checks, not checking pool.swapBorrowRateMode() since stable rate // isn't enabled on any reserve. - function _checkUserActionsUnfrozen( + function _checkAllUserActionsAvailable( address asset, uint256 supplyAmount, uint256 withdrawAmount, @@ -89,6 +77,7 @@ contract IntegrationTestsBase is Test { internal { assertEq(_isFrozen(asset), false); + assertEq(_isPaused(asset), false); // Make a new address for each asset to avoid side effects, eg. siloed borrowing address sparkUser = makeAddr(string.concat(IERC20(asset).name(), " user")); @@ -153,6 +142,43 @@ contract IntegrationTestsBase is Test { vm.stopPrank(); } + function _checkUserActionsPaused( + address asset, + uint256 supplyAmount, + uint256 withdrawAmount, + uint256 borrowAmount, + uint256 repayAmount + ) + internal + { + assertEq(_isPaused(asset), true); + + // Use same address that borrowed when unfrozen to repay debt + address sparkUser = makeAddr(string.concat(IERC20(asset).name(), " user")); + + vm.startPrank(sparkUser); + + // User can't supply + vm.expectRevert(bytes("29")); // RESERVE_PAUSED + pool.supply(asset, supplyAmount, sparkUser, 0); + + // User can't borrow + vm.expectRevert(bytes("29")); // RESERVE_PAUSED + pool.borrow(asset, borrowAmount, 2, 0, sparkUser); + + // User can't withdraw collateral + vm.expectRevert(bytes("29")); // RESERVE_PAUSED + pool.withdraw(asset, withdrawAmount, sparkUser); + + // User can't repay + if (_borrowingEnabled(asset)) { + vm.expectRevert(bytes("29")); // RESERVE_PAUSED + pool.repay(asset, repayAmount, 2, sparkUser); + } + + vm.stopPrank(); + } + function _supplyWethCollateral(address user, uint256 amount) internal { bool frozenWeth = _isFrozen(WETH); @@ -180,6 +206,10 @@ contract IntegrationTestsBase is Test { ( ,,,,,,,,, isFrozen ) = dataProvider.getReserveConfigurationData(asset); } + function _isPaused(address asset) internal view returns (bool isPaused) { + return dataProvider.getPaused(asset); + } + function _borrowingEnabled(address asset) internal view returns (bool borrowingEnabled) { ( ,,,,,, borrowingEnabled,,, ) = dataProvider.getReserveConfigurationData(asset); } @@ -196,133 +226,105 @@ contract IntegrationTestsBase is Test { } -contract FreezeSingleAssetSpellFailures is IntegrationTestsBase { +abstract contract ExecuteOnceSpellTests is IntegrationTestsBase { - FreezeSingleAssetSpell freezeAssetSpell; + IExecuteOnceSpell spell; + bool isPauseSpell; + string contractName; - function setUp() public override { + function setUp() public virtual override { super.setUp(); - freezeAssetSpell = new FreezeSingleAssetSpell(address(freezer), WETH); - } - - function test_cannotCallWithoutHat() external { - assertTrue(authority.hat() != address(freezeAssetSpell)); - assertTrue( - !authority.canCall( - address(freezeAssetSpell), - address(freezer), - freezer.freezeMarket.selector - ) - ); - vm.expectRevert("SparkLendFreezerMom/not-authorized"); - freezeAssetSpell.freeze(); + vm.startPrank(SPARK_PROXY); + aclManager.addEmergencyAdmin(address(freezerMom)); + aclManager.addRiskAdmin(address(freezerMom)); + vm.stopPrank(); } - function test_cannotCallWithoutRoleSetup() external { - _vote(address(freezeAssetSpell)); - - assertTrue(authority.hat() == address(freezeAssetSpell)); - assertTrue( - authority.canCall( - address(freezeAssetSpell), - address(freezer), - freezer.freezeMarket.selector - ) - ); - - vm.expectRevert(bytes("4")); // CALLER_NOT_RISK_OR_POOL_ADMIN - freezeAssetSpell.freeze(); + function _vote() internal { + _vote(address(spell)); } - function test_cannotCallTwice() external { - vm.prank(SPARK_PROXY); - aclManager.addRiskAdmin(address(freezer)); - - _vote(address(freezeAssetSpell)); + function _vote(address _spell) internal { + uint256 amount = 1_000_000 ether; - assertTrue(authority.hat() == address(freezeAssetSpell)); - assertTrue( - authority.canCall( - address(freezeAssetSpell), - address(freezer), - freezer.freezeMarket.selector - ) - ); + deal(MKR, mkrWhale, amount); - vm.startPrank(randomUser); // Demonstrate no ACL in spell - freezeAssetSpell.freeze(); + vm.startPrank(mkrWhale); + IERC20(MKR).approve(AUTHORITY, amount); + authority.lock(amount); - vm.expectRevert("FreezeSingleAssetSpell/already-executed"); - freezeAssetSpell.freeze(); - } + address[] memory slate = new address[](1); + slate[0] = _spell; + authority.vote(slate); -} + vm.roll(block.number + 1); -contract FreezeAllAssetsSpellFailures is IntegrationTestsBase { + authority.lift(_spell); - FreezeAllAssetsSpell freezeAllAssetsSpell; + vm.stopPrank(); - function setUp() public override { - super.setUp(); - freezeAllAssetsSpell = new FreezeAllAssetsSpell(address(freezer)); + assertTrue(authority.hat() == _spell); } function test_cannotCallWithoutHat() external { - assertTrue(authority.hat() != address(freezeAllAssetsSpell)); + assertTrue(authority.hat() != address(spell)); assertTrue( !authority.canCall( - address(freezeAllAssetsSpell), - address(freezer), - freezer.freezeAllMarkets.selector + address(spell), + address(freezerMom), + freezerMom.freezeMarket.selector ) ); vm.expectRevert("SparkLendFreezerMom/not-authorized"); - freezeAllAssetsSpell.freeze(); + spell.execute(); } function test_cannotCallWithoutRoleSetup() external { - _vote(address(freezeAllAssetsSpell)); + vm.startPrank(SPARK_PROXY); + aclManager.removeEmergencyAdmin(address(freezerMom)); + aclManager.removeRiskAdmin(address(freezerMom)); + vm.stopPrank(); + + _vote(); - assertTrue(authority.hat() == address(freezeAllAssetsSpell)); + assertTrue(authority.hat() == address(spell)); assertTrue( authority.canCall( - address(freezeAllAssetsSpell), - address(freezer), - freezer.freezeAllMarkets.selector + address(spell), + address(freezerMom), + freezerMom.freezeMarket.selector ) ); - vm.expectRevert(bytes("4")); // CALLER_NOT_RISK_OR_POOL_ADMIN - freezeAllAssetsSpell.freeze(); + if (isPauseSpell) vm.expectRevert(bytes("3")); // CALLER_NOT_POOL_OR_EMERGENCY_ADMIN + else vm.expectRevert(bytes("4")); // CALLER_NOT_RISK_OR_POOL_ADMIN + spell.execute(); } function test_cannotCallTwice() external { - vm.prank(SPARK_PROXY); - aclManager.addRiskAdmin(address(freezer)); + _vote(); - _vote(address(freezeAllAssetsSpell)); - - assertTrue(authority.hat() == address(freezeAllAssetsSpell)); + assertTrue(authority.hat() == address(spell)); assertTrue( authority.canCall( - address(freezeAllAssetsSpell), - address(freezer), - freezer.freezeMarket.selector + address(spell), + address(freezerMom), + freezerMom.freezeMarket.selector ) ); vm.startPrank(randomUser); // Demonstrate no ACL in spell - freezeAllAssetsSpell.freeze(); + spell.execute(); - vm.expectRevert("FreezeAllAssetsSpell/already-executed"); - freezeAllAssetsSpell.freeze(); + vm.expectRevert(bytes(string.concat(contractName, "/already-executed"))); + spell.execute(); } } -contract FreezeSingleAssetSpellTest is IntegrationTestsBase { +contract FreezeSingleAssetSpellTest is ExecuteOnceSpellTests { using SafeERC20 for IERC20; @@ -330,8 +332,11 @@ contract FreezeSingleAssetSpellTest is IntegrationTestsBase { function setUp() public override { super.setUp(); - vm.prank(SPARK_PROXY); - aclManager.addRiskAdmin(address(freezer)); + + // For the revert testing + spell = new FreezeSingleAssetSpell(address(freezerMom), WETH); + isPauseSpell = false; + contractName = "FreezeSingleAssetSpell"; } function test_freezeAssetSpell_allAssets() external { @@ -360,7 +365,7 @@ contract FreezeSingleAssetSpellTest is IntegrationTestsBase { uint256 snapshot = vm.snapshot(); address freezeAssetSpell - = address(new FreezeSingleAssetSpell(address(freezer), asset)); + = address(new FreezeSingleAssetSpell(address(freezerMom), asset)); // Setup spell, max out supply caps so that they aren't hit during test _vote(freezeAssetSpell); @@ -370,7 +375,7 @@ contract FreezeSingleAssetSpellTest is IntegrationTestsBase { poolConfig.setBorrowCap(asset, 0); vm.stopPrank(); - _checkUserActionsUnfrozen( + _checkAllUserActionsAvailable( asset, supplyAmount, withdrawAmount, @@ -381,7 +386,7 @@ contract FreezeSingleAssetSpellTest is IntegrationTestsBase { assertEq(FreezeSingleAssetSpell(freezeAssetSpell).executed(), false); vm.prank(randomUser); // Demonstrate no ACL in spell - FreezeSingleAssetSpell(freezeAssetSpell).freeze(); + FreezeSingleAssetSpell(freezeAssetSpell).execute(); assertEq(FreezeSingleAssetSpell(freezeAssetSpell).executed(), true); @@ -396,7 +401,7 @@ contract FreezeSingleAssetSpellTest is IntegrationTestsBase { vm.prank(SPARK_PROXY); poolConfig.setReserveFreeze(asset, false); - _checkUserActionsUnfrozen( + _checkAllUserActionsAvailable( asset, supplyAmount, withdrawAmount, @@ -410,7 +415,7 @@ contract FreezeSingleAssetSpellTest is IntegrationTestsBase { } -contract FreezeAllAssetsSpellTest is IntegrationTestsBase { +contract FreezeAllAssetsSpellTest is ExecuteOnceSpellTests { using SafeERC20 for IERC20; @@ -418,8 +423,10 @@ contract FreezeAllAssetsSpellTest is IntegrationTestsBase { function setUp() public override { super.setUp(); - vm.prank(SPARK_PROXY); - aclManager.addRiskAdmin(address(freezer)); + + spell = new FreezeAllAssetsSpell(address(freezerMom)); + isPauseSpell = false; + contractName = "FreezeAllAssetsSpell"; } function test_freezeAllAssetsSpell() external { @@ -432,10 +439,8 @@ contract FreezeAllAssetsSpellTest is IntegrationTestsBase { uint256 borrowAmount = 2; uint256 repayAmount = 1; - address freezeAllAssetsSpell = address(new FreezeAllAssetsSpell(address(freezer))); - // Setup spell - _vote(freezeAllAssetsSpell); + _vote(); // Check that protocol is working as expected before spell for each asset for (uint256 i = 0; i < reserves.length; i++) { @@ -454,7 +459,7 @@ contract FreezeAllAssetsSpellTest is IntegrationTestsBase { poolConfig.setBorrowCap(asset, 0); vm.stopPrank(); - _checkUserActionsUnfrozen( + _checkAllUserActionsAvailable( asset, supplyAmount * 10 ** decimals, withdrawAmount * 10 ** decimals, @@ -465,20 +470,19 @@ contract FreezeAllAssetsSpellTest is IntegrationTestsBase { assertEq(untestedReserves.length, 0); - assertEq(FreezeAllAssetsSpell(freezeAllAssetsSpell).executed(), false); + assertEq(spell.executed(), false); // Freeze all assets in the protocol vm.prank(randomUser); // Demonstrate no ACL in spell - FreezeAllAssetsSpell(freezeAllAssetsSpell).freeze(); + spell.execute(); - assertEq(FreezeAllAssetsSpell(freezeAllAssetsSpell).executed(), true); + assertEq(spell.executed(), true); // Check that protocol is working as expected after the freeze spell for all assets for (uint256 i = 0; i < reserves.length; i++) { address asset = reserves[i]; uint256 decimals = IERC20(asset).decimals(); - // Check all assets, including unfrozen ones from start _checkUserActionsFrozen( asset, supplyAmount * 10 ** decimals, @@ -488,7 +492,7 @@ contract FreezeAllAssetsSpellTest is IntegrationTestsBase { ); } - // Undo all freezes, including WBTC and make sure that protocol is back to working + // Undo all freezes and make sure that protocol is back to working // as expected for (uint256 i = 0; i < reserves.length; i++) { address asset = reserves[i]; @@ -497,7 +501,178 @@ contract FreezeAllAssetsSpellTest is IntegrationTestsBase { vm.prank(SPARK_PROXY); poolConfig.setReserveFreeze(asset, false); - _checkUserActionsUnfrozen( + _checkAllUserActionsAvailable( + asset, + supplyAmount * 10 ** decimals, + withdrawAmount * 10 ** decimals, + borrowAmount * 10 ** decimals, + repayAmount * 10 ** decimals + ); + } + } + +} + +contract PauseSingleAssetSpellTest is ExecuteOnceSpellTests { + + using SafeERC20 for IERC20; + + address[] public untestedReserves; + + function setUp() public override { + super.setUp(); + + // For the revert testing + spell = new PauseSingleAssetSpell(address(freezerMom), WETH); + isPauseSpell = true; + contractName = "PauseSingleAssetSpell"; + } + + function test_pauseAssetSpell_allAssets() external { + address[] memory reserves = pool.getReservesList(); + + assertEq(reserves.length, 9); + + for (uint256 i = 0; i < reserves.length; i++) { + address asset = reserves[i]; + + // If the asset is paused on mainnet, skip the test + if (_isPaused(asset)) { + untestedReserves.push(asset); + continue; + } + + assertEq(untestedReserves.length, 0); + + uint256 decimals = IERC20(asset).decimals(); + + uint256 supplyAmount = 1_000 * 10 ** decimals; + uint256 withdrawAmount = 1 * 10 ** decimals; + uint256 borrowAmount = 2 * 10 ** decimals; + uint256 repayAmount = 1 * 10 ** decimals; + + uint256 snapshot = vm.snapshot(); + + address pauseAssetSpell + = address(new PauseSingleAssetSpell(address(freezerMom), asset)); + + // Setup spell, max out supply caps so that they aren't hit during test + _vote(pauseAssetSpell); + + vm.startPrank(SPARK_PROXY); + poolConfig.setSupplyCap(asset, 0); + poolConfig.setBorrowCap(asset, 0); + vm.stopPrank(); + + _checkAllUserActionsAvailable( + asset, + supplyAmount, + withdrawAmount, + borrowAmount, + repayAmount + ); + + assertEq(PauseSingleAssetSpell(pauseAssetSpell).executed(), false); + + vm.prank(randomUser); // Demonstrate no ACL in spell + PauseSingleAssetSpell(pauseAssetSpell).execute(); + + assertEq(PauseSingleAssetSpell(pauseAssetSpell).executed(), true); + + _checkUserActionsPaused( + asset, + supplyAmount, + withdrawAmount, + borrowAmount, + repayAmount + ); + + vm.prank(SPARK_PROXY); + poolConfig.setReservePause(asset, false); + + _checkAllUserActionsAvailable( + asset, + supplyAmount, + withdrawAmount, + borrowAmount, + repayAmount + ); + + vm.revertTo(snapshot); + } + } + +} + +contract PauseAllAssetsSpellTest is ExecuteOnceSpellTests { + + using SafeERC20 for IERC20; + + address[] public untestedReserves; + + function setUp() public override { + super.setUp(); + + spell = new PauseAllAssetsSpell(address(freezerMom)); + isPauseSpell = true; + contractName = "PauseAllAssetsSpell"; + } + + function test_pauseAllAssetsSpell() external { + address[] memory reserves = pool.getReservesList(); + + assertEq(reserves.length, 9); + + uint256 supplyAmount = 1_000; + uint256 withdrawAmount = 1; + uint256 borrowAmount = 2; + uint256 repayAmount = 1; + + // Setup spell + _vote(); + + // Check that protocol is working as expected before spell for each asset + for (uint256 i = 0; i < reserves.length; i++) { + address asset = reserves[i]; + uint256 decimals = IERC20(asset).decimals(); + + // If the asset is already paused on mainnet, skip the test + if (_isPaused(asset)) { + untestedReserves.push(asset); + continue; + } + + // Max out supply caps for this asset so that they aren't hit during test + vm.startPrank(SPARK_PROXY); + poolConfig.setSupplyCap(asset, 0); + poolConfig.setBorrowCap(asset, 0); + vm.stopPrank(); + + _checkAllUserActionsAvailable( + asset, + supplyAmount * 10 ** decimals, + withdrawAmount * 10 ** decimals, + borrowAmount * 10 ** decimals, + repayAmount * 10 ** decimals + ); + } + + assertEq(untestedReserves.length, 0); + + assertEq(spell.executed(), false); + + // Pause all assets in the protocol + vm.prank(randomUser); // Demonstrate no ACL in spell + spell.execute(); + + assertEq(spell.executed(), true); + + // Check that protocol is working as expected after the pause spell for all assets + for (uint256 i = 0; i < reserves.length; i++) { + address asset = reserves[i]; + uint256 decimals = IERC20(asset).decimals(); + + _checkUserActionsPaused( asset, supplyAmount * 10 ** decimals, withdrawAmount * 10 ** decimals, @@ -505,6 +680,114 @@ contract FreezeAllAssetsSpellTest is IntegrationTestsBase { repayAmount * 10 ** decimals ); } + + // Undo all pauses and make sure that protocol is back to working + // as expected + for (uint256 i = 0; i < reserves.length; i++) { + vm.prank(SPARK_PROXY); + poolConfig.setReservePause(reserves[i], false); + } + + // Need to unpause all first before checking that protocol is working + for (uint256 i = 0; i < reserves.length; i++) { + address asset = reserves[i]; + uint256 decimals = IERC20(asset).decimals(); + + _checkAllUserActionsAvailable( + asset, + supplyAmount * 10 ** decimals, + withdrawAmount * 10 ** decimals, + borrowAmount * 10 ** decimals, + repayAmount * 10 ** decimals + ); + } + } + +} + +contract MultisigTest is IntegrationTestsBase { + + function setUp() public override { + super.setUp(); + + vm.startPrank(SPARK_PROXY); + + aclManager.addRiskAdmin(address(freezerMom)); + aclManager.addEmergencyAdmin(address(freezerMom)); + + vm.stopPrank(); + } + + function test_freezeSingleAsset() public { + assertEq(_isFrozen(WETH), false); + + vm.prank(multisig); + freezerMom.freezeMarket(WETH, true); + + assertEq(_isFrozen(WETH), true); + + vm.prank(multisig); + freezerMom.freezeMarket(WETH, false); + + assertEq(_isFrozen(WETH), false); + } + + function test_freezeAllAssets() public { + address[] memory reserves = pool.getReservesList(); + + for (uint256 i = 0; i < reserves.length; i++) { + assertEq(_isFrozen(reserves[i]), false); + } + + vm.prank(multisig); + freezerMom.freezeAllMarkets(true); + + for (uint256 i = 0; i < reserves.length; i++) { + assertEq(_isFrozen(reserves[i]), true); + } + + vm.prank(multisig); + freezerMom.freezeAllMarkets(false); + + for (uint256 i = 0; i < reserves.length; i++) { + assertEq(_isFrozen(reserves[i]), false); + } + } + + function test_pauseSingleAsset() public { + assertEq(_isPaused(WETH), false); + + vm.prank(multisig); + freezerMom.pauseMarket(WETH, true); + + assertEq(_isPaused(WETH), true); + + vm.prank(multisig); + freezerMom.pauseMarket(WETH, false); + + assertEq(_isPaused(WETH), false); + } + + function test_pauseAllAssets() public { + address[] memory reserves = pool.getReservesList(); + + for (uint256 i = 0; i < reserves.length; i++) { + assertEq(_isPaused(reserves[i]), false); + } + + vm.prank(multisig); + freezerMom.pauseAllMarkets(true); + + for (uint256 i = 0; i < reserves.length; i++) { + assertEq(_isPaused(reserves[i]), true); + } + + vm.prank(multisig); + freezerMom.pauseAllMarkets(false); + + for (uint256 i = 0; i < reserves.length; i++) { + assertEq(_isPaused(reserves[i]), false); + } } } diff --git a/test/Mocks.sol b/test/Mocks.sol index da6467b..8dd6881 100644 --- a/test/Mocks.sol +++ b/test/Mocks.sol @@ -25,6 +25,8 @@ contract ConfiguratorMock { function setReserveFreeze(address asset, bool freeze) external {} + function setReservePause(address asset, bool pause) external {} + } contract PoolMock { diff --git a/test/SparkLendFreezerMom.t.sol b/test/SparkLendFreezerMom.t.sol index f1e136c..7d1f6f7 100644 --- a/test/SparkLendFreezerMom.t.sol +++ b/test/SparkLendFreezerMom.t.sol @@ -83,11 +83,55 @@ contract SetAuthorityTests is SparkLendFreezerMomUnitTestBase { } +contract RelyTests is SparkLendFreezerMomUnitTestBase { + + function test_rely_noAuth() public { + vm.expectRevert("SparkLendFreezerMom/only-owner"); + freezer.rely(makeAddr("authedContract")); + } + + function test_rely() public { + address authedContract = makeAddr("authedContract"); + assertEq(freezer.wards(authedContract), 0); + + vm.prank(owner); + freezer.rely(authedContract); + + assertEq(freezer.wards(authedContract), 1); + } + +} + +contract DenyTests is SparkLendFreezerMomUnitTestBase { + + function test_deny_noAuth() public { + vm.expectRevert("SparkLendFreezerMom/only-owner"); + freezer.deny(makeAddr("authedContract")); + } + + function test_deny() public { + address authedContract = makeAddr("authedContract"); + + vm.prank(owner); + freezer.rely(authedContract); + + assertEq(freezer.wards(authedContract), 1); + + vm.prank(owner); + freezer.deny(authedContract); + + assertEq(freezer.wards(authedContract), 0); + } + +} + contract FreezeAllMarketsTests is SparkLendFreezerMomUnitTestBase { function test_freezeAllMarkets_noAuth() public { vm.expectRevert("SparkLendFreezerMom/not-authorized"); - freezer.freezeAllMarkets(); + freezer.freezeAllMarkets(false); + vm.expectRevert("SparkLendFreezerMom/not-authorized"); + freezer.freezeAllMarkets(true); } function test_freezeAllMarkets() public { @@ -113,7 +157,13 @@ contract FreezeAllMarketsTests is SparkLendFreezerMomUnitTestBase { vm.expectCall(pool, abi.encodePacked(poolSig)); vm.expectCall(configurator, abi.encodePacked(configSig, abi.encode(asset1, true))); vm.expectCall(configurator, abi.encodePacked(configSig, abi.encode(asset2, true))); - freezer.freezeAllMarkets(); + freezer.freezeAllMarkets(true); + + vm.prank(caller); + vm.expectCall(pool, abi.encodePacked(poolSig)); + vm.expectCall(configurator, abi.encodePacked(configSig, abi.encode(asset1, false))); + vm.expectCall(configurator, abi.encodePacked(configSig, abi.encode(asset2, false))); + freezer.freezeAllMarkets(false); } } @@ -124,7 +174,9 @@ contract FreezeMarketTests is SparkLendFreezerMomUnitTestBase { function test_freezeMarket_noAuth() public { vm.expectRevert("SparkLendFreezerMom/not-authorized"); - freezer.freezeMarket(reserve); + freezer.freezeMarket(reserve, false); + vm.expectRevert("SparkLendFreezerMom/not-authorized"); + freezer.freezeMarket(reserve, true); } function test_freezeMarket() public { @@ -141,7 +193,88 @@ contract FreezeMarketTests is SparkLendFreezerMomUnitTestBase { vm.prank(caller); vm.expectCall(configurator, abi.encodePacked(configSig, abi.encode(reserve, true))); - freezer.freezeMarket(reserve); + freezer.freezeMarket(reserve, true); + + vm.prank(caller); + vm.expectCall(configurator, abi.encodePacked(configSig, abi.encode(reserve, false))); + freezer.freezeMarket(reserve, false); + } + +} + +contract PauseAllMarketsTests is SparkLendFreezerMomUnitTestBase { + + function test_pauseAllMarkets_noAuth() public { + vm.expectRevert("SparkLendFreezerMom/not-authorized"); + freezer.pauseAllMarkets(false); + vm.expectRevert("SparkLendFreezerMom/not-authorized"); + freezer.pauseAllMarkets(true); + } + + function test_pauseAllMarkets() public { + address caller = makeAddr("caller"); + + authority.__setCanCall( + caller, + address(freezer), + freezer.pauseAllMarkets.selector, + true + ); + + address asset1 = makeAddr("asset1"); + address asset2 = makeAddr("asset2"); + + PoolMock(pool).__addAsset(asset1); + PoolMock(pool).__addAsset(asset2); + + bytes4 poolSig = PoolMock.getReservesList.selector; + bytes4 configSig = ConfiguratorMock.setReservePause.selector; + + vm.prank(caller); + vm.expectCall(pool, abi.encodePacked(poolSig)); + vm.expectCall(configurator, abi.encodePacked(configSig, abi.encode(asset1, true))); + vm.expectCall(configurator, abi.encodePacked(configSig, abi.encode(asset2, true))); + freezer.pauseAllMarkets(true); + + vm.prank(caller); + vm.expectCall(pool, abi.encodePacked(poolSig)); + vm.expectCall(configurator, abi.encodePacked(configSig, abi.encode(asset1, false))); + vm.expectCall(configurator, abi.encodePacked(configSig, abi.encode(asset2, false))); + freezer.pauseAllMarkets(false); + } + +} + +contract PauseMarketTests is SparkLendFreezerMomUnitTestBase { + + address reserve = makeAddr("reserve"); + + function test_pauseMarket_noAuth() public { + vm.expectRevert("SparkLendFreezerMom/not-authorized"); + freezer.pauseMarket(reserve, false); + vm.expectRevert("SparkLendFreezerMom/not-authorized"); + freezer.pauseMarket(reserve, true); + } + + function test_pauseMarket() public { + address caller = makeAddr("caller"); + + authority.__setCanCall( + caller, + address(freezer), + freezer.pauseMarket.selector, + true + ); + + bytes4 configSig = ConfiguratorMock.setReservePause.selector; + + vm.prank(caller); + vm.expectCall(configurator, abi.encodePacked(configSig, abi.encode(reserve, true))); + freezer.pauseMarket(reserve, true); + + vm.prank(caller); + vm.expectCall(configurator, abi.encodePacked(configSig, abi.encode(reserve, false))); + freezer.pauseMarket(reserve, false); } } @@ -151,6 +284,7 @@ contract SparkLendFreezerMomIsAuthorizedTest is Test { address public configurator; address public pool; address public owner; + address public authedContract; AuthorityMock public authority; @@ -159,13 +293,15 @@ contract SparkLendFreezerMomIsAuthorizedTest is Test { address caller = makeAddr("caller"); function setUp() public { - owner = makeAddr("owner"); + owner = makeAddr("owner"); + authedContract = makeAddr("authedContract"); authority = new AuthorityMock(); configurator = address(new ConfiguratorMock()); pool = address(new PoolMock()); freezer = new SparkLendFreezerMomHarness(configurator, pool); + freezer.rely(authedContract); freezer.setAuthority(address(authority)); freezer.setOwner(owner); } @@ -178,6 +314,10 @@ contract SparkLendFreezerMomIsAuthorizedTest is Test { assertEq(freezer.isAuthorizedExternal(owner, bytes4("0")), true); } + function test_isAuthorized_srcIsWard() external { + assertEq(freezer.isAuthorizedExternal(authedContract, bytes4("0")), true); + } + function test_isAuthorized_authorityIsZero() external { vm.prank(owner); freezer.setAuthority(address(0)); @@ -202,9 +342,12 @@ contract SparkLendFreezerMomIsAuthorizedTest is Test { contract EventTests is SparkLendFreezerMomUnitTestBase { - event FreezeMarket(address indexed reserve); + event FreezeMarket(address indexed reserve, bool freeze); + event PauseMarket(address indexed reserve, bool pause); event SetOwner(address indexed oldOwner, address indexed newOwner); event SetAuthority(address indexed oldAuthority, address indexed newAuthority); + event Rely(address indexed usr); + event Deny(address indexed usr); function test_setAuthority_eventData() external { address newAuthority = makeAddr("newAuthority"); @@ -224,6 +367,24 @@ contract EventTests is SparkLendFreezerMomUnitTestBase { freezer.setOwner(newOwner); } + function test_rely_eventData() external { + address authedContract = makeAddr("authedContract"); + + vm.prank(owner); + vm.expectEmit(address(freezer)); + emit Rely(authedContract); + freezer.rely(authedContract); + } + + function test_deny_eventData() external { + address authedContract = makeAddr("authedContract"); + + vm.prank(owner); + vm.expectEmit(address(freezer)); + emit Deny(authedContract); + freezer.deny(authedContract); + } + function test_freezeMarket_eventData() public { address caller = makeAddr("caller"); address asset = makeAddr("asset"); @@ -236,8 +397,12 @@ contract EventTests is SparkLendFreezerMomUnitTestBase { ); vm.prank(caller); - emit FreezeMarket(asset); - freezer.freezeMarket(asset); + emit FreezeMarket(asset, true); + freezer.freezeMarket(asset, true); + + vm.prank(caller); + emit FreezeMarket(asset, false); + freezer.freezeMarket(asset, false); } function test_freezeAllMarkets_eventData() public { @@ -258,10 +423,68 @@ contract EventTests is SparkLendFreezerMomUnitTestBase { vm.prank(caller); vm.expectEmit(address(freezer)); - emit FreezeMarket(asset1); + emit FreezeMarket(asset1, true); + vm.expectEmit(address(freezer)); + emit FreezeMarket(asset2, true); + freezer.freezeAllMarkets(true); + + vm.prank(caller); + vm.expectEmit(address(freezer)); + emit FreezeMarket(asset1, false); + vm.expectEmit(address(freezer)); + emit FreezeMarket(asset2, false); + freezer.freezeAllMarkets(false); + } + + function test_pauseMarket_eventData() public { + address caller = makeAddr("caller"); + address asset = makeAddr("asset"); + + authority.__setCanCall( + caller, + address(freezer), + freezer.pauseMarket.selector, + true + ); + + vm.prank(caller); + emit PauseMarket(asset, true); + freezer.pauseMarket(asset, true); + + vm.prank(caller); + emit PauseMarket(asset, false); + freezer.pauseMarket(asset, false); + } + + function test_pauseAllMarkets_eventData() public { + address caller = makeAddr("caller"); + + authority.__setCanCall( + caller, + address(freezer), + freezer.pauseAllMarkets.selector, + true + ); + + address asset1 = makeAddr("asset1"); + address asset2 = makeAddr("asset2"); + + PoolMock(pool).__addAsset(asset1); + PoolMock(pool).__addAsset(asset2); + + vm.prank(caller); + vm.expectEmit(address(freezer)); + emit PauseMarket(asset1, true); + vm.expectEmit(address(freezer)); + emit PauseMarket(asset2, true); + freezer.pauseAllMarkets(true); + + vm.prank(caller); + vm.expectEmit(address(freezer)); + emit PauseMarket(asset1, false); vm.expectEmit(address(freezer)); - emit FreezeMarket(asset2); - freezer.freezeAllMarkets(); + emit PauseMarket(asset2, false); + freezer.pauseAllMarkets(false); } }