Bad debt (shortfall) liquidation leaves liquidated user in a negative collateral balance which can cause bank run and loss of funds for the last users to withdraw
Consider the following scenario:\\nUser1 and User2 are the only makers in the market each with maker=50 position and each with collateral=500. (price=$100)\\nA new user comes into the market and opens long=10 position with collateral=10.\\nPrice drops to $90. Some liquidator liquidates the user, taking $10 liquidation fee. User is now left with the negative collateral = -$100\\nSince User1 and User2 were the other party for the user, each of them has a profit of $50 (both users have collateral=550)\\nAt this point protocol has total funds from deposit of User1($500) + User2($500) + new user($10) - liquidator($10) = $1000. However, User1 and User2 have total collateral of 1100.\\nUser1 closes position and withdraws $550. This succeeds. Protocol now has only $450 funds remaining and 550 collateral owed to User2.\\nUser2 closes position and tries to withdraw $550, but fails, because protocol doesn't have enough funds. User2 can only withdraw $450, effectively losing $100.\\nSince all users know about this feature, after bad debt they will race to be the first to withdraw, triggering a bank run.\\nThe scenario above is demonstrated in the test, add this to test/unit/market/Market.test.ts:\\n```\\nit('panprog bad debt liquidation bankrun', async () => {\\n\\n    function setupOracle(price: string, timestamp : number, nextTimestamp : number) {\\n        const oracleVersion = {\\n        price: parse6decimal(price),\\n        timestamp: timestamp,\\n        valid: true,\\n        }\\n        oracle.at.whenCalledWith(oracleVersion.timestamp).returns(oracleVersion)\\n        oracle.status.returns([oracleVersion, nextTimestamp])\\n        oracle.request.returns()\\n    }\\n\\n    var riskParameter = {\\n        maintenance: parse6decimal('0.01'),\\n        takerFee: parse6decimal('0.00'),\\n        takerSkewFee: 0,\\n        takerImpactFee: 0,\\n        makerFee: parse6decimal('0.00'),\\n        makerImpactFee: 0,\\n        makerLimit: parse6decimal('1000'),\\n        efficiencyLimit: parse6decimal('0.2'),\\n        liquidationFee: parse6decimal('0.50'),\\n        minLiquidationFee: parse6decimal('10'),\\n        maxLiquidationFee: parse6decimal('1000'),\\n        utilizationCurve: {\\n        minRate: parse6decimal('0.0'),\\n        maxRate: parse6decimal('1.00'),\\n        targetRate: parse6decimal('0.10'),\\n        targetUtilization: parse6decimal('0.50'),\\n        },\\n        pController: {\\n        k: parse6decimal('40000'),\\n        max: parse6decimal('1.20'),\\n        },\\n        minMaintenance: parse6decimal('10'),\\n        virtualTaker: parse6decimal('0'),\\n        staleAfter: 14400,\\n        makerReceiveOnly: false,\\n    }\\n    var marketParameter = {\\n        fundingFee: parse6decimal('0.0'),\\n        interestFee: parse6decimal('0.0'),\\n        oracleFee: parse6decimal('0.0'),\\n        riskFee: parse6decimal('0.0'),\\n        positionFee: parse6decimal('0.0'),\\n        maxPendingGlobal: 5,\\n        maxPendingLocal: 3,\\n        settlementFee: parse6decimal('0'),\\n        makerRewardRate: parse6decimal('0'),\\n        longRewardRate: parse6decimal('0'),\\n        shortRewardRate: parse6decimal('0'),\\n        makerCloseAlways: false,\\n        takerCloseAlways: false,\\n        closed: false,\\n    }\\n        \\n    await market.connect(owner).updateRiskParameter(riskParameter);\\n    await market.connect(owner).updateParameter(marketParameter);\\n\\n    setupOracle('100', TIMESTAMP, TIMESTAMP + 100);\\n\\n    var collateral = parse6decimal('500')\\n    dsu.transferFrom.whenCalledWith(userB.address, market.address, collateral.mul(1e12)).returns(true)\\n    await market.connect(userB).update(userB.address, parse6decimal('50.000'), 0, 0, collateral, false)\\n    dsu.transferFrom.whenCalledWith(userC.address, market.address, collateral.mul(1e12)).returns(true)\\n    await market.connect(userC).update(userC.address, parse6decimal('50.000'), 0, 0, collateral, false)\\n\\n    var collateral = parse6decimal('10')\\n    dsu.transferFrom.whenCalledWith(user.address, market.address, collateral.mul(1e12)).returns(true)\\n    await market.connect(user).update(user.address, 0, parse6decimal('10.000'), 0, collateral, false)\\n\\n    var info = await market.locals(user.address);\\n    var infoB = await market.locals(userB.address);\\n    var infoC = await market.locals(userC.address);\\n    console.log("collateral before liquidation: " + info.collateral + " + " + infoB.collateral + " + " + infoC.collateral + " = " + \\n        info.collateral.add(infoB.collateral).add(infoC.collateral));\\n\\n    setupOracle('100', TIMESTAMP + 100, TIMESTAMP + 200);\\n    setupOracle('90', TIMESTAMP + 200, TIMESTAMP + 300);\\n    // liquidate\\n    const EXPECTED_LIQUIDATION_FEE = parse6decimal('10')\\n    dsu.transfer.whenCalledWith(liquidator.address, EXPECTED_LIQUIDATION_FEE.mul(1e12)).returns(true)\\n    dsu.balanceOf.whenCalledWith(market.address).returns(COLLATERAL.mul(1e12))\\n    await market.connect(liquidator).update(user.address, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true)\\n\\n    setupOracle('90', TIMESTAMP + 200, TIMESTAMP + 300);\\n    await market.connect(userB).update(userB.address, 0, 0, 0, 0, false)\\n    await market.connect(userC).update(userC.address, 0, 0, 0, 0, false)\\n\\n    var info = await market.locals(user.address);\\n    var infoB = await market.locals(userB.address);\\n    var infoC = await market.locals(userC.address);\\n    console.log("collateral after liquidation: " + info.collateral + " + " + infoB.collateral + " + " + infoC.collateral + " = " + \\n        info.collateral.add(infoB.collateral).add(infoC.collateral));\\n})\\n```\\n\\nConsole output for the code:\\n```\\ncollateral before liquidation: 10000000 + 500000000 + 500000000 = 1010000000\\ncollateral after liquidation: -100000080 + 550000000 + 550000000 = 999999920\\n```\\n\\nAfter initial total deposit of $1010, in the end liquidated user will just abandon his account, and remaining user accounts have $550+$550=$1100 but only $1000 funds in the protocol to withdraw.
There should be no negative collateral accounts with 0-position and no incentive to cover shortfall. When liquidated, if account is left with negative collateral, the bad debt should be added to the opposite position pnl (long position bad debt should be socialized between short position holders) or maybe to makers pnl only (socialized between makers). The account will have to be left with collateral = 0.\\nImplementation details for such solution can be tricky due to settlement in the future (pnl is not known at the time of liquidation initiation). Possibly a 2nd step of bad debt liquidation should be added: a keeper will call the user account to socialize bad debt and get some reward for this. Although this is not the best solution, because users who close their positions before the keeper socializes the bad debt, will be able to avoid this social loss. One of the solutions for this will be to introduce delayed withdrawals and delayed socialization (like withdrawals are allowed only after 5 oracle versions and socialization is applied to all positions opened before socialization and still active or closed within 5 last oracle versions), but it will make protocol much more complicated.
After ANY bad debt, the protocol collateral for all non-negative users will be higher than protocol funds available, which can cause a bank run and a loss of funds for the users who are the last to withdraw.\\nEven if someone covers the shortfall for the user with negative collateral, this doesn't guarantee absence of bank run:\\nIf the shortfall is not covered quickly for any reason, the other users can notice disparency between collateral and funds in the protocol and start to withdraw\\nIt is possible that bad debt is so high that any entity ("insurance fund") just won't have enough funds to cover it.
```\\nit('panprog bad debt liquidation bankrun', async () => {\\n\\n    function setupOracle(price: string, timestamp : number, nextTimestamp : number) {\\n        const oracleVersion = {\\n        price: parse6decimal(price),\\n        timestamp: timestamp,\\n        valid: true,\\n        }\\n        oracle.at.whenCalledWith(oracleVersion.timestamp).returns(oracleVersion)\\n        oracle.status.returns([oracleVersion, nextTimestamp])\\n        oracle.request.returns()\\n    }\\n\\n    var riskParameter = {\\n        maintenance: parse6decimal('0.01'),\\n        takerFee: parse6decimal('0.00'),\\n        takerSkewFee: 0,\\n        takerImpactFee: 0,\\n        makerFee: parse6decimal('0.00'),\\n        makerImpactFee: 0,\\n        makerLimit: parse6decimal('1000'),\\n        efficiencyLimit: parse6decimal('0.2'),\\n        liquidationFee: parse6decimal('0.50'),\\n        minLiquidationFee: parse6decimal('10'),\\n        maxLiquidationFee: parse6decimal('1000'),\\n        utilizationCurve: {\\n        minRate: parse6decimal('0.0'),\\n        maxRate: parse6decimal('1.00'),\\n        targetRate: parse6decimal('0.10'),\\n        targetUtilization: parse6decimal('0.50'),\\n        },\\n        pController: {\\n        k: parse6decimal('40000'),\\n        max: parse6decimal('1.20'),\\n        },\\n        minMaintenance: parse6decimal('10'),\\n        virtualTaker: parse6decimal('0'),\\n        staleAfter: 14400,\\n        makerReceiveOnly: false,\\n    }\\n    var marketParameter = {\\n        fundingFee: parse6decimal('0.0'),\\n        interestFee: parse6decimal('0.0'),\\n        oracleFee: parse6decimal('0.0'),\\n        riskFee: parse6decimal('0.0'),\\n        positionFee: parse6decimal('0.0'),\\n        maxPendingGlobal: 5,\\n        maxPendingLocal: 3,\\n        settlementFee: parse6decimal('0'),\\n        makerRewardRate: parse6decimal('0'),\\n        longRewardRate: parse6decimal('0'),\\n        shortRewardRate: parse6decimal('0'),\\n        makerCloseAlways: false,\\n        takerCloseAlways: false,\\n        closed: false,\\n    }\\n        \\n    await market.connect(owner).updateRiskParameter(riskParameter);\\n    await market.connect(owner).updateParameter(marketParameter);\\n\\n    setupOracle('100', TIMESTAMP, TIMESTAMP + 100);\\n\\n    var collateral = parse6decimal('500')\\n    dsu.transferFrom.whenCalledWith(userB.address, market.address, collateral.mul(1e12)).returns(true)\\n    await market.connect(userB).update(userB.address, parse6decimal('50.000'), 0, 0, collateral, false)\\n    dsu.transferFrom.whenCalledWith(userC.address, market.address, collateral.mul(1e12)).returns(true)\\n    await market.connect(userC).update(userC.address, parse6decimal('50.000'), 0, 0, collateral, false)\\n\\n    var collateral = parse6decimal('10')\\n    dsu.transferFrom.whenCalledWith(user.address, market.address, collateral.mul(1e12)).returns(true)\\n    await market.connect(user).update(user.address, 0, parse6decimal('10.000'), 0, collateral, false)\\n\\n    var info = await market.locals(user.address);\\n    var infoB = await market.locals(userB.address);\\n    var infoC = await market.locals(userC.address);\\n    console.log("collateral before liquidation: " + info.collateral + " + " + infoB.collateral + " + " + infoC.collateral + " = " + \\n        info.collateral.add(infoB.collateral).add(infoC.collateral));\\n\\n    setupOracle('100', TIMESTAMP + 100, TIMESTAMP + 200);\\n    setupOracle('90', TIMESTAMP + 200, TIMESTAMP + 300);\\n    // liquidate\\n    const EXPECTED_LIQUIDATION_FEE = parse6decimal('10')\\n    dsu.transfer.whenCalledWith(liquidator.address, EXPECTED_LIQUIDATION_FEE.mul(1e12)).returns(true)\\n    dsu.balanceOf.whenCalledWith(market.address).returns(COLLATERAL.mul(1e12))\\n    await market.connect(liquidator).update(user.address, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true)\\n\\n    setupOracle('90', TIMESTAMP + 200, TIMESTAMP + 300);\\n    await market.connect(userB).update(userB.address, 0, 0, 0, 0, false)\\n    await market.connect(userC).update(userC.address, 0, 0, 0, 0, false)\\n\\n    var info = await market.locals(user.address);\\n    var infoB = await market.locals(userB.address);\\n    var infoC = await market.locals(userC.address);\\n    console.log("collateral after liquidation: " + info.collateral + " + " + infoB.collateral + " + " + infoC.collateral + " = " + \\n        info.collateral.add(infoB.collateral).add(infoC.collateral));\\n})\\n```\\n