diff --git a/.gitmodules b/.gitmodules index 888d42d..27164b4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/erc20-helpers"] + path = lib/erc20-helpers + url = https://github.com/marsfoundation/erc20-helpers +[submodule "lib/aave-v3-core"] + path = lib/aave-v3-core + url = https://github.com/marsfoundation/aave-v3-core diff --git a/lib/aave-v3-core b/lib/aave-v3-core new file mode 160000 index 0000000..6070e82 --- /dev/null +++ b/lib/aave-v3-core @@ -0,0 +1 @@ +Subproject commit 6070e82d962d9b12835c88e68210d0e63f08d035 diff --git a/lib/erc20-helpers b/lib/erc20-helpers new file mode 160000 index 0000000..9cf80a0 --- /dev/null +++ b/lib/erc20-helpers @@ -0,0 +1 @@ +Subproject commit 9cf80a0d21692a4442c38bf2cb76c866ee3b7d85 diff --git a/lib/forge-std b/lib/forge-std index f73c73d..37a37ab 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit f73c73d2018eb6a111f35e4dae7b4f27401e9421 +Subproject commit 37a37ab73364d6644bfe11edf88a07880f99bd56 diff --git a/src/SparkLendFreezerMom.sol b/src/SparkLendFreezerMom.sol index 042f3ba..6b6afd5 100644 --- a/src/SparkLendFreezerMom.sol +++ b/src/SparkLendFreezerMom.sol @@ -21,11 +21,11 @@ contract SparkLendFreezerMom is ISparkLendFreezerMom { /*** Declarations and Constructor ***/ /**********************************************************************************************/ - address public immutable poolConfigurator; - address public immutable pool; + address public immutable override poolConfigurator; + address public immutable override pool; - address public authority; - address public owner; + address public override authority; + address public override owner; constructor(address poolConfigurator_, address pool_) { poolConfigurator = poolConfigurator_; diff --git a/src/spells/EmergencySpell_SparkLend_FreezeSingleAsset.sol b/src/spells/EmergencySpell_SparkLend_FreezeSingleAsset.sol new file mode 100644 index 0000000..59d2375 --- /dev/null +++ b/src/spells/EmergencySpell_SparkLend_FreezeSingleAsset.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import { ISparkLendFreezerMom } from "src/interfaces/ISparkLendFreezerMom.sol"; + +contract EmergencySpell_SparkLend_FreezeSingleAsset { + + address public immutable sparkLendFreezerMom; + address public immutable reserve; + + constructor(address sparklendFreezerMom_, address reserve_) { + sparkLendFreezerMom = sparklendFreezerMom_; + reserve = reserve_; + } + + function freeze() external { + ISparkLendFreezerMom(sparkLendFreezerMom).freezeMarket(reserve); + } + +} diff --git a/test/IntegrationTests.t.sol b/test/IntegrationTests.t.sol new file mode 100644 index 0000000..ec037a6 --- /dev/null +++ b/test/IntegrationTests.t.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +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 { SparkLendFreezerMom } from "src/SparkLendFreezerMom.sol"; +import { EmergencySpell_SparkLend_FreezeSingleAsset as FreezeSingleAssetSpell } + from "src/spells/EmergencySpell_SparkLend_FreezeSingleAsset.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"; +import { IPool } from "lib/aave-v3-core/contracts/interfaces/IPool.sol"; + +import { IAuthorityLike } from "test/Interfaces.sol"; + +contract IntegrationTests is Test { + + using SafeERC20 for IERC20; + + address constant ACL_MANAGER = 0xdA135Cd78A086025BcdC87B038a1C462032b510C; + address constant AUTHORITY = 0x0a3f6849f78076aefaDf113F5BED87720274dDC0; + address constant DATA_PROVIDER = 0xFc21d6d146E6086B8359705C8b28512a983db0cb; + address constant MKR = 0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2; + address constant PAUSE_PROXY = 0xBE8E3e3618f7474F8cB1d074A26afFef007E98FB; + address constant POOL = 0xC13e21B648A5Ee794902342038FF3aDAB66BE987; + address constant POOL_CONFIG = 0x542DBa469bdE58FAeE189ffB60C6b49CE60E0738; + address constant SPARK_PROXY = 0x3300f198988e4C9C63F75dF86De36421f06af8c4; + address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + address mkrWhale = makeAddr("mkrWhale"); + address sparkUser = makeAddr("sparkUser"); + address randomUser = makeAddr("randomUser"); + + IAuthorityLike authority = IAuthorityLike(AUTHORITY); + IACLManager aclManager = IACLManager(ACL_MANAGER); + IPool pool = IPool(POOL); + IPoolConfigurator poolConfig = IPoolConfigurator(POOL_CONFIG); + IPoolDataProvider dataProvider = IPoolDataProvider(DATA_PROVIDER); + + SparkLendFreezerMom freezer; + FreezeSingleAssetSpell freezeWethSpell; + + function setUp() public { + vm.createSelectFork(getChain('mainnet').rpcUrl); + + freezer = new SparkLendFreezerMom(POOL_CONFIG, POOL); + freezeWethSpell = new FreezeSingleAssetSpell(address(freezer), WETH); + + freezer.setAuthority(AUTHORITY); + freezer.setOwner(PAUSE_PROXY); + } + + function test_cannotCallWithoutHat() external { + assertTrue(authority.hat() != address(freezeWethSpell)); + assertTrue( + !authority.canCall( + address(freezeWethSpell), + address(freezer), + freezer.freezeMarket.selector + ) + ); + + vm.expectRevert("SparkLendFreezerMom/not-authorized"); + freezeWethSpell.freeze(); + } + + function test_cannotCallWithoutRoleSetup() external { + _vote(address(freezeWethSpell)); + + assertTrue(authority.hat() == address(freezeWethSpell)); + assertTrue( + authority.canCall( + address(freezeWethSpell), + address(freezer), + freezer.freezeMarket.selector + ) + ); + + vm.expectRevert(bytes("4")); // CALLER_NOT_RISK_OR_POOL_ADMIN + freezeWethSpell.freeze(); + } + + function test_freezeWethSpell() external { + _vote(address(freezeWethSpell)); + + vm.prank(SPARK_PROXY); + aclManager.addRiskAdmin(address(freezer)); + + assertEq(_isFrozen(WETH), false); + + deal(WETH, sparkUser, 20 ether); // Deal enough for 2 supplies + + // 1. Check user actions before freeze + // NOTE: For all checks, not checking pool.swapBorrowRateMode() since stable rate + // isn't enabled on any reserve. + + vm.startPrank(sparkUser); + + IERC20(WETH).safeApprove(POOL, type(uint256).max); + + // User can supply, borrow, repay, and withdraw + pool.supply(WETH, 10 ether, sparkUser, 0); + pool.borrow(WETH, 1 ether, 2, 0, sparkUser); + pool.repay(WETH, 0.5 ether, 2, sparkUser); + pool.withdraw(WETH, 1 ether, sparkUser); + + vm.stopPrank(); + + // 2. Freeze market + + vm.prank(randomUser); // Demonstrate no ACL in spell + freezeWethSpell.freeze(); + + assertEq(_isFrozen(WETH), true); + + // 3. Check user actions after freeze + + vm.startPrank(sparkUser); + + // User can't supply + vm.expectRevert(bytes("28")); // RESERVE_FROZEN + pool.supply(WETH, 10 ether, sparkUser, 0); + + // User can't borrow + vm.expectRevert(bytes("28")); // RESERVE_FROZEN + pool.borrow(WETH, 1 ether, 2, 0, sparkUser); + + // User can still repay and withdraw + pool.repay(WETH, 0.5 ether, 2, sparkUser); + pool.withdraw(WETH, 1 ether, sparkUser); + + vm.stopPrank(); + + // 4. Simulate spell after freeze, unfreezing market + vm.prank(SPARK_PROXY); + poolConfig.setReserveFreeze(WETH, false); + + assertEq(_isFrozen(WETH), false); + + // 5. Check user actions after unfreeze + + vm.startPrank(sparkUser); + + // User can supply, borrow, repay, and withdraw + pool.supply(WETH, 10 ether, sparkUser, 0); + pool.borrow(WETH, 1 ether, 2, 0, sparkUser); + pool.repay(WETH, 1 ether, 2, sparkUser); + pool.withdraw(WETH, 1 ether, sparkUser); + } + + 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); + } + + function _isFrozen(address asset) internal view returns (bool isFrozen) { + ( ,,,,,,,,, isFrozen ) = dataProvider.getReserveConfigurationData(asset); + } + +} diff --git a/test/Interfaces.sol b/test/Interfaces.sol new file mode 100644 index 0000000..19097e5 --- /dev/null +++ b/test/Interfaces.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +interface IAuthorityLike { + function canCall(address src, address dst, bytes4 sig) external view returns (bool); + + function hat() external view returns (address); + + function lock(uint256 amount) external; + + function vote(address[] calldata slate) external; + + function lift(address target) external; +} diff --git a/test/SparkLendFreezerMom.t.sol b/test/SparkLendFreezerMom.t.sol index 2b6524b..f1e136c 100644 --- a/test/SparkLendFreezerMom.t.sol +++ b/test/SparkLendFreezerMom.t.sol @@ -5,6 +5,8 @@ import "forge-std/Test.sol"; import { SparkLendFreezerMom } from "../src/SparkLendFreezerMom.sol"; +import { SparkLendFreezerMomHarness } from "./harnesses/SparkLendFreezerMomHarness.sol"; + import { AuthorityMock, ConfiguratorMock, PoolMock } from "./Mocks.sol"; contract SparkLendFreezerMomUnitTestBase is Test { @@ -144,6 +146,60 @@ contract FreezeMarketTests is SparkLendFreezerMomUnitTestBase { } +contract SparkLendFreezerMomIsAuthorizedTest is Test { + + address public configurator; + address public pool; + address public owner; + + AuthorityMock public authority; + + SparkLendFreezerMomHarness public freezer; + + address caller = makeAddr("caller"); + + function setUp() public { + owner = makeAddr("owner"); + + authority = new AuthorityMock(); + configurator = address(new ConfiguratorMock()); + pool = address(new PoolMock()); + freezer = new SparkLendFreezerMomHarness(configurator, pool); + + freezer.setAuthority(address(authority)); + freezer.setOwner(owner); + } + + function test_isAuthorized_internalCall() external { + assertEq(freezer.isAuthorizedExternal(address(freezer), bytes4("0")), true); + } + + function test_isAuthorized_srcIsOwner() external { + assertEq(freezer.isAuthorizedExternal(owner, bytes4("0")), true); + } + + function test_isAuthorized_authorityIsZero() external { + vm.prank(owner); + freezer.setAuthority(address(0)); + assertEq(freezer.isAuthorizedExternal(caller, bytes4("0")), false); + } + + function test_isAuthorized_canCall() external { + vm.prank(owner); + authority.__setCanCall(caller, address(freezer), bytes4("0"), true); + + vm.expectCall( + address(authority), + abi.encodePacked( + AuthorityMock.canCall.selector, + abi.encode(caller, address(freezer), bytes4("0")) + ) + ); + assertEq(freezer.isAuthorizedExternal(caller, bytes4("0")), true); + } + +} + contract EventTests is SparkLendFreezerMomUnitTestBase { event FreezeMarket(address indexed reserve); @@ -207,6 +263,5 @@ contract EventTests is SparkLendFreezerMomUnitTestBase { emit FreezeMarket(asset2); freezer.freezeAllMarkets(); } -} - +} diff --git a/test/harnesses/SparkLendFreezerMomHarness.sol b/test/harnesses/SparkLendFreezerMomHarness.sol new file mode 100644 index 0000000..3b1c6b8 --- /dev/null +++ b/test/harnesses/SparkLendFreezerMomHarness.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.0; + +import { SparkLendFreezerMom } from "src/SparkLendFreezerMom.sol"; + +contract SparkLendFreezerMomHarness is SparkLendFreezerMom { + + constructor(address poolConfigurator_, address pool_) + SparkLendFreezerMom(poolConfigurator_, pool_) {} + + function isAuthorizedExternal(address src, bytes4 sig) public view returns (bool) { + return super.isAuthorized(src, sig); + } + +}