MultiInvoker liquidation action will revert most of the time due to incorrect closable amount initialization
`MultiInvoker` calculates the `closable` amount in its `_latest` function incorrectly. In particular, it doesn't initialize `closableAmount`, so it's set to 0 initially. It then scans pending positions, settling those which should be settled, and reducing `closableAmount` if necessary for remaining pending positions:\\n```\\nfunction _latest(\\n    IMarket market,\\n    address account\\n) internal view returns (Position memory latestPosition, Fixed6 latestPrice, UFixed6 closableAmount) {\\n    // load parameters from the market\\n    IPayoffProvider payoff = market.payoff();\\n\\n    // load latest settled position and price\\n    uint256 latestTimestamp = market.oracle().latest().timestamp;\\n    latestPosition = market.positions(account);\\n    latestPrice = market.global().latestPrice;\\n    UFixed6 previousMagnitude = latestPosition.magnitude();\\n\\n    // @audit-issue Should add:\\n    // closableAmount = previousMagnitude;\\n    // otherwise if no position is settled in the following loop, closableAmount incorrectly remains 0\\n\\n    // scan pending position for any ready-to-be-settled positions\\n    Local memory local = market.locals(account);\\n    for (uint256 id = local.latestId + 1; id <= local.currentId; id++) {\\n\\n        // load pending position\\n        Position memory pendingPosition = market.pendingPositions(account, id);\\n        pendingPosition.adjust(latestPosition);\\n\\n        // load oracle version for that position\\n        OracleVersion memory oracleVersion = market.oracle().at(pendingPosition.timestamp);\\n        if (address(payoff) != address(0)) oracleVersion.price = payoff.payoff(oracleVersion.price);\\n\\n        // virtual settlement\\n        if (pendingPosition.timestamp <= latestTimestamp) {\\n            if (!oracleVersion.valid) latestPosition.invalidate(pendingPosition);\\n            latestPosition.update(pendingPosition);\\n            if (oracleVersion.valid) latestPrice = oracleVersion.price;\\n\\n            previousMagnitude = latestPosition.magnitude();\\n@@@         closableAmount = previousMagnitude;\\n\\n        // process pending positions\\n        } else {\\n            closableAmount = closableAmount\\n                .sub(previousMagnitude.sub(pendingPosition.magnitude().min(previousMagnitude)));\\n            previousMagnitude = latestPosition.magnitude();\\n        }\\n    }\\n}\\n```\\n\\nNotice, that `closableAmount` is initialized to `previousMagnitude` only if there is at least one position that needs to be settled. However, if `local.latestId == local.currentId` (which is the case for most of the liquidations - position becomes liquidatable due to price changes without any pending positions created by the user), this loop is skipped entirely, never setting `closableAmount`, so it's incorrectly returned as 0, although it's not 0 (it should be the latest settled position magnitude).\\nSince `LIQUIDATE` action of `MultiInvoker` uses `_latest` to calculate `closableAmount` and `liquidationFee`, these values will be calculated incorrectly and will revert when trying to update the market. See the `_liquidate` market update reducing `currentPosition` by `closable` (which is 0 when it must be bigger):\\n```\\nmarket.update(\\n    account,\\n    currentPosition.maker.isZero() ? UFixed6Lib.ZERO : currentPosition.maker.sub(closable),\\n    currentPosition.long.isZero() ? UFixed6Lib.ZERO : currentPosition.long.sub(closable),\\n    currentPosition.short.isZero() ? UFixed6Lib.ZERO : currentPosition.short.sub(closable),\\n    Fixed6Lib.from(-1, liquidationFee),\\n    true\\n);\\n```\\n\\nThis line will revert because `Market._invariant` verifies that `closableAmount` must be 0 after updating liquidated position:\\n```\\nif (protected && (\\n@@@ !closableAmount.isZero() ||\\n    context.latestPosition.local.maintained(\\n        context.latestVersion,\\n        context.riskParameter,\\n        collateralAfterFees.sub(collateral)\\n    ) ||\\n    collateral.lt(Fixed6Lib.from(-1, _liquidationFee(context, newOrder)))\\n)) revert MarketInvalidProtectionError();\\n```\\n
Initialize `closableAmount` to previousMagnitude:\\n```\\n    function _latest(\\n        IMarket market,\\n        address account\\n    ) internal view returns (Position memory latestPosition, Fixed6 latestPrice, UFixed6 closableAmount) {\\n        // load parameters from the market\\n        IPayoffProvider payoff = market.payoff();\\n\\n        // load latest settled position and price\\n        uint256 latestTimestamp = market.oracle().latest().timestamp;\\n        latestPosition = market.positions(account);\\n        latestPrice = market.global().latestPrice;\\n        UFixed6 previousMagnitude = latestPosition.magnitude();\\n+       closableAmount = previousMagnitude;\\n```\\n
All `MultiInvoker` liquidation actions will revert if trying to liquidate users without positions which can be settled, which can happen in 2 cases:\\nLiquidated user doesn't have any pending positions at all (local.latestId == local.currentId). This is the most common case (price has changed and user is liquidated without doing any actions) and we can reasonably expect that this will be the case for at least 50% of liquidations (probably more, like 80-90%).\\nLiquidated user does have pending positions, but no pending position is ready to be settled yet. For example, if liquidator commits unrequested oracle version which liquidates user, even if the user already has pending position (but which is not yet ready to be settled).\\nSince this breaks important `MultiInvoker` functionality in most cases and causes loss of funds to liquidator (revert instead of getting liquidation fee), I believe this should be High severity.
```\\nfunction _latest(\\n    IMarket market,\\n    address account\\n) internal view returns (Position memory latestPosition, Fixed6 latestPrice, UFixed6 closableAmount) {\\n    // load parameters from the market\\n    IPayoffProvider payoff = market.payoff();\\n\\n    // load latest settled position and price\\n    uint256 latestTimestamp = market.oracle().latest().timestamp;\\n    latestPosition = market.positions(account);\\n    latestPrice = market.global().latestPrice;\\n    UFixed6 previousMagnitude = latestPosition.magnitude();\\n\\n    // @audit-issue Should add:\\n    // closableAmount = previousMagnitude;\\n    // otherwise if no position is settled in the following loop, closableAmount incorrectly remains 0\\n\\n    // scan pending position for any ready-to-be-settled positions\\n    Local memory local = market.locals(account);\\n    for (uint256 id = local.latestId + 1; id <= local.currentId; id++) {\\n\\n        // load pending position\\n        Position memory pendingPosition = market.pendingPositions(account, id);\\n        pendingPosition.adjust(latestPosition);\\n\\n        // load oracle version for that position\\n        OracleVersion memory oracleVersion = market.oracle().at(pendingPosition.timestamp);\\n        if (address(payoff) != address(0)) oracleVersion.price = payoff.payoff(oracleVersion.price);\\n\\n        // virtual settlement\\n        if (pendingPosition.timestamp <= latestTimestamp) {\\n            if (!oracleVersion.valid) latestPosition.invalidate(pendingPosition);\\n            latestPosition.update(pendingPosition);\\n            if (oracleVersion.valid) latestPrice = oracleVersion.price;\\n\\n            previousMagnitude = latestPosition.magnitude();\\n@@@         closableAmount = previousMagnitude;\\n\\n        // process pending positions\\n        } else {\\n            closableAmount = closableAmount\\n                .sub(previousMagnitude.sub(pendingPosition.magnitude().min(previousMagnitude)));\\n            previousMagnitude = latestPosition.magnitude();\\n        }\\n    }\\n}\\n```\\n