From 56f66e4c2a74e499d6741615a3001c26da3323a3 Mon Sep 17 00:00:00 2001 From: Sunny Vempati <5723490+sunnyvempati@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:32:02 -0500 Subject: [PATCH] Audit / other fixes (#2168) * wip * audit and other fixes * more fixes * fix tests * fix storage * fix and add more tests * add test for margin only liquidation * storage dump fix * storage dump --- markets/perps-market/cannonfile.test.toml | 7 - .../contracts/interfaces/IAccountEvents.sol | 8 +- .../IAsyncOrderSettlementPythModule.sol | 4 +- .../interfaces/IGlobalPerpsMarketModule.sol | 20 -- .../interfaces/ILiquidationModule.sol | 12 + .../modules/AsyncOrderCancelModule.sol | 20 +- .../AsyncOrderSettlementPythModule.sol | 30 +- .../modules/GlobalPerpsMarketModule.sol | 21 -- .../contracts/modules/LiquidationModule.sol | 13 +- .../modules/PerpsMarketFactoryModule.sol | 4 +- .../contracts/storage/GlobalPerpsMarket.sol | 24 +- .../GlobalPerpsMarketConfiguration.sol | 16 +- .../contracts/storage/PerpsAccount.sol | 200 +++---------- .../storage/PerpsCollateralConfiguration.sol | 2 +- .../contracts/storage/PerpsMarketFactory.sol | 10 +- markets/perps-market/storage.dump.sol | 7 +- .../test/integration/Account/Debt.test.ts | 69 +---- .../Margins.multiCollateral.failure.test.ts | 44 ++- .../Account/Margins.multiCollateral.test.ts | 7 +- .../KeeperRewards.Settlement.test.ts | 17 +- .../Liquidation.marginOnly.test.ts | 265 ++++++++++++++++++ .../Market/MarketDebt.withFunding.test.ts | 235 +++++++++++----- .../Markets/GlobalPerpsMarket.test.ts | 37 +-- .../Orders/OffchainAsyncOrder.cancel.test.ts | 38 +-- .../Orders/OffchainAsyncOrder.fees.test.ts | 62 ++-- .../Orders/OffchainAsyncOrder.settle.test.ts | 41 ++- .../test/integration/bootstrap/bootstrap.ts | 7 - 27 files changed, 687 insertions(+), 533 deletions(-) create mode 100644 markets/perps-market/test/integration/Liquidation/Liquidation.marginOnly.test.ts diff --git a/markets/perps-market/cannonfile.test.toml b/markets/perps-market/cannonfile.test.toml index ac10b1ec6f..ef38c2a0fb 100644 --- a/markets/perps-market/cannonfile.test.toml +++ b/markets/perps-market/cannonfile.test.toml @@ -132,13 +132,6 @@ func = "setFeatureFlagAllowAll" from = "<%= settings.owner %>" args = ["<%= formatBytes32String('perpsSystem') %>", true] -# add snxUSD as the only priority to deduct from on a given account, as default -[invoke.setSynthDeductionPriority] -target = ["PerpsMarketProxy"] -func = "setSynthDeductionPriority" -from = "<%= settings.owner %>" -args = [[0]] - [contract.MockPythERC7412Wrapper] artifact = "contracts/mocks/MockPythERC7412Wrapper.sol:MockPythERC7412Wrapper" diff --git a/markets/perps-market/contracts/interfaces/IAccountEvents.sol b/markets/perps-market/contracts/interfaces/IAccountEvents.sol index 9d40c41104..62822b6955 100644 --- a/markets/perps-market/contracts/interfaces/IAccountEvents.sol +++ b/markets/perps-market/contracts/interfaces/IAccountEvents.sol @@ -6,10 +6,10 @@ pragma solidity >=0.8.11 <0.9.0; */ interface IAccountEvents { /** - * @notice Gets fired when some collateral is deducted from the account for paying fees or liquidations. - * @param account Id of the account being deducted. - * @param collateralId Id of the collateral (synth) deducted. + * @notice Gets fired anytime an account is charged with fees, paying settlement rewards. + * @param accountId Id of the account being deducted. * @param amount Amount of synth market deducted from the account. + * @param accountDebt current debt of the account after charged amount. */ - event CollateralDeducted(uint256 account, uint128 collateralId, uint256 amount); + event AccountCharged(uint128 accountId, int256 amount, uint256 accountDebt); } diff --git a/markets/perps-market/contracts/interfaces/IAsyncOrderSettlementPythModule.sol b/markets/perps-market/contracts/interfaces/IAsyncOrderSettlementPythModule.sol index b345c0f03e..1030bf613c 100644 --- a/markets/perps-market/contracts/interfaces/IAsyncOrderSettlementPythModule.sol +++ b/markets/perps-market/contracts/interfaces/IAsyncOrderSettlementPythModule.sol @@ -52,8 +52,6 @@ interface IAsyncOrderSettlementPythModule { int256 pnl; uint256 chargedInterest; int256 accruedFunding; - uint256 pnlUint; - uint256 amountToDeduct; uint256 settlementReward; uint256 fillPrice; uint256 totalFees; @@ -64,6 +62,8 @@ interface IAsyncOrderSettlementPythModule { uint256 synthDeductionIterator; uint128[] deductedSynthIds; uint256[] deductedAmount; + int256 chargedAmount; + uint256 newAccountDebt; } /** diff --git a/markets/perps-market/contracts/interfaces/IGlobalPerpsMarketModule.sol b/markets/perps-market/contracts/interfaces/IGlobalPerpsMarketModule.sol index 01129cc2f7..b9ef04a9a0 100644 --- a/markets/perps-market/contracts/interfaces/IGlobalPerpsMarketModule.sol +++ b/markets/perps-market/contracts/interfaces/IGlobalPerpsMarketModule.sol @@ -12,12 +12,6 @@ interface IGlobalPerpsMarketModule { */ event InterestRateUpdated(uint128 indexed superMarketId, uint128 interestRate); - /** - * @notice Gets fired when the synth deduction priority is updated by owner. - * @param newSynthDeductionPriority new synth id priority order for deductions. - */ - event SynthDeductionPrioritySet(uint128[] newSynthDeductionPriority); - /** * @notice Gets fired when keeper reward guard is set or updated. * @param minKeeperRewardUsd Minimum keeper reward expressed as USD value. @@ -97,20 +91,6 @@ interface IGlobalPerpsMarketModule { view returns (uint256[] memory supportedCollaterals); - /** - * @notice Sets the synth deduction priority ordered list. - * @dev The synth deduction priority is used to determine the order in which synths are deducted from an account. Id 0 is snxUSD and should be first in the list. - * @param newSynthDeductionPriority Ordered array of synth market ids for deduction priority. - */ - function setSynthDeductionPriority(uint128[] memory newSynthDeductionPriority) external; - - /** - * @notice Gets the synth deduction priority ordered list. - * @dev The synth deduction priority is used to determine the order in which synths are deducted from an account. Id 0 is snxUSD and should be first in the list. - * @return synthDeductionPriority Ordered array of synth market ids for deduction priority. - */ - function getSynthDeductionPriority() external view returns (uint128[] memory); - /** * @notice Sets the keeper reward guard (min and max). * @param minKeeperRewardUsd Minimum keeper reward expressed as USD value. diff --git a/markets/perps-market/contracts/interfaces/ILiquidationModule.sol b/markets/perps-market/contracts/interfaces/ILiquidationModule.sol index 4a77b263fd..8768300ab8 100644 --- a/markets/perps-market/contracts/interfaces/ILiquidationModule.sol +++ b/markets/perps-market/contracts/interfaces/ILiquidationModule.sol @@ -63,6 +63,18 @@ interface ILiquidationModule { bool fullLiquidation ); + /** + * @notice Gets fired when an account margin is liquidated due to not paying down debt. + * @param accountId Id of the account liquidated. + * @param seizedMarginValue margin seized due to liquidation. + * @param liquidationReward reward for liquidating margin account + */ + event AccountMarginLiquidation( + uint128 indexed accountId, + uint256 seizedMarginValue, + uint256 liquidationReward + ); + /** * @notice Liquidates an account. * @dev according to the current situation and account size it can be a partial or full liquidation. diff --git a/markets/perps-market/contracts/modules/AsyncOrderCancelModule.sol b/markets/perps-market/contracts/modules/AsyncOrderCancelModule.sol index 4c96a5399c..569b6d2c92 100644 --- a/markets/perps-market/contracts/modules/AsyncOrderCancelModule.sol +++ b/markets/perps-market/contracts/modules/AsyncOrderCancelModule.sol @@ -69,19 +69,13 @@ contract AsyncOrderCancelModule is IAsyncOrderCancelModule, IMarketEvents, IAcco runtime.fillPrice = asyncOrder.validateCancellation(settlementStrategy, price); if (runtime.settlementReward > 0) { - // deduct keeper reward - (uint128[] memory deductedSynthIds, uint256[] memory deductedAmount) = PerpsAccount - .load(runtime.accountId) - .deductFromAccount(runtime.settlementReward); - for (uint256 i = 0; i < deductedSynthIds.length; i++) { - if (deductedAmount[i] > 0) { - emit CollateralDeducted( - runtime.accountId, - deductedSynthIds[i], - deductedAmount[i] - ); - } - } + // charge account the settlement reward + uint256 accountDebt = PerpsAccount.load(runtime.accountId).charge( + -runtime.settlementReward.toInt() + ); + + emit AccountCharged(runtime.accountId, runtime.settlementReward.toInt(), accountDebt); + // pay keeper PerpsMarketFactory.load().withdrawMarketUsd( ERC2771Context._msgSender(), diff --git a/markets/perps-market/contracts/modules/AsyncOrderSettlementPythModule.sol b/markets/perps-market/contracts/modules/AsyncOrderSettlementPythModule.sol index f09e54b5a4..8fb9d29c58 100644 --- a/markets/perps-market/contracts/modules/AsyncOrderSettlementPythModule.sol +++ b/markets/perps-market/contracts/modules/AsyncOrderSettlementPythModule.sol @@ -79,7 +79,6 @@ contract AsyncOrderSettlementPythModule is .validateRequest(settlementStrategy, price); asyncOrder.validateAcceptablePrice(runtime.fillPrice); - runtime.amountToDeduct = runtime.totalFees; runtime.sizeDelta = asyncOrder.request.sizeDelta; PerpsMarketFactory.Data storage factory = PerpsMarketFactory.load(); @@ -89,7 +88,10 @@ contract AsyncOrderSettlementPythModule is (runtime.pnl, , runtime.chargedInterest, runtime.accruedFunding, , ) = oldPosition.getPnl( runtime.fillPrice ); - perpsAccount.applyPnl(runtime.pnl); + + runtime.chargedAmount = runtime.pnl - runtime.totalFees.toInt(); + perpsAccount.charge(runtime.chargedAmount); + emit AccountCharged(runtime.accountId, runtime.chargedAmount, perpsAccount.debt); // after pnl is realized, update position runtime.updateData = PerpsMarket.loadValid(runtime.marketId).updatePositionData( @@ -109,29 +111,7 @@ contract AsyncOrderSettlementPythModule is runtime.updateData.interestRate ); - // since margin is deposited when trader deposits, as long as the owed collateral is deducted - // from internal accounting, fees are automatically realized by the stakers - if (runtime.amountToDeduct > 0) { - (runtime.deductedSynthIds, runtime.deductedAmount) = perpsAccount.deductFromAccount( - runtime.amountToDeduct - ); - for ( - runtime.synthDeductionIterator = 0; - runtime.synthDeductionIterator < runtime.deductedSynthIds.length; - runtime.synthDeductionIterator++ - ) { - if (runtime.deductedAmount[runtime.synthDeductionIterator] > 0) { - emit CollateralDeducted( - runtime.accountId, - runtime.deductedSynthIds[runtime.synthDeductionIterator], - runtime.deductedAmount[runtime.synthDeductionIterator] - ); - } - } - } - runtime.settlementReward = - settlementStrategy.settlementReward + - KeeperCosts.load().getSettlementKeeperCosts(); + runtime.settlementReward = AsyncOrder.settlementRewardCost(settlementStrategy); if (runtime.settlementReward > 0) { // pay keeper diff --git a/markets/perps-market/contracts/modules/GlobalPerpsMarketModule.sol b/markets/perps-market/contracts/modules/GlobalPerpsMarketModule.sol index 03ae22798d..68d3f30e81 100644 --- a/markets/perps-market/contracts/modules/GlobalPerpsMarketModule.sol +++ b/markets/perps-market/contracts/modules/GlobalPerpsMarketModule.sol @@ -39,27 +39,6 @@ contract GlobalPerpsMarketModule is IGlobalPerpsMarketModule { supportedCollaterals = store.supportedCollateralTypes.values(); } - /** - * @inheritdoc IGlobalPerpsMarketModule - */ - function setSynthDeductionPriority( - uint128[] memory newSynthDeductionPriority - ) external override { - OwnableStorage.onlyOwner(); - GlobalPerpsMarketConfiguration.load().updateSynthDeductionPriority( - newSynthDeductionPriority - ); - - emit SynthDeductionPrioritySet(newSynthDeductionPriority); - } - - /** - * @inheritdoc IGlobalPerpsMarketModule - */ - function getSynthDeductionPriority() external view override returns (uint128[] memory) { - return GlobalPerpsMarketConfiguration.load().synthDeductionPriority; - } - /** * @inheritdoc IGlobalPerpsMarketModule */ diff --git a/markets/perps-market/contracts/modules/LiquidationModule.sol b/markets/perps-market/contracts/modules/LiquidationModule.sol index 3b8b5d5bba..aca2b1def0 100644 --- a/markets/perps-market/contracts/modules/LiquidationModule.sol +++ b/markets/perps-market/contracts/modules/LiquidationModule.sol @@ -97,6 +97,10 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { seizedMarginValue, true ); + // clear debt + account.updateAccountDebt(-(account.debt.toInt())); + + emit AccountMarginLiquidation(accountId, seizedMarginValue, liquidationReward); } else { revert NotEligibleForMarginLiquidation(accountId); } @@ -167,9 +171,12 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents { function canLiquidateMarginOnly( uint128 accountId ) external view override returns (bool isEligible) { - (isEligible, ) = PerpsAccount.load(accountId).isEligibleForMarginLiquidation( - PerpsPrice.Tolerance.DEFAULT - ); + PerpsAccount.Data storage account = PerpsAccount.load(accountId); + if (account.hasOpenPositions()) { + return false; + } else { + (isEligible, ) = account.isEligibleForMarginLiquidation(PerpsPrice.Tolerance.DEFAULT); + } } /** diff --git a/markets/perps-market/contracts/modules/PerpsMarketFactoryModule.sol b/markets/perps-market/contracts/modules/PerpsMarketFactoryModule.sol index 0b24885c1e..6117d9524c 100644 --- a/markets/perps-market/contracts/modules/PerpsMarketFactoryModule.sol +++ b/markets/perps-market/contracts/modules/PerpsMarketFactoryModule.sol @@ -129,7 +129,9 @@ contract PerpsMarketFactoryModule is IPerpsMarketFactoryModule { ); } - int256 totalDebt = collateralValue.toInt() + totalMarketDebt; + int256 totalDebt = collateralValue.toInt() + + totalMarketDebt - + globalMarket.totalAccountsDebt.toInt(); return MathUtil.max(0, totalDebt).toUint(); } diff --git a/markets/perps-market/contracts/storage/GlobalPerpsMarket.sol b/markets/perps-market/contracts/storage/GlobalPerpsMarket.sol index 0d2cc16a20..6c1b0f746d 100644 --- a/markets/perps-market/contracts/storage/GlobalPerpsMarket.sol +++ b/markets/perps-market/contracts/storage/GlobalPerpsMarket.sol @@ -7,7 +7,6 @@ import {SetUtil} from "@synthetixio/core-contracts/contracts/utils/SetUtil.sol"; import {MathUtil} from "../utils/MathUtil.sol"; import {GlobalPerpsMarketConfiguration} from "./GlobalPerpsMarketConfiguration.sol"; import {SafeCastU256, SafeCastI256, SafeCastU128} from "@synthetixio/core-contracts/contracts/utils/SafeCast.sol"; -import {Price} from "@synthetixio/spot-market/contracts/storage/Price.sol"; import {PerpsAccount, SNX_USD_MARKET_ID} from "./PerpsAccount.sol"; import {PerpsMarket} from "./PerpsMarket.sol"; import {PerpsPrice} from "./PerpsPrice.sol"; @@ -23,6 +22,7 @@ library GlobalPerpsMarket { using SafeCastU128 for uint128; using DecimalMath for uint256; using SetUtil for SetUtil.UintSet; + using PerpsCollateralConfiguration for PerpsCollateralConfiguration.Data; bytes32 private constant _SLOT_GLOBAL_PERPS_MARKET = keccak256(abi.encode("io.synthetix.perps-market.GlobalPerpsMarket")); @@ -62,6 +62,10 @@ library GlobalPerpsMarket { mapping(uint128 => uint256) collateralAmounts; SetUtil.UintSet activeCollateralTypes; SetUtil.UintSet activeMarkets; + /** + * @dev Total debt that hasn't been paid across all accounts. + */ + uint256 totalAccountsDebt; } function load() internal pure returns (Data storage marketData) { @@ -113,11 +117,14 @@ library GlobalPerpsMarket { if (collateralId == SNX_USD_MARKET_ID) { total += self.collateralAmounts[collateralId]; } else { - (uint256 collateralValue, ) = spotMarket.quoteSellExactIn( - collateralId, - self.collateralAmounts[collateralId], - Price.Tolerance.DEFAULT - ); + (uint256 collateralValue, ) = PerpsCollateralConfiguration + .load(collateralId) + .valueInUsd( + self.collateralAmounts[collateralId], + spotMarket, + PerpsPrice.Tolerance.DEFAULT, + false + ); total += collateralValue; } } @@ -139,6 +146,11 @@ library GlobalPerpsMarket { } } + function updateDebt(Data storage self, int256 debtDelta) internal { + int256 newTotalAccountsDebt = self.totalAccountsDebt.toInt() + debtDelta; + self.totalAccountsDebt = newTotalAccountsDebt < 0 ? 0 : newTotalAccountsDebt.toUint(); + } + /** * @notice Check if the account is set as liquidatable. */ diff --git a/markets/perps-market/contracts/storage/GlobalPerpsMarketConfiguration.sol b/markets/perps-market/contracts/storage/GlobalPerpsMarketConfiguration.sol index cf1709f8f3..7e14ddb4aa 100644 --- a/markets/perps-market/contracts/storage/GlobalPerpsMarketConfiguration.sol +++ b/markets/perps-market/contracts/storage/GlobalPerpsMarketConfiguration.sol @@ -39,9 +39,10 @@ library GlobalPerpsMarketConfiguration { // solhint-disable-next-line var-name-mixedcase mapping(uint128 => uint256) __unused_1; /** - * @dev when deducting from user's margin which is made up of many synths, this priority governs which synth to sell for deduction + * @dev previously synth deduction priority */ - uint128[] synthDeductionPriority; + // solhint-disable-next-line var-name-mixedcase + uint128[] __unused_2; /** * @dev minimum configured keeper reward for the sender who liquidates the account */ @@ -153,17 +154,6 @@ library GlobalPerpsMarketConfiguration { return MathUtil.min(MathUtil.max(minCap, keeperRewards + costOfExecutionInUsd), maxCap); } - function updateSynthDeductionPriority( - Data storage self, - uint128[] memory newSynthDeductionPriority - ) internal { - delete self.synthDeductionPriority; - - for (uint256 i = 0; i < newSynthDeductionPriority.length; i++) { - self.synthDeductionPriority.push(newSynthDeductionPriority[i]); - } - } - function collectFees( Data storage self, uint256 orderFees, diff --git a/markets/perps-market/contracts/storage/PerpsAccount.sol b/markets/perps-market/contracts/storage/PerpsAccount.sol index 864a58f553..e67eeebac4 100644 --- a/markets/perps-market/contracts/storage/PerpsAccount.sol +++ b/markets/perps-market/contracts/storage/PerpsAccount.sol @@ -53,6 +53,7 @@ library PerpsAccount { // @dev set of open position market ids SetUtil.UintSet openPositionMarketIds; // @dev account's debt accrued from previous positions + // @dev please use updateAccountDebt() to update this value which will update global debt also uint256 debt; } @@ -77,6 +78,8 @@ library PerpsAccount { error MaxCollateralsPerAccountReached(uint128 maxCollateralsPerAccount); + error NonexistentDebt(uint128 accountId); + function load(uint128 id) internal pure returns (Data storage account) { bytes32 s = keccak256(abi.encode("io.synthetix.perps-market.Account", id)); @@ -120,30 +123,41 @@ library PerpsAccount { } /** - * @notice This function applies the pnl of a closing position to the account - * @dev It will either reduce the account's debt or increase the account's debt - * @dev It will also update the account's collateral amount if the debt is fully paid off + * @notice This function charges the account the specified amount + * @dev This is the only function that changes account debt. + * @dev Excess credit is added to account's snxUSD amount. + * @dev if the amount is positive, it is credit, if negative, it is debt. */ - function applyPnl(Data storage self, int256 pnl) internal { - if (pnl > 0) { - int256 leftoverDebt = self.debt.toInt() - pnl; + function charge(Data storage self, int256 amount) internal returns (uint256 debt) { + uint256 newDebt; + if (amount > 0) { + int256 leftoverDebt = self.debt.toInt() - amount; if (leftoverDebt > 0) { - self.debt = leftoverDebt.toUint(); + newDebt = leftoverDebt.toUint(); } else { - self.debt = 0; + newDebt = 0; updateCollateralAmount(self, SNX_USD_MARKET_ID, -leftoverDebt); } } else { int256 creditAvailable = self.collateralAmounts[SNX_USD_MARKET_ID].toInt(); - int256 leftoverCredit = creditAvailable + pnl; + int256 leftoverCredit = creditAvailable + amount; if (leftoverCredit > 0) { - updateCollateralAmount(self, SNX_USD_MARKET_ID, pnl); + updateCollateralAmount(self, SNX_USD_MARKET_ID, amount); } else { updateCollateralAmount(self, SNX_USD_MARKET_ID, -creditAvailable); - self.debt += (-leftoverCredit).toUint(); + newDebt = (self.debt.toInt() - leftoverCredit).toUint(); } } + + return updateAccountDebt(self, newDebt.toInt() - self.debt.toInt()); + } + + function updateAccountDebt(Data storage self, int256 amount) internal returns (uint256 debt) { + self.debt = (self.debt.toInt() + amount).toUint(); + GlobalPerpsMarket.load().updateDebt(amount); + + return self.debt; } function isEligibleForMarginLiquidation( @@ -190,6 +204,8 @@ library PerpsAccount { liquidatableAccounts.add(self.id); seizedMarginValue = transferAllCollateral(self); AsyncOrder.load(self.id).reset(); + + updateAccountDebt(self, -self.debt.toInt()); } } @@ -234,6 +250,10 @@ library PerpsAccount { } function payDebt(Data storage self, uint256 amount) internal { + if (self.debt == 0) { + revert NonexistentDebt(self.id); + } + PerpsMarketFactory.Data storage perpsMarketFactory = PerpsMarketFactory.load(); perpsMarketFactory.synthetix.depositMarketUsd( perpsMarketFactory.perpsMarketId, @@ -276,10 +296,11 @@ library PerpsAccount { if (collateralId == SNX_USD_MARKET_ID) { amountToWithdrawUsd = amountToWithdraw; } else { - (amountToWithdrawUsd, ) = spotMarket.quoteSellExactIn( - collateralId, + (amountToWithdrawUsd, ) = PerpsCollateralConfiguration.load(collateralId).valueInUsd( amountToWithdraw, - Price.Tolerance.STRICT + spotMarket, + PerpsPrice.Tolerance.STRICT, + false ); } @@ -303,6 +324,9 @@ library PerpsAccount { ) internal view returns (int256 withdrawableMargin) { bool hasActivePositions = hasOpenPositions(self); + // not allowed to withdraw until debt is paid off fully. + if (self.debt > 0) return 0; + if (hasActivePositions) { ( uint256 requiredInitialMargin, @@ -314,9 +338,7 @@ library PerpsAccount { getAvailableMargin(self, stalenessTolerance) - requiredMargin.toInt(); } else { - withdrawableMargin = self.debt > 0 - ? getAvailableMargin(self, stalenessTolerance) - : getTotalCollateralValue(self, stalenessTolerance, false).toInt(); + withdrawableMargin = getTotalCollateralValue(self, stalenessTolerance, false).toInt(); } } @@ -493,30 +515,6 @@ library PerpsAccount { possibleLiquidationReward = liquidateAndFlagCost + liquidateWindowsCosts; } - function convertAllCollateralToUsd( - Data storage self - ) internal returns (uint256 totalConvertedCollateral) { - PerpsMarketFactory.Data storage factory = PerpsMarketFactory.load(); - uint256[] memory activeCollateralTypes = self.activeCollateralTypes.values(); - - // 1. withdraw all collateral from synthetix - // 2. sell all collateral for snxUSD - // 3. deposit snxUSD into synthetix - for (uint256 i = 0; i < activeCollateralTypes.length; i++) { - uint128 collateralId = activeCollateralTypes[i].to128(); - if (collateralId == SNX_USD_MARKET_ID) { - totalConvertedCollateral += self.collateralAmounts[collateralId]; - updateCollateralAmount( - self, - collateralId, - -(self.collateralAmounts[collateralId].toInt()) - ); - } else { - totalConvertedCollateral += _deductAllSynth(self, factory, collateralId); - } - } - } - function transferAllCollateral( Data storage self ) internal returns (uint256 seizedCollateralValue) { @@ -542,103 +540,6 @@ library PerpsAccount { } } - /** - * @notice This function deducts snxUSD from an account - * @dev It uses the synth deduction priority to determine which synth to deduct from first - * @dev if the synth is not snxUSD it will sell the synth for snxUSD - * @dev Returns two arrays with the synth ids and amounts deducted - */ - function deductFromAccount( - Data storage self, - uint256 amount // snxUSD - ) internal returns (uint128[] memory deductedSynthIds, uint256[] memory deductedAmount) { - uint256 leftoverAmount = amount; - uint128[] storage synthDeductionPriority = GlobalPerpsMarketConfiguration - .load() - .synthDeductionPriority; - PerpsMarketFactory.Data storage factory = PerpsMarketFactory.load(); - ISpotMarketSystem spotMarket = factory.spotMarket; - - deductedSynthIds = new uint128[](synthDeductionPriority.length); - deductedAmount = new uint256[](synthDeductionPriority.length); - - for (uint256 i = 0; i < synthDeductionPriority.length; i++) { - uint128 collateralId = synthDeductionPriority[i]; - uint256 availableAmount = self.collateralAmounts[collateralId]; - if (availableAmount == 0) { - continue; - } - deductedSynthIds[i] = collateralId; - - if (collateralId == SNX_USD_MARKET_ID) { - // snxUSD - if (availableAmount >= leftoverAmount) { - deductedAmount[i] = leftoverAmount; - updateCollateralAmount(self, collateralId, -(leftoverAmount.toInt())); - leftoverAmount = 0; - break; - } else { - deductedAmount[i] = availableAmount; - updateCollateralAmount(self, collateralId, -(availableAmount.toInt())); - leftoverAmount -= availableAmount; - } - } else { - (uint256 synthAmountRequired, ) = spotMarket.quoteSellExactOut( - collateralId, - leftoverAmount, - Price.Tolerance.STRICT - ); - - address synthToken = factory.spotMarket.getSynth(collateralId); - - if (availableAmount >= synthAmountRequired) { - factory.synthetix.withdrawMarketCollateral( - factory.perpsMarketId, - synthToken, - synthAmountRequired - ); - - (uint256 amountToDeduct, ) = spotMarket.sellExactOut( - collateralId, - leftoverAmount, - type(uint256).max, - address(0) - ); - - factory.depositMarketUsd(leftoverAmount); - - deductedAmount[i] = amountToDeduct; - updateCollateralAmount(self, collateralId, -(amountToDeduct.toInt())); - leftoverAmount = 0; - break; - } else { - factory.synthetix.withdrawMarketCollateral( - factory.perpsMarketId, - synthToken, - availableAmount - ); - - (uint256 amountToDeductUsd, ) = spotMarket.sellExactIn( - collateralId, - availableAmount, - 0, - address(0) - ); - - factory.depositMarketUsd(amountToDeductUsd); - - deductedAmount[i] = availableAmount; - updateCollateralAmount(self, collateralId, -(availableAmount.toInt())); - leftoverAmount -= amountToDeductUsd; - } - } - } - - if (leftoverAmount > 0) { - revert InsufficientAccountMargin(leftoverAmount); - } - } - function liquidatePosition( Data storage self, uint128 marketId, @@ -700,29 +601,6 @@ library PerpsAccount { ); } - function _deductAllSynth( - Data storage self, - PerpsMarketFactory.Data storage factory, - uint128 collateralId - ) private returns (uint256 amountUsd) { - uint256 amount = self.collateralAmounts[collateralId]; - address synth = factory.spotMarket.getSynth(collateralId); - - // 1. withdraw collateral from market manager - factory.synthetix.withdrawMarketCollateral(factory.perpsMarketId, synth, amount); - - // 2. sell collateral for snxUSD - (amountUsd, ) = PerpsMarketFactory.load().spotMarket.sellExactIn( - collateralId, - amount, - 0, - address(0) - ); - - // 3. deposit snxUSD into market manager - factory.depositMarketUsd(amountUsd); - } - function hasOpenPositions(Data storage self) internal view returns (bool) { return self.openPositionMarketIds.length() > 0; } diff --git a/markets/perps-market/contracts/storage/PerpsCollateralConfiguration.sol b/markets/perps-market/contracts/storage/PerpsCollateralConfiguration.sol index 02cbda7edf..8dcb7c77c0 100644 --- a/markets/perps-market/contracts/storage/PerpsCollateralConfiguration.sol +++ b/markets/perps-market/contracts/storage/PerpsCollateralConfiguration.sol @@ -22,7 +22,7 @@ library PerpsCollateralConfiguration { struct Data { /** - * @dev Collateral Id (same as synth id) + * @dev Collateral Id */ uint128 id; /** diff --git a/markets/perps-market/contracts/storage/PerpsMarketFactory.sol b/markets/perps-market/contracts/storage/PerpsMarketFactory.sol index 5ffde2484c..31af6a5bb3 100644 --- a/markets/perps-market/contracts/storage/PerpsMarketFactory.sol +++ b/markets/perps-market/contracts/storage/PerpsMarketFactory.sol @@ -7,11 +7,11 @@ import {ISynthetixSystem} from "../interfaces/external/ISynthetixSystem.sol"; import {ISpotMarketSystem} from "../interfaces/external/ISpotMarketSystem.sol"; import {GlobalPerpsMarket} from "../storage/GlobalPerpsMarket.sol"; import {PerpsMarket} from "../storage/PerpsMarket.sol"; +import {PerpsPrice} from "../storage/PerpsPrice.sol"; import {PerpsCollateralConfiguration} from "../storage/PerpsCollateralConfiguration.sol"; import {LiquidationAssetManager} from "../storage/LiquidationAssetManager.sol"; import {SafeCastI256, SafeCastU256} from "@synthetixio/core-contracts/contracts/utils/SafeCast.sol"; import {SetUtil} from "@synthetixio/core-contracts/contracts/utils/SetUtil.sol"; -import {Price} from "@synthetixio/spot-market/contracts/storage/Price.sol"; /** * @title Main factory library that registers perps markets. Also houses global configuration for all perps markets. @@ -23,6 +23,7 @@ library PerpsMarketFactory { using GlobalPerpsMarket for GlobalPerpsMarket.Data; using PerpsMarket for PerpsMarket.Data; using LiquidationAssetManager for LiquidationAssetManager.Data; + using PerpsCollateralConfiguration for PerpsCollateralConfiguration.Data; bytes32 private constant _SLOT_PERPS_MARKET_FACTORY = keccak256(abi.encode("io.synthetix.perps-market.PerpsMarketFactory")); @@ -116,10 +117,11 @@ library PerpsMarketFactory { address synth = self.spotMarket.getSynth(collateralId); self.synthetix.withdrawMarketCollateral(self.perpsMarketId, synth, amount); - (synthValue, ) = self.spotMarket.quoteSellExactIn( - collateralId, + (synthValue, ) = PerpsCollateralConfiguration.load(collateralId).valueInUsd( amount, - Price.Tolerance.DEFAULT + self.spotMarket, + PerpsPrice.Tolerance.DEFAULT, + false ); PerpsCollateralConfiguration.loadValidLam(collateralId).distributeCollateral(synth, amount); diff --git a/markets/perps-market/storage.dump.sol b/markets/perps-market/storage.dump.sol index b920a0d74f..aacf1d87f1 100644 --- a/markets/perps-market/storage.dump.sol +++ b/markets/perps-market/storage.dump.sol @@ -573,8 +573,6 @@ interface IAsyncOrderSettlementPythModule { int256 pnl; uint256 chargedInterest; int256 accruedFunding; - uint256 pnlUint; - uint256 amountToDeduct; uint256 settlementReward; uint256 fillPrice; uint256 totalFees; @@ -585,6 +583,8 @@ interface IAsyncOrderSettlementPythModule { uint256 synthDeductionIterator; uint128[] deductedSynthIds; uint256[] deductedAmount; + int256 chargedAmount; + uint256 newAccountDebt; } } @@ -679,6 +679,7 @@ library GlobalPerpsMarket { mapping(uint128 => uint256) collateralAmounts; SetUtil.UintSet activeCollateralTypes; SetUtil.UintSet activeMarkets; + uint256 totalAccountsDebt; } function load() internal pure returns (Data storage marketData) { bytes32 s = _SLOT_GLOBAL_PERPS_MARKET; @@ -695,7 +696,7 @@ library GlobalPerpsMarketConfiguration { address feeCollector; mapping(address => uint256) referrerShare; mapping(uint128 => uint256) __unused_1; - uint128[] synthDeductionPriority; + uint128[] __unused_2; uint256 minKeeperRewardUsd; uint256 maxKeeperRewardUsd; uint128 maxPositionsPerAccount; diff --git a/markets/perps-market/test/integration/Account/Debt.test.ts b/markets/perps-market/test/integration/Account/Debt.test.ts index 96010c0373..369ba0ceab 100644 --- a/markets/perps-market/test/integration/Account/Debt.test.ts +++ b/markets/perps-market/test/integration/Account/Debt.test.ts @@ -77,16 +77,6 @@ describe('Account Debt', () => { const openOrderFee = computeFees(wei(0), wei(50), initialFillPrice, orderFees); const closeOrderFee = computeFees(wei(50), wei(-50), finalFillPrice, orderFees); - let synthUsedForOpenOrderFee: Wei, synthUsedForCloseOrderFee: Wei; - before('identify synth amount required to pay open order fee', async () => { - const { synthToBurn } = await systems().SpotMarket.quoteSellExactOut( - synthMarkets()[0].marketId(), - openOrderFee.totalFees, - bn(0) - ); - synthUsedForOpenOrderFee = wei(synthToBurn); - }); - before(`open position size ${size.toString()}`, async () => { await perpsMarkets()[0].aggregator().mockSetCurrentPrice(startingPrice.toBN()); @@ -107,15 +97,6 @@ describe('Account Debt', () => { await perpsMarkets()[0].aggregator().mockSetCurrentPrice(endingPrice.toBN()); }); - before('identify synth amount required to pay open order fee', async () => { - const { synthToBurn } = await systems().SpotMarket.quoteSellExactOut( - synthMarkets()[0].marketId(), - closeOrderFee.totalFees, - bn(0) - ); - synthUsedForCloseOrderFee = wei(synthToBurn); - }); - before('close position', async () => { await openPosition({ systems, @@ -133,59 +114,40 @@ describe('Account Debt', () => { return { openOrderFee, closeOrderFee, - synthUsedForOpenOrderFee: () => synthUsedForOpenOrderFee, - synthUsedForCloseOrderFee: () => synthUsedForCloseOrderFee, + totalFees: openOrderFee.totalFees.add(closeOrderFee.totalFees), pnl: finalFillPrice.sub(initialFillPrice).mul(size), }; }; let currentDebt: Wei; describe('negative pnl', () => { - let startingCollateralAmount: Wei; - before('identify collateral amount', async () => { - startingCollateralAmount = wei( - await systems().PerpsMarket.getCollateralAmount(accountId, synthMarkets()[0].marketId()) - ); - }); - - const { - pnl: expectedPnl, - synthUsedForOpenOrderFee, - synthUsedForCloseOrderFee, - } = openAndClosePosition(wei(50), wei(2000), wei(1500)); + const { pnl: expectedPnl, totalFees } = openAndClosePosition(wei(50), wei(2000), wei(1500)); it('accrues correct amount of debt', async () => { - currentDebt = expectedPnl; + currentDebt = expectedPnl.sub(totalFees); assertBn.equal(currentDebt.abs().toBN(), await systems().PerpsMarket.debt(accountId)); }); - - it('used collateral to pay order fees', async () => { - const synthUsedForFees = synthUsedForOpenOrderFee().add(synthUsedForCloseOrderFee()); - assertBn.equal( - startingCollateralAmount.sub(synthUsedForFees).toBN(), - await systems().PerpsMarket.getCollateralAmount(accountId, synthMarkets()[0].marketId()) - ); - }); }); describe('positive pnl to lower debt', () => { - const { pnl: expectedPnl } = openAndClosePosition(wei(50), wei(1500), wei(1750)); + const { pnl: expectedPnl, totalFees } = openAndClosePosition(wei(50), wei(1500), wei(1750)); it('reduces debt', async () => { - currentDebt = currentDebt.add(expectedPnl); + currentDebt = currentDebt.add(expectedPnl).sub(totalFees); assertBn.equal(currentDebt.abs().toBN(), await systems().PerpsMarket.debt(accountId)); }); }); describe('positive pnl to eliminate debt and add snxUSD', () => { - const { pnl: expectedPnl, closeOrderFee } = openAndClosePosition(wei(50), wei(1750), wei(2250)); + const { pnl: expectedPnl, totalFees } = openAndClosePosition(wei(50), wei(1750), wei(2250)); it('sets debt to 0', async () => { assertBn.equal(0, await systems().PerpsMarket.debt(accountId)); }); it('sets snxUSD to leftover profit', async () => { - currentDebt = currentDebt.add(expectedPnl).sub(closeOrderFee.totalFees); + // removes fees from snxUSD if available + currentDebt = currentDebt.add(expectedPnl).sub(totalFees); assertBn.equal( currentDebt.toBN(), await systems().PerpsMarket.getCollateralAmount(accountId, 0) @@ -194,21 +156,14 @@ describe('Account Debt', () => { }); describe('negative pnl reduces snxUSD', () => { - const { - pnl: expectedPnl, - openOrderFee, - closeOrderFee, - } = openAndClosePosition(wei(50), wei(2250), wei(2150)); + const { pnl: expectedPnl, totalFees } = openAndClosePosition(wei(50), wei(2250), wei(2150)); it('debt is still 0', async () => { assertBn.equal(0, await systems().PerpsMarket.debt(accountId)); }); it('reduces snxUSD amount', async () => { - currentDebt = currentDebt - .add(expectedPnl) - .sub(openOrderFee.totalFees) - .sub(closeOrderFee.totalFees); + currentDebt = currentDebt.add(expectedPnl).sub(totalFees); assertBn.equal( currentDebt.abs().toBN(), await systems().PerpsMarket.getCollateralAmount(accountId, 0) @@ -217,10 +172,10 @@ describe('Account Debt', () => { }); describe('negative pnl adds debt', () => { - const { pnl: expectedPnl, openOrderFee } = openAndClosePosition(wei(50), wei(2150), wei(1800)); + const { pnl: expectedPnl, totalFees } = openAndClosePosition(wei(50), wei(2150), wei(1800)); it('adds debt', async () => { - currentDebt = currentDebt.add(expectedPnl).sub(openOrderFee.totalFees); + currentDebt = currentDebt.add(expectedPnl).sub(totalFees); assertBn.equal(currentDebt.abs().toBN(), await systems().PerpsMarket.debt(accountId)); }); diff --git a/markets/perps-market/test/integration/Account/Margins.multiCollateral.failure.test.ts b/markets/perps-market/test/integration/Account/Margins.multiCollateral.failure.test.ts index 056307a341..9f0cd305f3 100644 --- a/markets/perps-market/test/integration/Account/Margins.multiCollateral.failure.test.ts +++ b/markets/perps-market/test/integration/Account/Margins.multiCollateral.failure.test.ts @@ -1,7 +1,7 @@ import { bn, bootstrapMarkets } from '../bootstrap'; import assertBn from '@synthetixio/core-utils/src/utils/assertions/assert-bignumber'; import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert'; -import { depositCollateral, discountedValue, openPosition } from '../helpers'; +import { calculateFillPrice, depositCollateral, discountedValue, openPosition } from '../helpers'; import Wei, { wei } from '@synthetixio/wei'; import { ethers } from 'ethers'; @@ -77,7 +77,6 @@ describe('Account margins - Multicollateral - InsufficientCollateralAvailableFor let btcAmount: Wei, btcMarketId: ethers.BigNumber; before('identify', async () => { - spotMarket = systems().SpotMarket; btcMarketId = synthMarkets()[0].marketId(); btcAmount = wei( await systems().PerpsMarket.getCollateralAmount(accountId, synthMarkets()[0].marketId()) @@ -124,13 +123,11 @@ describe('Account margins - Multicollateral - InsufficientCollateralAvailableFor }); let expectedWithdrawableMargin: Wei; - it('should have correct available margin', async () => { + it('should have correct withdrawable margin', async () => { const { totalPnl } = await systems().PerpsMarket.getOpenPosition( accountId, perpsMarkets()[0].marketId() ); - - console.log(availableTradingMargin, requiredMargin, totalPnl); expectedWithdrawableMargin = availableTradingMargin.sub(requiredMargin).add(totalPnl); assertBn.equal( @@ -140,18 +137,43 @@ describe('Account margins - Multicollateral - InsufficientCollateralAvailableFor }); it('reverts when attempting to withdraw more than available', async () => { - const { synthToBurn: btcSynthToWithdraw } = await systems().SpotMarket.quoteSellExactOut( - btcMarketId, - expectedWithdrawableMargin.add(1).toBN(), - bn(0) - ); + const amountToWithdraw = expectedWithdrawableMargin.add(wei(1)); + const btcToWithdraw = amountToWithdraw.div(SYNTH_BTC_PRICE).toBN(); await assertRevert( systems() .PerpsMarket.connect(trader1()) - .modifyCollateral(accountId, btcMarketId, btcSynthToWithdraw.mul(-1)), + .modifyCollateral(accountId, btcMarketId, btcToWithdraw.mul(-1)), 'InsufficientCollateralAvailableForWithdraw' ); }); }); + + describe('close position, accruing debt', () => { + let pnl: Wei; + + before(async () => { + const initialFillPrice = calculateFillPrice(wei(0), wei(100), wei(10), wei(20000)); + const finalFillPrice = calculateFillPrice(wei(10), wei(100), wei(-10), wei(19500)); + pnl = finalFillPrice.sub(initialFillPrice).mul(wei(10)); + await perpsMarkets()[0].aggregator().mockSetCurrentPrice(bn(19500)); + + await openPosition({ + systems, + provider, + trader: trader1(), + accountId, + keeper: trader1(), + marketId: perpsMarkets()[0].marketId(), + sizeDelta: bn(-10), + settlementStrategyId: perpsMarkets()[0].strategyId(), + price: bn(19500), + }); + }); + + it('cannot withdraw due to debt', async () => { + assertBn.equal(await systems().PerpsMarket.getWithdrawableMargin(accountId), 0); + assertBn.equal(await systems().PerpsMarket.debt(accountId), pnl.abs().toBN()); + }); + }); }); diff --git a/markets/perps-market/test/integration/Account/Margins.multiCollateral.test.ts b/markets/perps-market/test/integration/Account/Margins.multiCollateral.test.ts index 9f7c9201f0..f34af2434c 100644 --- a/markets/perps-market/test/integration/Account/Margins.multiCollateral.test.ts +++ b/markets/perps-market/test/integration/Account/Margins.multiCollateral.test.ts @@ -131,6 +131,7 @@ describe('Account margins - Multicollateral', () => { let btcAmount: Wei, ethAmount: Wei, btcMarketId: ethers.BigNumber; before('identify', async () => { + btcMarketId = synthMarkets()[0].marketId(); btcAmount = wei( await systems().PerpsMarket.getCollateralAmount(accountId, synthMarkets()[0].marketId()) ); @@ -240,10 +241,8 @@ describe('Account margins - Multicollateral', () => { it('has correct withdrawable margin', async () => { accruedDebt = await systems().PerpsMarket.debt(accountId); - assertBn.equal( - await systems().PerpsMarket.getWithdrawableMargin(accountId), - availableTradingMargin.sub(accruedDebt).toBN() - ); + // not allowed to withdraw when account has debt + assertBn.equal(await systems().PerpsMarket.getWithdrawableMargin(accountId), 0); assertBn.equal( await systems().PerpsMarket.getAvailableMargin(accountId), diff --git a/markets/perps-market/test/integration/KeeperRewards/KeeperRewards.Settlement.test.ts b/markets/perps-market/test/integration/KeeperRewards/KeeperRewards.Settlement.test.ts index 2ad588c80b..650dc6156d 100644 --- a/markets/perps-market/test/integration/KeeperRewards/KeeperRewards.Settlement.test.ts +++ b/markets/perps-market/test/integration/KeeperRewards/KeeperRewards.Settlement.test.ts @@ -183,23 +183,12 @@ describe('Keeper Rewards - Settlement', () => { await assertEvent(settleTx, `MarketUpdated(${params.join(', ')})`, systems().PerpsMarket); }); - it('emits collateral deducted events', async () => { - let pendingTotalFees = DEFAULT_SETTLEMENT_STRATEGY.settlementReward.add( - KeeperCosts.settlementCost - ); + it('emits account charged event', async () => { const accountId = 2; - - const collateral = collateralsTestCase[0].collateralData.collaterals[0]; - const synthMarket = 0; - let deductedCollateralAmount: ethers.BigNumber = bn(0); - deductedCollateralAmount = collateral.snxUSDAmount().lt(pendingTotalFees) - ? collateral.snxUSDAmount() - : pendingTotalFees; - pendingTotalFees = pendingTotalFees.sub(deductedCollateralAmount); - + const totalFees = DEFAULT_SETTLEMENT_STRATEGY.settlementReward.add(KeeperCosts.settlementCost); await assertEvent( settleTx, - `CollateralDeducted(${accountId}, ${synthMarket}, ${deductedCollateralAmount})`, + `AccountCharged(${accountId}, ${totalFees.mul(-1)}, 0)`, // 0 debt since snxUSD available systems().PerpsMarket ); }); diff --git a/markets/perps-market/test/integration/Liquidation/Liquidation.marginOnly.test.ts b/markets/perps-market/test/integration/Liquidation/Liquidation.marginOnly.test.ts new file mode 100644 index 0000000000..eb7b30efa8 --- /dev/null +++ b/markets/perps-market/test/integration/Liquidation/Liquidation.marginOnly.test.ts @@ -0,0 +1,265 @@ +import assertBn from '@synthetixio/core-utils/utils/assertions/assert-bignumber'; +import { bn, bootstrapMarkets } from '../bootstrap'; +import { OpenPositionData, depositCollateral, openPosition } from '../helpers'; +import { SynthMarkets } from '@synthetixio/spot-market/test/common'; +import assertEvent from '@synthetixio/core-utils/utils/assertions/assert-event'; +import { ethers } from 'ethers'; +import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert'; + +const perpsMarketConfigs = [ + { + requestedMarketId: 51, + name: 'Ether', + token: 'ETH', + price: bn(2000), + fundingParams: { skewScale: bn(1000), maxFundingVelocity: bn(0) }, + liquidationParams: { + initialMarginFraction: bn(2), + minimumInitialMarginRatio: bn(0.01), + maintenanceMarginScalar: bn(0.5), + maxLiquidationLimitAccumulationMultiplier: bn(1), + liquidationRewardRatio: bn(0.02), + maxSecondsInLiquidationWindow: ethers.BigNumber.from(10), + minimumPositionMargin: bn(0), + }, + settlementStrategy: { + settlementReward: bn(0), + }, + }, + { + requestedMarketId: 52, + name: 'snx', + token: 'SNX', + price: bn(10), + fundingParams: { skewScale: bn(100_000), maxFundingVelocity: bn(0) }, + liquidationParams: { + initialMarginFraction: bn(2), + minimumInitialMarginRatio: bn(0.01), + maintenanceMarginScalar: bn(0.5), + maxLiquidationLimitAccumulationMultiplier: bn(1), + liquidationRewardRatio: bn(0.05), + maxSecondsInLiquidationWindow: ethers.BigNumber.from(10), + minimumPositionMargin: bn(0), + }, + settlementStrategy: { + settlementReward: bn(0), + }, + }, +]; + +const btcDiscountConfig = { + upperLimitDiscount: bn(0.08), + lowerLimitDiscount: bn(0.03), + discountScalar: bn(2), + skewScale: bn(100), +}; + +const ethDiscountConfig = { + upperLimitDiscount: bn(0.04), + lowerLimitDiscount: bn(0.02), + discountScalar: bn(3), + skewScale: bn(10_000), +}; + +const KeeperCosts = { + settlementCost: bn(10), + flagCost: bn(20), + liquidateCost: bn(15), +}; + +const MIN_LIQ_REWARD = bn(10); + +describe('liquidation margin only', () => { + const { + systems, + provider, + owner, + trader1, + synthMarkets, + keeper, + keeperCostOracleNode, + perpsMarkets, + superMarketId, + } = bootstrapMarkets({ + liquidationGuards: { + minLiquidationReward: MIN_LIQ_REWARD, + minKeeperProfitRatioD18: bn(0), + maxLiquidationReward: bn(1000), + maxKeeperScalingRatioD18: bn(0.5), + }, + synthMarkets: [ + { + name: 'Bitcoin', + token: 'snxBTC', + buyPrice: bn(30_000), + sellPrice: bn(30_000), + ...btcDiscountConfig, + }, + { + name: 'Ethereum', + token: 'snxETH', + buyPrice: bn(2000), + sellPrice: bn(2000), + ...ethDiscountConfig, + }, + ], + perpsMarkets: perpsMarketConfigs, + traderAccountIds: [2], + }); + + let btcSynth: SynthMarkets[number], ethSynth: SynthMarkets[number]; + let commonOpenPositionProps: Pick< + OpenPositionData, + 'systems' | 'provider' | 'trader' | 'accountId' | 'keeper' + >; + + before('set keeper costs', async () => { + await keeperCostOracleNode() + .connect(owner()) + .setCosts(KeeperCosts.settlementCost, KeeperCosts.flagCost, KeeperCosts.liquidateCost); + }); + + before('add collateral to margin', async () => { + btcSynth = synthMarkets()[0]; + ethSynth = synthMarkets()[1]; + + await depositCollateral({ + systems, + trader: trader1, + accountId: () => 2, + collaterals: [ + { + synthMarket: () => btcSynth, + snxUSDAmount: () => bn(30_000), + }, + { + synthMarket: () => ethSynth, + snxUSDAmount: () => bn(15_000), + }, + { + snxUSDAmount: () => bn(2_000), + }, + ], + }); + }); + before('open positions', async () => { + commonOpenPositionProps = { + systems, + provider, + trader: trader1(), + accountId: 2, + keeper: trader1(), + }; + + const positionSizes = [ + bn(50), // eth long + bn(2000), // link long + ]; + + for (const [i, perpsMarket] of perpsMarkets().entries()) { + await openPosition({ + ...commonOpenPositionProps, + marketId: perpsMarket.marketId(), + sizeDelta: positionSizes[i], + settlementStrategyId: perpsMarket.strategyId(), + price: perpsMarketConfigs[i].price, + }); + } + }); + + it('should not be eligible for liquidation', async () => { + // is not eligible for liquidation + await assertRevert( + systems().PerpsMarket.connect(keeper()).liquidate(2), + 'NotEligibleForLiquidation' + ); + + await assertRevert( + systems().PerpsMarket.connect(keeper()).liquidateMarginOnly(2), + 'AccountHasOpenPositions' + ); + }); + + describe('close positions at a loss', async () => { + before(async () => { + await perpsMarkets()[0].aggregator().mockSetCurrentPrice(bn(1900)); + await perpsMarkets()[1].aggregator().mockSetCurrentPrice(bn(9)); + + await openPosition({ + ...commonOpenPositionProps, + marketId: perpsMarkets()[0].marketId(), + sizeDelta: bn(-50), + settlementStrategyId: perpsMarkets()[0].strategyId(), + price: bn(1400), + }); + await openPosition({ + ...commonOpenPositionProps, + marketId: perpsMarkets()[1].marketId(), + sizeDelta: bn(-2000), + settlementStrategyId: perpsMarkets()[1].strategyId(), + price: bn(7), + }); + }); + + it('accrued debt', async () => { + assertBn.equal(await systems().PerpsMarket.getCollateralAmount(2, 0), bn(0)); // snxUSD empty + }); + + it('is not liquidatable', async () => { + await assertRevert( + systems().PerpsMarket.connect(keeper()).liquidateMarginOnly(2), + 'NotEligibleForMarginLiquidation' + ); + }); + + it('cannot withdraw', async () => { + await assertRevert( + systems().PerpsMarket.connect(trader1()).modifyCollateral(2, btcSynth.marketId(), -1), + 'InsufficientCollateralAvailableForWithdraw' + ); + + await assertRevert( + systems().PerpsMarket.connect(trader1()).modifyCollateral(2, ethSynth.marketId(), -1), + 'InsufficientCollateralAvailableForWithdraw' + ); + + assertBn.equal(await systems().PerpsMarket.getWithdrawableMargin(2), 0); + }); + }); + + describe('move synth prices to make account liquidatable', async () => { + let liquidateTxn: ethers.providers.TransactionResponse, seizedCollateralValue: ethers.BigNumber; + before(async () => { + await btcSynth.sellAggregator().mockSetCurrentPrice(bn(25_000)); + await ethSynth.sellAggregator().mockSetCurrentPrice(bn(1500)); + + seizedCollateralValue = await systems().PerpsMarket.totalCollateralValue(2); + + // liquidate margin only + liquidateTxn = await systems().PerpsMarket.connect(keeper()).liquidateMarginOnly(2); + }); + + const keeperReward = KeeperCosts.flagCost + .mul(2) + .add(KeeperCosts.liquidateCost) + .add(MIN_LIQ_REWARD); + + it('sent keeper the right reward', async () => { + const keeperBalance = await systems().USD.balanceOf(await keeper().getAddress()); + assertBn.equal(keeperBalance, keeperReward); + }); + + it('emits event', async () => { + await assertEvent( + liquidateTxn, + `AccountMarginLiquidation(2, ${seizedCollateralValue}, ${keeperReward})`, + systems().PerpsMarket + ); + }); + + it('resets debt', async () => { + assertBn.equal(await systems().PerpsMarket.debt(2), 0); + assertBn.equal(await systems().PerpsMarket.reportedDebt(superMarketId()), 0); + }); + }); +}); diff --git a/markets/perps-market/test/integration/Market/MarketDebt.withFunding.test.ts b/markets/perps-market/test/integration/Market/MarketDebt.withFunding.test.ts index ffc722624f..531913b7a7 100644 --- a/markets/perps-market/test/integration/Market/MarketDebt.withFunding.test.ts +++ b/markets/perps-market/test/integration/Market/MarketDebt.withFunding.test.ts @@ -1,6 +1,6 @@ import { fastForwardTo, getTxTime } from '@synthetixio/core-utils/utils/hardhat/rpc'; import { PerpsMarket, bn, bootstrapMarkets } from '../bootstrap'; -import { openPosition } from '../helpers'; +import { depositCollateral, openPosition } from '../helpers'; import assertBn from '@synthetixio/core-utils/utils/assertions/assert-bignumber'; import { ethers } from 'ethers'; @@ -16,52 +16,61 @@ const interestRateParams = { describe('Market Debt - with funding', () => { const traderAccountIds = [2, 3, 4]; - const { systems, superMarketId, perpsMarkets, provider, trader1, trader2, trader3, keeper } = - bootstrapMarkets({ - interestRateParams, - synthMarkets: [ - { - name: 'Bitcoin', - token: 'snxBTC', - buyPrice: bn(10_000), - sellPrice: bn(10_000), + const { + systems, + superMarketId, + synthMarkets, + perpsMarkets, + provider, + trader1, + trader2, + trader3, + keeper, + } = bootstrapMarkets({ + interestRateParams, + synthMarkets: [ + { + name: 'Bitcoin', + token: 'snxBTC', + buyPrice: bn(10_000), + sellPrice: bn(10_000), + }, + ], + perpsMarkets: [ + { + requestedMarketId: bn(25), + name: 'Ether', + token: 'snxETH', + price: bn(1000), + lockedOiRatioD18: bn(1), + // setting to 0 to avoid funding and p/d price change affecting pnl + orderFees: { + makerFee: bn(0.0005), // 0bps no fees + takerFee: bn(0.00025), }, - ], - perpsMarkets: [ - { - requestedMarketId: bn(25), - name: 'Ether', - token: 'snxETH', - price: bn(1000), - lockedOiRatioD18: bn(1), - // setting to 0 to avoid funding and p/d price change affecting pnl - orderFees: { - makerFee: bn(0.0005), // 0bps no fees - takerFee: bn(0.00025), - }, - fundingParams: { skewScale: _SKEW_SCALE, maxFundingVelocity: _MAX_FUNDING_VELOCITY }, - liquidationParams: { - initialMarginFraction: bn(3), - minimumInitialMarginRatio: bn(0.01), - maintenanceMarginScalar: bn(0.5), - maxLiquidationLimitAccumulationMultiplier: bn(1), - liquidationRewardRatio: bn(0.05), - maxSecondsInLiquidationWindow: ethers.BigNumber.from(10), - minimumPositionMargin: bn(0), - }, - settlementStrategy: { - settlementReward: bn(0), - }, + fundingParams: { skewScale: _SKEW_SCALE, maxFundingVelocity: _MAX_FUNDING_VELOCITY }, + liquidationParams: { + initialMarginFraction: bn(3), + minimumInitialMarginRatio: bn(0.01), + maintenanceMarginScalar: bn(0.5), + maxLiquidationLimitAccumulationMultiplier: bn(1), + liquidationRewardRatio: bn(0.05), + maxSecondsInLiquidationWindow: ethers.BigNumber.from(10), + minimumPositionMargin: bn(0), + }, + settlementStrategy: { + settlementReward: bn(0), }, - ], - traderAccountIds, - liquidationGuards: { - minLiquidationReward: bn(0), - minKeeperProfitRatioD18: bn(0), - maxLiquidationReward: bn(10_000), - maxKeeperScalingRatioD18: bn(1), }, - }); + ], + traderAccountIds, + liquidationGuards: { + minLiquidationReward: bn(0), + minKeeperProfitRatioD18: bn(0), + maxLiquidationReward: bn(10_000), + maxKeeperScalingRatioD18: bn(1), + }, + }); let perpsMarket: PerpsMarket; before('identify actors', async () => { @@ -69,8 +78,28 @@ describe('Market Debt - with funding', () => { }); before('add collateral to margin', async () => { - await systems().PerpsMarket.connect(trader1()).modifyCollateral(2, 0, bn(12_500)); - await systems().PerpsMarket.connect(trader2()).modifyCollateral(3, 0, bn(12_500)); + await depositCollateral({ + systems, + trader: trader1, + accountId: () => 2, + collaterals: [ + { + synthMarket: () => synthMarkets()[0], + snxUSDAmount: () => bn(15_000), + }, + ], + }); + await depositCollateral({ + systems, + trader: trader2, + accountId: () => 3, + collaterals: [ + { + synthMarket: () => synthMarkets()[0], + snxUSDAmount: () => bn(12_500), + }, + ], + }); await systems().PerpsMarket.connect(trader3()).modifyCollateral(4, 0, bn(100_000)); }); @@ -89,15 +118,24 @@ describe('Market Debt - with funding', () => { ); const interestCharges = trader1Interest.add(trader2Interest).add(trader3Interest); return { + totalAccountDebt: await calculateAccountDebt(), totalUnrealizedPnl: trader1Pnl.add(trader2Pnl).add(trader3Pnl).add(interestCharges), marketCollateralValue: await systems().PerpsMarket.totalGlobalCollateralValue(), }; }; + const calculateAccountDebt = async () => { + const account1Debt = await systems().PerpsMarket.debt(2); + const account2Debt = await systems().PerpsMarket.debt(3); + const account3Debt = await systems().PerpsMarket.debt(4); + + return account1Debt.add(account2Debt).add(account3Debt); + }; + describe('with no positions', () => { it('should report total collateral value as debt', async () => { const debt = await systems().PerpsMarket.reportedDebt(superMarketId()); - assertBn.equal(debt, bn(125_000)); + assertBn.equal(debt, bn(127_500)); }); }); @@ -140,10 +178,15 @@ describe('Market Debt - with funding', () => { }); it('reports correct debt', async () => { - const { totalUnrealizedPnl, marketCollateralValue } = await calculateExpectedPnls(); + const { totalAccountDebt, totalUnrealizedPnl, marketCollateralValue } = + await calculateExpectedPnls(); const debt = await systems().PerpsMarket.reportedDebt(superMarketId()); - assertBn.near(debt, marketCollateralValue.add(totalUnrealizedPnl), bn(0.0001)); + assertBn.near( + debt, + marketCollateralValue.add(totalUnrealizedPnl).sub(totalAccountDebt), + bn(0.0001) + ); }); }); @@ -153,23 +196,62 @@ describe('Market Debt - with funding', () => { }); it('reports correct debt', async () => { - const { totalUnrealizedPnl, marketCollateralValue } = await calculateExpectedPnls(); + const { totalAccountDebt, totalUnrealizedPnl, marketCollateralValue } = + await calculateExpectedPnls(); const debt = await systems().PerpsMarket.reportedDebt(superMarketId()); - assertBn.near(debt, marketCollateralValue.add(totalUnrealizedPnl), bn(0.0001)); + assertBn.near( + debt, + marketCollateralValue.add(totalUnrealizedPnl).sub(totalAccountDebt), + bn(0.0001) + ); + }); + }); + + describe('price change and reduce position 1', () => { + before(async () => { + await perpsMarket.aggregator().mockSetCurrentPrice(bn(998)); + await openPosition({ + systems, + provider, + trader: trader1(), + accountId: 2, + keeper: keeper(), + marketId: perpsMarket.marketId(), + sizeDelta: bn(-50), + settlementStrategyId: perpsMarket.strategyId(), + price: bn(998), + }); + }); + + it('reports correct debt', async () => { + const { totalAccountDebt, totalUnrealizedPnl, marketCollateralValue } = + await calculateExpectedPnls(); + + const debt = await systems().PerpsMarket.reportedDebt(superMarketId()); + assertBn.near( + debt, + marketCollateralValue.add(totalUnrealizedPnl).sub(totalAccountDebt), + bn(0.0001) + ); }); }); describe('price change', () => { - before('change price', async () => { + before(async () => { await perpsMarket.aggregator().mockSetCurrentPrice(bn(1050)); }); it('reports correct debt', async () => { - const { totalUnrealizedPnl, marketCollateralValue } = await calculateExpectedPnls(); + const { totalAccountDebt, totalUnrealizedPnl, marketCollateralValue } = + await calculateExpectedPnls(); const debt = await systems().PerpsMarket.reportedDebt(superMarketId()); - assertBn.near(debt, marketCollateralValue.add(totalUnrealizedPnl), bn(0.00001)); + assertBn.near( + debt, + marketCollateralValue.add(totalUnrealizedPnl).sub(totalAccountDebt), + bn(0.00001) + ); }); }); @@ -189,10 +271,11 @@ describe('Market Debt - with funding', () => { }); it('reports correct debt', async () => { - const { totalUnrealizedPnl, marketCollateralValue } = await calculateExpectedPnls(); + const { totalAccountDebt, totalUnrealizedPnl, marketCollateralValue } = + await calculateExpectedPnls(); const debt = await systems().PerpsMarket.reportedDebt(superMarketId()); - assertBn.equal(debt, marketCollateralValue.add(totalUnrealizedPnl)); + assertBn.equal(debt, marketCollateralValue.add(totalUnrealizedPnl).sub(totalAccountDebt)); }); }); @@ -212,10 +295,11 @@ describe('Market Debt - with funding', () => { }); it('reports correct debt', async () => { - const { totalUnrealizedPnl, marketCollateralValue } = await calculateExpectedPnls(); + const { totalAccountDebt, totalUnrealizedPnl, marketCollateralValue } = + await calculateExpectedPnls(); const debt = await systems().PerpsMarket.reportedDebt(superMarketId()); - assertBn.equal(debt, marketCollateralValue.add(totalUnrealizedPnl)); + assertBn.equal(debt, marketCollateralValue.add(totalUnrealizedPnl).sub(totalAccountDebt)); }); }); @@ -225,27 +309,38 @@ describe('Market Debt - with funding', () => { await systems().PerpsMarket.connect(keeper()).liquidate(3); }); + it('resets trader debt to 0', async () => { + const debt = await systems().PerpsMarket.debt(3); + assertBn.equal(debt, 0); + }); + it('reports correct debt', async () => { - const { totalUnrealizedPnl, marketCollateralValue } = await calculateExpectedPnls(); + const { totalAccountDebt, totalUnrealizedPnl, marketCollateralValue } = + await calculateExpectedPnls(); const debt = await systems().PerpsMarket.reportedDebt(superMarketId()); - assertBn.equal(debt, marketCollateralValue.add(totalUnrealizedPnl)); + assertBn.equal(debt, marketCollateralValue.add(totalUnrealizedPnl).sub(totalAccountDebt)); }); }); let partialLiquidationTime: number; describe('trader 1 gets partially liquidated', () => { before('change price', async () => { - await perpsMarket.aggregator().mockSetCurrentPrice(bn(950)); + await perpsMarket.aggregator().mockSetCurrentPrice(bn(900)); const liquidateTxn = await systems().PerpsMarket.connect(keeper()).liquidate(2); partialLiquidationTime = await getTxTime(provider(), liquidateTxn); }); it('reports correct debt', async () => { - const { totalUnrealizedPnl, marketCollateralValue } = await calculateExpectedPnls(); + const { totalAccountDebt, totalUnrealizedPnl, marketCollateralValue } = + await calculateExpectedPnls(); const debt = await systems().PerpsMarket.reportedDebt(superMarketId()); - assertBn.near(debt, marketCollateralValue.add(totalUnrealizedPnl), bn(0.000001)); + assertBn.near( + debt, + marketCollateralValue.add(totalUnrealizedPnl).sub(totalAccountDebt), + bn(0.000001) + ); }); }); @@ -262,11 +357,21 @@ describe('Market Debt - with funding', () => { await systems().PerpsMarket.connect(keeper()).liquidate(2); }); + it('resets trader debt to 0', async () => { + const debt = await systems().PerpsMarket.debt(2); + assertBn.equal(debt, 0); + }); + it('reports correct debt', async () => { - const { totalUnrealizedPnl, marketCollateralValue } = await calculateExpectedPnls(); + const { totalAccountDebt, totalUnrealizedPnl, marketCollateralValue } = + await calculateExpectedPnls(); const debt = await systems().PerpsMarket.reportedDebt(superMarketId()); - assertBn.near(debt, marketCollateralValue.add(totalUnrealizedPnl), bn(0.000001)); + assertBn.near( + debt, + marketCollateralValue.add(totalUnrealizedPnl).sub(totalAccountDebt), + bn(0.000001) + ); }); it('fully liquidated trader 1', async () => { diff --git a/markets/perps-market/test/integration/Markets/GlobalPerpsMarket.test.ts b/markets/perps-market/test/integration/Markets/GlobalPerpsMarket.test.ts index ed35abdf2c..e78d329050 100644 --- a/markets/perps-market/test/integration/Markets/GlobalPerpsMarket.test.ts +++ b/markets/perps-market/test/integration/Markets/GlobalPerpsMarket.test.ts @@ -1,4 +1,4 @@ -import { BigNumber, ethers } from 'ethers'; +import { ethers } from 'ethers'; import { bn, bootstrapMarkets } from '../bootstrap'; import assertBn from '@synthetixio/core-utils/src/utils/assertions/assert-bignumber'; import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert'; @@ -17,20 +17,16 @@ describe('GlobalPerpsMarket', () => { traderAccountIds: [], }); - before( - 'set maxCollateralAmounts, synthDeductionPriority, minLiquidationRewardUsd, maxLiquidationRewardUsd', - async () => { - await systems().PerpsMarket.setCollateralConfiguration( - perpsMarkets()[0].marketId(), - bn(10000), - 0, - 0, - 0 - ); - await systems().PerpsMarket.setSynthDeductionPriority([1, 2]); - await systems().PerpsMarket.setKeeperRewardGuards(100, bn(0.001), 500, bn(0.005)); - } - ); + before('set maxCollateralAmounts, minLiquidationRewardUsd, maxLiquidationRewardUsd', async () => { + await systems().PerpsMarket.setCollateralConfiguration( + perpsMarkets()[0].marketId(), + bn(10000), + 0, + 0, + 0 + ); + await systems().PerpsMarket.setKeeperRewardGuards(100, bn(0.001), 500, bn(0.005)); + }); it('returns the supermarket name', async () => { assert.equal(await systems().PerpsMarket.name(superMarketId()), 'SuperMarket Perps Market'); @@ -68,13 +64,6 @@ describe('GlobalPerpsMarket', () => { assertBn.equal(maxCollateralAmount, bn(10000)); }); - it('returns the correct synthDeductionPriority ', async () => { - const synths = await systems().PerpsMarket.getSynthDeductionPriority(); - synths.forEach((synth, index) => { - assertBn.equal(synth, BigNumber.from(index + 1)); - }); - }); - it('returns the correct minKeeperRewardUsd ', async () => { const liquidationGuards = await systems().PerpsMarket.getKeeperRewardGuards(); assertBn.equal(liquidationGuards.minKeeperRewardUsd, 100); @@ -96,10 +85,6 @@ describe('GlobalPerpsMarket', () => { .setCollateralConfiguration(perpsMarkets()[0].marketId(), bn(10000), 0, 0, 0), `Unauthorized("${await trader1().getAddress()}")` ); - await assertRevert( - systems().PerpsMarket.connect(trader1()).setSynthDeductionPriority([1, 2]), - `Unauthorized("${await trader1().getAddress()}")` - ); }); it('transaction should fail if min and max are inverted', async () => { diff --git a/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.cancel.test.ts b/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.cancel.test.ts index 3497db56f4..a1c86ba27a 100644 --- a/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.cancel.test.ts +++ b/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.cancel.test.ts @@ -236,9 +236,15 @@ describe('Cancel Offchain Async Order test', () => { let cancelTx: ethers.ContractTransaction; let accountBalanceBefore: ethers.BigNumber; let keeperBalanceBefore: ethers.BigNumber; + let expectedDebt: ethers.BigNumber; const settlementReward = DEFAULT_SETTLEMENT_STRATEGY.settlementReward; before('collect initial balances', async () => { + // if snxUSD available, subtract settlementReward from snxUSD otherwise add to debt + const startingSnxUSDBalance = await systems().PerpsMarket.getCollateralAmount(2, 0); + const leftoverSnxUsd = startingSnxUSDBalance.sub(settlementReward); + expectedDebt = leftoverSnxUsd.lt(0) ? leftoverSnxUsd.abs() : wei(0).toBN(); + accountBalanceBefore = await systems().PerpsMarket.getAvailableMargin(2); keeperBalanceBefore = await systems().USD.balanceOf(await keeper().getAddress()); }); @@ -275,29 +281,13 @@ describe('Cancel Offchain Async Order test', () => { ); }); - it('emits collateral deducted events', async () => { - let pendingSettlementRewards = settlementReward; - const accountId = 2; - - for (let i = 0; i < testCase.collateralData.collaterals.length; i++) { - const collateral = testCase.collateralData.collaterals[i]; - const synthMarket = collateral.synthMarket ? collateral.synthMarket().marketId() : 0; - let deductedCollateralAmount: ethers.BigNumber = bn(0); - if (synthMarket == 0) { - deductedCollateralAmount = collateral.snxUSDAmount().lt(pendingSettlementRewards) - ? collateral.snxUSDAmount() - : pendingSettlementRewards; - } else { - deductedCollateralAmount = pendingSettlementRewards.div(10_000); - } - pendingSettlementRewards = pendingSettlementRewards.sub(deductedCollateralAmount); - - await assertEvent( - cancelTx, - `CollateralDeducted(${accountId}, ${synthMarket}, ${deductedCollateralAmount})`, - systems().PerpsMarket - ); - } + it('emits AccountCharged event', async () => { + const params = [2, settlementReward, expectedDebt]; + await assertEvent( + cancelTx, + `AccountCharged(${params.join(', ')})`, + systems().PerpsMarket + ); }); it('updates balances accordingly', async () => { @@ -305,6 +295,8 @@ describe('Cancel Offchain Async Order test', () => { const keeperBalanceAfter = await systems().USD.balanceOf(await keeper().getAddress()); assertBn.equal(keeperBalanceAfter, keeperBalanceBefore.add(settlementReward)); assertBn.equal(accountBalanceAfter, accountBalanceBefore.sub(settlementReward)); + + assertBn.equal(await systems().PerpsMarket.debt(2), expectedDebt); }); it('check account open position market ids', async () => { diff --git a/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.fees.test.ts b/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.fees.test.ts index 25aa8d0e43..50c0f934b3 100644 --- a/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.fees.test.ts +++ b/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.fees.test.ts @@ -70,6 +70,7 @@ describe('Offchain Async Order test - fees', () => { }, ], }, + availableUsdAmount: bn(10_000), // using this to track snxUSDAmount }, { name: 'only snxBTC', @@ -84,6 +85,7 @@ describe('Offchain Async Order test - fees', () => { }, ], }, + availableUsdAmount: bn(0), // only snxBTC so in this case, fees/pnl added to debt }, { name: 'snxUSD and snxBTC', @@ -101,6 +103,7 @@ describe('Offchain Async Order test - fees', () => { }, ], }, + availableUsdAmount: bn(2), }, ]; @@ -233,13 +236,16 @@ describe('Offchain Async Order test - fees', () => { }); it('validate fees paid on settle', async () => { - const { traderBalance, keeperBalance } = await getBalances(); + const { traderBalance, keeperBalance, traderDebt } = await getBalances(); - assertBn.equal( - traderBalance, - balancesBeforeLong.traderBalance.sub(feesPaidOnSettle.totalFees) + const { traderBalanceUsed, debt } = calculateDebtAndLeftover( + testCase.availableUsdAmount, + feesPaidOnSettle.totalFees ); + assertBn.equal(traderBalance, balancesBeforeLong.traderBalance.sub(traderBalanceUsed)); + assertBn.equal(traderDebt, debt); + assertBn.equal( keeperBalance, balancesBeforeLong.keeperBalance.add(feesPaidOnSettle.keeperFee) @@ -304,10 +310,15 @@ describe('Offchain Async Order test - fees', () => { }); it('validate fees paid on long position', async () => { - assertBn.equal( - balancesAfterLong.traderBalance, - balancesBeforeLong.traderBalance.sub(feesPaidOnLong.totalFees) + const { traderBalanceUsed, debt } = calculateDebtAndLeftover( + testCase.availableUsdAmount, + feesPaidOnLong.totalFees ); + + const availableTraderBalance = balancesBeforeLong.traderBalance.sub(traderBalanceUsed); + + assertBn.equal(balancesAfterLong.traderBalance, availableTraderBalance); + assertBn.equal(balancesAfterLong.traderDebt, debt); assertBn.equal( balancesAfterLong.keeperBalance, balancesBeforeLong.keeperBalance.add(feesPaidOnLong.keeperFee) @@ -340,12 +351,11 @@ describe('Offchain Async Order test - fees', () => { it('validate fees paid on short position', async () => { const { traderBalance, keeperBalance } = await getBalances(); - assertBn.equal( - traderBalance, - balancesAfterLong.traderBalance - .sub(feesPaidOnShort.totalFees) - .add(balancesAfterLong.accountPnl) - ); + const expectedTraderBalance = testCase.availableUsdAmount.gt(bn(2)) + ? balancesAfterLong.traderBalance.sub(feesPaidOnShort.totalFees) + : balancesAfterLong.traderBalance; + + assertBn.equal(traderBalance, expectedTraderBalance); assertBn.equal( keeperBalance, balancesAfterLong.keeperBalance.add(feesPaidOnShort.keeperFee) @@ -377,10 +387,11 @@ describe('Offchain Async Order test - fees', () => { it('validate fees paid', async () => { const { traderBalance, keeperBalance } = await getBalances(); - assertBn.equal( - traderBalance, - balancesAfterLong.traderBalance.sub(feesPaidOnShort.totalFees) - ); + const expectedTraderBalance = testCase.availableUsdAmount.gt(bn(2)) + ? balancesAfterLong.traderBalance.sub(feesPaidOnShort.totalFees) + : balancesAfterLong.traderBalance; + + assertBn.equal(traderBalance, expectedTraderBalance); assertBn.equal( keeperBalance, balancesAfterLong.keeperBalance.add(feesPaidOnShort.keeperFee) @@ -393,12 +404,29 @@ describe('Offchain Async Order test - fees', () => { const getBalances = async () => { const traderBalance = await systems().PerpsMarket.totalCollateralValue(2); + const traderDebt = await systems().PerpsMarket.debt(2); const keeperBalance = await systems().USD.balanceOf(await keeper().getAddress()); const accountPnl = (await systems().PerpsMarket.getOpenPosition(2, ethMarketId))[0]; return { traderBalance, keeperBalance, accountPnl, + traderDebt, }; }; }); + +const calculateDebtAndLeftover = ( + availableUsdAmount: ethers.BigNumber, + totalFees: ethers.BigNumber +) => { + const leftoverUsd = availableUsdAmount.sub(totalFees); + const traderBalanceUsed = leftoverUsd.gt(0) ? totalFees : availableUsdAmount; + + const hasSnxUsdToCoverFees = availableUsdAmount.gt(totalFees); + + return { + traderBalanceUsed, + debt: hasSnxUsdToCoverFees ? bn(0) : leftoverUsd.mul(-1), + }; +}; diff --git a/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.settle.test.ts b/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.settle.test.ts index 6699331746..4e8260fa7e 100644 --- a/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.settle.test.ts +++ b/markets/perps-market/test/integration/Orders/OffchainAsyncOrder.settle.test.ts @@ -310,6 +310,22 @@ describe('Settle Offchain Async Order test', () => { ); }); + it('emits AccountCharged event', async () => { + const snxUsdAmount = + testCase.collateralData.collaterals[0].synthMarket === undefined + ? testCase.collateralData.collaterals[0].snxUSDAmount() + : bn(0); + const chargeAmount = DEFAULT_SETTLEMENT_STRATEGY.settlementReward; + const accruedDebt = chargeAmount.gt(snxUsdAmount) + ? chargeAmount.sub(snxUsdAmount) + : bn(0); + await assertEvent( + settleTx, + `AccountCharged(2, ${chargeAmount.mul(-1)}, ${accruedDebt})`, + systems().PerpsMarket + ); + }); + it('emits market updated event', async () => { const price = bn(1000); const marketSize = bn(1); @@ -338,31 +354,6 @@ describe('Settle Offchain Async Order test', () => { ); }); - it('emits collateral deducted events', async () => { - let pendingTotalFees = DEFAULT_SETTLEMENT_STRATEGY.settlementReward; - const accountId = 2; - - for (let i = 0; i < testCase.collateralData.collaterals.length; i++) { - const collateral = testCase.collateralData.collaterals[i]; - const synthMarket = collateral.synthMarket ? collateral.synthMarket().marketId() : 0; - let deductedCollateralAmount: ethers.BigNumber = bn(0); - if (synthMarket == 0) { - deductedCollateralAmount = collateral.snxUSDAmount().lt(pendingTotalFees) - ? collateral.snxUSDAmount() - : pendingTotalFees; - } else { - deductedCollateralAmount = pendingTotalFees.div(10_000); - } - pendingTotalFees = pendingTotalFees.sub(deductedCollateralAmount); - - await assertEvent( - settleTx, - `CollateralDeducted(${accountId}, ${synthMarket}, ${deductedCollateralAmount})`, - systems().PerpsMarket - ); - } - }); - it('check position is live', async () => { const [pnl, funding, size] = await systems().PerpsMarket.getOpenPosition( 2, diff --git a/markets/perps-market/test/integration/bootstrap/bootstrap.ts b/markets/perps-market/test/integration/bootstrap/bootstrap.ts index 4a9471cb65..9cbe884eb4 100644 --- a/markets/perps-market/test/integration/bootstrap/bootstrap.ts +++ b/markets/perps-market/test/integration/bootstrap/bootstrap.ts @@ -216,13 +216,6 @@ export function bootstrapMarkets(data: BootstrapArgs) { ); }); - // auto add all synth markets in the row they were created for deduction priority - before('set synth deduction priority', async () => { - // first item is always snxUSD - const synthIds = [bn(0), ...synthMarkets().map((s) => s.marketId())]; - await systems().PerpsMarket.connect(owner()).setSynthDeductionPriority(synthIds); - }); - before('set reward distributor', async () => { const { collateralLiquidateRewardRatio } = data; await systems()