A liquidation should decrease the account's unrealized loss from the collateral. However, the current implementation deducts the required maintenance margin from the collateral.
pragma solidity 0.8.25;
import { Errors } from "@zaros/utils/Errors.sol";
import { LiquidationBranch } from "@zaros/perpetuals/branches/LiquidationBranch.sol";
import { MarketOrder } from "@zaros/perpetuals/leaves/MarketOrder.sol";
import { Position } from "@zaros/perpetuals/leaves/Position.sol";
import { Base_Test } from "test/Base.t.sol";
import { PerpMarket } from "@zaros/perpetuals/leaves/PerpMarket.sol";
import { OrderBranch } from "@zaros/perpetuals/branches/OrderBranch.sol";
import { TradingAccountHarness } from "test/harnesses/perpetuals/leaves/TradingAccountHarness.sol";
import { GlobalConfigurationHarness } from "test/harnesses/perpetuals/leaves/GlobalConfigurationHarness.sol";
import { PerpMarketHarness } from "test/harnesses/perpetuals/leaves/PerpMarketHarness.sol";
import { PositionHarness } from "test/harnesses/perpetuals/leaves/PositionHarness.sol";
import { UD60x18 } from "@prb-math/UD60x18.sol";
import { SD59x18, sd59x18 } from "@prb-math/SD59x18.sol";
contract LiquidateAccounts_POC_Integration_Test is Base_Test {
function setUp() public override {
Base_Test.setUp();
changePrank({ msgSender: users.owner.account });
configureSystemParameters();
createPerpMarkets();
changePrank({ msgSender: users.naruto.account });
}
struct TestFuzz_GivenThereAreLiquidatableAccountsInTheArray_Context {
MarketConfig fuzzMarketConfig;
MarketConfig secondMarketConfig;
uint256 marginValueUsd;
uint256 initialMarginRate;
uint128[] accountsIds;
uint256 accountMarginValueUsd;
uint128 tradingAccountId;
PerpMarket.Data perpMarketData;
int256 expectedLastFundingRate;
int256 expectedLastFundingFeePerUnit;
uint256 expectedLastFundingTime;
uint128 nonLiquidatableTradingAccountId;
MarketOrder.Data marketOrder;
Position.Data expectedPosition;
Position.Data position;
UD60x18 openInterestX18;
uint256 expectedOpenInterest;
SD59x18 skewX18;
int256 expectedSkew;
}
function simpleOpenPosition(
MarketConfig memory fuzzMarketConfig,
uint128 tradingAccountId,
uint256 initialMarginRate,
uint256 marginValueUsd,
bool isLong
)
internal
{
address marketOrderKeeper = marketOrderKeepers[fuzzMarketConfig.marketId];
deal({ token: address(usdz), to: users.naruto.account, give: marginValueUsd });
int128 sizeDelta = 5e18;
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: tradingAccountId,
marketId: fuzzMarketConfig.marketId,
sizeDelta: sizeDelta
})
);
bytes memory mockSignedReport =
getMockedSignedReport(fuzzMarketConfig.streamId, fuzzMarketConfig.mockUsdPrice);
changePrank({ msgSender: marketOrderKeeper });
perpsEngine.fillMarketOrder(tradingAccountId, fuzzMarketConfig.marketId, mockSignedReport);
changePrank({ msgSender: users.naruto.account });
}
function testPOC_liquidateAccounts() external {
TestFuzz_GivenThereAreLiquidatableAccountsInTheArray_Context memory ctx;
uint128 marketId = uint128(INITIAL_MARKET_ID) + 1;
bool isLong = true;
uint256 amountOfTradingAccounts = 2;
uint256 timeDelta = 1 days;
ctx.fuzzMarketConfig = getFuzzMarketConfig(marketId);
ctx.marginValueUsd = 10_000e18 / amountOfTradingAccounts;
ctx.initialMarginRate = ctx.fuzzMarketConfig.imr;
deal({ token: address(usdz), to: users.naruto.account, give: ctx.marginValueUsd });
ctx.accountsIds = new uint128[](amountOfTradingAccounts + 2);
ctx.accountMarginValueUsd = ctx.marginValueUsd / (amountOfTradingAccounts);
for (uint256 i; i < amountOfTradingAccounts; i++) {
ctx.tradingAccountId = createAccountAndDeposit(ctx.accountMarginValueUsd, address(usdz));
simpleOpenPosition(
ctx.fuzzMarketConfig,
ctx.tradingAccountId,
ctx.initialMarginRate,
ctx.accountMarginValueUsd,
isLong
);
ctx.accountsIds[i] = ctx.tradingAccountId;
deal({ token: address(usdz), to: users.naruto.account, give: ctx.marginValueUsd });
}
uint256 newIndexPrice = 502e18;
updateMockPriceFeed(marketId, newIndexPrice);
ctx.nonLiquidatableTradingAccountId = createAccountAndDeposit(ctx.accountMarginValueUsd, address(usdz));
ctx.accountsIds[amountOfTradingAccounts] = ctx.nonLiquidatableTradingAccountId;
changePrank({ msgSender: liquidationKeeper });
for (uint256 i; i < ctx.accountsIds.length; i++) {
if (ctx.accountsIds[i] == ctx.nonLiquidatableTradingAccountId || ctx.accountsIds[i] == 0) {
continue;
}
vm.expectEmit({
checkTopic1: true,
checkTopic2: true,
checkTopic3: false,
checkData: false,
emitter: address(perpsEngine)
});
emit LiquidationBranch.LogLiquidateAccount({
keeper: liquidationKeeper,
tradingAccountId: ctx.accountsIds[i],
amountOfOpenPositions: 0,
requiredMaintenanceMarginUsd: 0,
marginBalanceUsd: 0,
liquidatedCollateralUsd: 0,
liquidationFeeUsd: 0
});
}
uint128 accountId = ctx.accountsIds[0];
uint256 marginBalance = UD60x18.unwrap(TradingAccountHarness(address(perpsEngine)).exposed_getMarginCollateralBalance({
tradingAccountId: accountId,
collateralType: address(usdz)
}));
uint256 orderFee = 4000001000000000000;
uint256 settlementFee = 2000000000000000000;
assertEq(
UD60x18.unwrap(TradingAccountHarness(address(perpsEngine)).exposed_getMarginCollateralBalance({
tradingAccountId: accountId,
collateralType: address(usdz)
})),
2500e18 - orderFee - settlementFee,
"Wrong margin collateral balance before liquidation"
);
skip(timeDelta);
ctx.expectedLastFundingRate = perpsEngine.getFundingRate(ctx.fuzzMarketConfig.marketId).intoInt256();
ctx.expectedLastFundingTime = block.timestamp;
perpsEngine.liquidateAccounts(ctx.accountsIds);
assertEq(
UD60x18.unwrap(TradingAccountHarness(address(perpsEngine)).exposed_getMarginCollateralBalance({
tradingAccountId: accountId,
collateralType: address(usdz)
}))
, 2476.4499895875e18,
"Wrong margin collateral balance after liquidation"
);
}
}
A liquidation does not give a proper amount of profit to LP.
https://github.com/Cyfrin/2024-07-zaros/blob/7439d79e627286ade431d4ea02805715e46ccf42/src/perpetuals/branches/LiquidationBranch.sol#L158