Skip to content

Commit 2a2882c

Browse files
authored
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
1 parent 32d2eb1 commit 2a2882c

27 files changed

+688
-534
lines changed

markets/perps-market/cannonfile.test.toml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,6 @@ func = "setFeatureFlagAllowAll"
132132
from = "<%= settings.owner %>"
133133
args = ["<%= formatBytes32String('perpsSystem') %>", true]
134134

135-
# add snxUSD as the only priority to deduct from on a given account, as default
136-
[invoke.setSynthDeductionPriority]
137-
target = ["PerpsMarketProxy"]
138-
func = "setSynthDeductionPriority"
139-
from = "<%= settings.owner %>"
140-
args = [[0]]
141-
142135
[contract.MockPythERC7412Wrapper]
143136
artifact = "contracts/mocks/MockPythERC7412Wrapper.sol:MockPythERC7412Wrapper"
144137

markets/perps-market/contracts/interfaces/IAccountEvents.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ pragma solidity >=0.8.11 <0.9.0;
66
*/
77
interface IAccountEvents {
88
/**
9-
* @notice Gets fired when some collateral is deducted from the account for paying fees or liquidations.
10-
* @param account Id of the account being deducted.
11-
* @param collateralId Id of the collateral (synth) deducted.
9+
* @notice Gets fired anytime an account is charged with fees, paying settlement rewards.
10+
* @param accountId Id of the account being deducted.
1211
* @param amount Amount of synth market deducted from the account.
12+
* @param accountDebt current debt of the account after charged amount.
1313
*/
14-
event CollateralDeducted(uint256 account, uint128 collateralId, uint256 amount);
14+
event AccountCharged(uint128 accountId, int256 amount, uint256 accountDebt);
1515
}

markets/perps-market/contracts/interfaces/IAsyncOrderSettlementPythModule.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@ interface IAsyncOrderSettlementPythModule {
5252
int256 pnl;
5353
uint256 chargedInterest;
5454
int256 accruedFunding;
55-
uint256 pnlUint;
56-
uint256 amountToDeduct;
5755
uint256 settlementReward;
5856
uint256 fillPrice;
5957
uint256 totalFees;
@@ -64,6 +62,8 @@ interface IAsyncOrderSettlementPythModule {
6462
uint256 synthDeductionIterator;
6563
uint128[] deductedSynthIds;
6664
uint256[] deductedAmount;
65+
int256 chargedAmount;
66+
uint256 newAccountDebt;
6767
}
6868

6969
/**

markets/perps-market/contracts/interfaces/IGlobalPerpsMarketModule.sol

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,6 @@ interface IGlobalPerpsMarketModule {
1212
*/
1313
event InterestRateUpdated(uint128 indexed superMarketId, uint128 interestRate);
1414

15-
/**
16-
* @notice Gets fired when the synth deduction priority is updated by owner.
17-
* @param newSynthDeductionPriority new synth id priority order for deductions.
18-
*/
19-
event SynthDeductionPrioritySet(uint128[] newSynthDeductionPriority);
20-
2115
/**
2216
* @notice Gets fired when keeper reward guard is set or updated.
2317
* @param minKeeperRewardUsd Minimum keeper reward expressed as USD value.
@@ -97,20 +91,6 @@ interface IGlobalPerpsMarketModule {
9791
view
9892
returns (uint256[] memory supportedCollaterals);
9993

100-
/**
101-
* @notice Sets the synth deduction priority ordered list.
102-
* @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.
103-
* @param newSynthDeductionPriority Ordered array of synth market ids for deduction priority.
104-
*/
105-
function setSynthDeductionPriority(uint128[] memory newSynthDeductionPriority) external;
106-
107-
/**
108-
* @notice Gets the synth deduction priority ordered list.
109-
* @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.
110-
* @return synthDeductionPriority Ordered array of synth market ids for deduction priority.
111-
*/
112-
function getSynthDeductionPriority() external view returns (uint128[] memory);
113-
11494
/**
11595
* @notice Sets the keeper reward guard (min and max).
11696
* @param minKeeperRewardUsd Minimum keeper reward expressed as USD value.

markets/perps-market/contracts/interfaces/ILiquidationModule.sol

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ interface ILiquidationModule {
6363
bool fullLiquidation
6464
);
6565

66+
/**
67+
* @notice Gets fired when an account margin is liquidated due to not paying down debt.
68+
* @param accountId Id of the account liquidated.
69+
* @param seizedMarginValue margin seized due to liquidation.
70+
* @param liquidationReward reward for liquidating margin account
71+
*/
72+
event AccountMarginLiquidation(
73+
uint128 indexed accountId,
74+
uint256 seizedMarginValue,
75+
uint256 liquidationReward
76+
);
77+
6678
/**
6779
* @notice Liquidates an account.
6880
* @dev according to the current situation and account size it can be a partial or full liquidation.

markets/perps-market/contracts/modules/AsyncOrderCancelModule.sol

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,13 @@ contract AsyncOrderCancelModule is IAsyncOrderCancelModule, IMarketEvents, IAcco
6969
runtime.fillPrice = asyncOrder.validateCancellation(settlementStrategy, price);
7070

7171
if (runtime.settlementReward > 0) {
72-
// deduct keeper reward
73-
(uint128[] memory deductedSynthIds, uint256[] memory deductedAmount) = PerpsAccount
74-
.load(runtime.accountId)
75-
.deductFromAccount(runtime.settlementReward);
76-
for (uint256 i = 0; i < deductedSynthIds.length; i++) {
77-
if (deductedAmount[i] > 0) {
78-
emit CollateralDeducted(
79-
runtime.accountId,
80-
deductedSynthIds[i],
81-
deductedAmount[i]
82-
);
83-
}
84-
}
72+
// charge account the settlement reward
73+
uint256 accountDebt = PerpsAccount.load(runtime.accountId).charge(
74+
-runtime.settlementReward.toInt()
75+
);
76+
77+
emit AccountCharged(runtime.accountId, runtime.settlementReward.toInt(), accountDebt);
78+
8579
// pay keeper
8680
PerpsMarketFactory.load().withdrawMarketUsd(
8781
ERC2771Context._msgSender(),

markets/perps-market/contracts/modules/AsyncOrderSettlementPythModule.sol

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@ contract AsyncOrderSettlementPythModule is
7777
Position.Data storage oldPosition;
7878
(runtime.newPosition, runtime.totalFees, runtime.fillPrice, oldPosition) = asyncOrder
7979
.validateRequest(settlementStrategy, price);
80-
81-
runtime.amountToDeduct = runtime.totalFees;
8280
runtime.sizeDelta = asyncOrder.request.sizeDelta;
8381

8482
PerpsMarketFactory.Data storage factory = PerpsMarketFactory.load();
@@ -88,7 +86,10 @@ contract AsyncOrderSettlementPythModule is
8886
(runtime.pnl, , runtime.chargedInterest, runtime.accruedFunding, , ) = oldPosition.getPnl(
8987
runtime.fillPrice
9088
);
91-
perpsAccount.applyPnl(runtime.pnl);
89+
90+
runtime.chargedAmount = runtime.pnl - runtime.totalFees.toInt();
91+
perpsAccount.charge(runtime.chargedAmount);
92+
emit AccountCharged(runtime.accountId, runtime.chargedAmount, perpsAccount.debt);
9293

9394
// after pnl is realized, update position
9495
runtime.updateData = PerpsMarket.loadValid(runtime.marketId).updatePositionData(
@@ -108,29 +109,7 @@ contract AsyncOrderSettlementPythModule is
108109
runtime.updateData.interestRate
109110
);
110111

111-
// since margin is deposited when trader deposits, as long as the owed collateral is deducted
112-
// from internal accounting, fees are automatically realized by the stakers
113-
if (runtime.amountToDeduct > 0) {
114-
(runtime.deductedSynthIds, runtime.deductedAmount) = perpsAccount.deductFromAccount(
115-
runtime.amountToDeduct
116-
);
117-
for (
118-
runtime.synthDeductionIterator = 0;
119-
runtime.synthDeductionIterator < runtime.deductedSynthIds.length;
120-
runtime.synthDeductionIterator++
121-
) {
122-
if (runtime.deductedAmount[runtime.synthDeductionIterator] > 0) {
123-
emit CollateralDeducted(
124-
runtime.accountId,
125-
runtime.deductedSynthIds[runtime.synthDeductionIterator],
126-
runtime.deductedAmount[runtime.synthDeductionIterator]
127-
);
128-
}
129-
}
130-
}
131-
runtime.settlementReward =
132-
settlementStrategy.settlementReward +
133-
KeeperCosts.load().getSettlementKeeperCosts();
112+
runtime.settlementReward = AsyncOrder.settlementRewardCost(settlementStrategy);
134113

135114
if (runtime.settlementReward > 0) {
136115
// pay keeper

markets/perps-market/contracts/modules/GlobalPerpsMarketModule.sol

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,27 +38,6 @@ contract GlobalPerpsMarketModule is IGlobalPerpsMarketModule {
3838
supportedCollaterals = store.supportedCollateralTypes.values();
3939
}
4040

41-
/**
42-
* @inheritdoc IGlobalPerpsMarketModule
43-
*/
44-
function setSynthDeductionPriority(
45-
uint128[] memory newSynthDeductionPriority
46-
) external override {
47-
OwnableStorage.onlyOwner();
48-
GlobalPerpsMarketConfiguration.load().updateSynthDeductionPriority(
49-
newSynthDeductionPriority
50-
);
51-
52-
emit SynthDeductionPrioritySet(newSynthDeductionPriority);
53-
}
54-
55-
/**
56-
* @inheritdoc IGlobalPerpsMarketModule
57-
*/
58-
function getSynthDeductionPriority() external view override returns (uint128[] memory) {
59-
return GlobalPerpsMarketConfiguration.load().synthDeductionPriority;
60-
}
61-
6241
/**
6342
* @inheritdoc IGlobalPerpsMarketModule
6443
*/

markets/perps-market/contracts/modules/LiquidationModule.sol

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents {
9797
seizedMarginValue,
9898
true
9999
);
100+
// clear debt
101+
account.updateAccountDebt(-(account.debt.toInt()));
102+
103+
emit AccountMarginLiquidation(accountId, seizedMarginValue, liquidationReward);
100104
} else {
101105
revert NotEligibleForMarginLiquidation(accountId);
102106
}
@@ -167,9 +171,12 @@ contract LiquidationModule is ILiquidationModule, IMarketEvents {
167171
function canLiquidateMarginOnly(
168172
uint128 accountId
169173
) external view override returns (bool isEligible) {
170-
(isEligible, ) = PerpsAccount.load(accountId).isEligibleForMarginLiquidation(
171-
PerpsPrice.Tolerance.DEFAULT
172-
);
174+
PerpsAccount.Data storage account = PerpsAccount.load(accountId);
175+
if (account.hasOpenPositions()) {
176+
return false;
177+
} else {
178+
(isEligible, ) = account.isEligibleForMarginLiquidation(PerpsPrice.Tolerance.DEFAULT);
179+
}
173180
}
174181

175182
/**

markets/perps-market/contracts/modules/PerpsMarketFactoryModule.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,9 @@ contract PerpsMarketFactoryModule is IPerpsMarketFactoryModule {
129129
);
130130
}
131131

132-
int256 totalDebt = collateralValue.toInt() + totalMarketDebt;
132+
int256 totalDebt = collateralValue.toInt() +
133+
totalMarketDebt -
134+
globalMarket.totalAccountsDebt.toInt();
133135
return MathUtil.max(0, totalDebt).toUint();
134136
}
135137

markets/perps-market/contracts/storage/GlobalPerpsMarket.sol

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {SetUtil} from "@synthetixio/core-contracts/contracts/utils/SetUtil.sol";
77
import {MathUtil} from "../utils/MathUtil.sol";
88
import {GlobalPerpsMarketConfiguration} from "./GlobalPerpsMarketConfiguration.sol";
99
import {SafeCastU256, SafeCastI256, SafeCastU128} from "@synthetixio/core-contracts/contracts/utils/SafeCast.sol";
10-
import {Price} from "@synthetixio/spot-market/contracts/storage/Price.sol";
1110
import {PerpsAccount, SNX_USD_MARKET_ID} from "./PerpsAccount.sol";
1211
import {PerpsMarket} from "./PerpsMarket.sol";
12+
import {PerpsPrice} from "./PerpsPrice.sol";
1313
import {PerpsMarketFactory} from "./PerpsMarketFactory.sol";
1414
import {PerpsCollateralConfiguration} from "./PerpsCollateralConfiguration.sol";
1515

@@ -22,6 +22,7 @@ library GlobalPerpsMarket {
2222
using SafeCastU128 for uint128;
2323
using DecimalMath for uint256;
2424
using SetUtil for SetUtil.UintSet;
25+
using PerpsCollateralConfiguration for PerpsCollateralConfiguration.Data;
2526

2627
bytes32 private constant _SLOT_GLOBAL_PERPS_MARKET =
2728
keccak256(abi.encode("io.synthetix.perps-market.GlobalPerpsMarket"));
@@ -61,6 +62,10 @@ library GlobalPerpsMarket {
6162
mapping(uint128 => uint256) collateralAmounts;
6263
SetUtil.UintSet activeCollateralTypes;
6364
SetUtil.UintSet activeMarkets;
65+
/**
66+
* @dev Total debt that hasn't been paid across all accounts.
67+
*/
68+
uint256 totalAccountsDebt;
6469
}
6570

6671
function load() internal pure returns (Data storage marketData) {
@@ -107,11 +112,14 @@ library GlobalPerpsMarket {
107112
if (collateralId == SNX_USD_MARKET_ID) {
108113
total += self.collateralAmounts[collateralId];
109114
} else {
110-
(uint256 collateralValue, ) = spotMarket.quoteSellExactIn(
111-
collateralId,
112-
self.collateralAmounts[collateralId],
113-
Price.Tolerance.DEFAULT
114-
);
115+
(uint256 collateralValue, ) = PerpsCollateralConfiguration
116+
.load(collateralId)
117+
.valueInUsd(
118+
self.collateralAmounts[collateralId],
119+
spotMarket,
120+
PerpsPrice.Tolerance.DEFAULT,
121+
false
122+
);
115123
total += collateralValue;
116124
}
117125
}
@@ -133,6 +141,11 @@ library GlobalPerpsMarket {
133141
}
134142
}
135143

144+
function updateDebt(Data storage self, int256 debtDelta) internal {
145+
int256 newTotalAccountsDebt = self.totalAccountsDebt.toInt() + debtDelta;
146+
self.totalAccountsDebt = newTotalAccountsDebt < 0 ? 0 : newTotalAccountsDebt.toUint();
147+
}
148+
136149
/**
137150
* @notice Check if the account is set as liquidatable.
138151
*/

markets/perps-market/contracts/storage/GlobalPerpsMarketConfiguration.sol

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ library GlobalPerpsMarketConfiguration {
3939
// solhint-disable-next-line var-name-mixedcase
4040
mapping(uint128 => uint256) __unused_1;
4141
/**
42-
* @dev when deducting from user's margin which is made up of many synths, this priority governs which synth to sell for deduction
42+
* @dev previously synth deduction priority
4343
*/
44-
uint128[] synthDeductionPriority;
44+
// solhint-disable-next-line var-name-mixedcase
45+
uint128[] __unused_2;
4546
/**
4647
* @dev minimum configured keeper reward for the sender who liquidates the account
4748
*/
@@ -153,17 +154,6 @@ library GlobalPerpsMarketConfiguration {
153154
return MathUtil.min(MathUtil.max(minCap, keeperRewards + costOfExecutionInUsd), maxCap);
154155
}
155156

156-
function updateSynthDeductionPriority(
157-
Data storage self,
158-
uint128[] memory newSynthDeductionPriority
159-
) internal {
160-
delete self.synthDeductionPriority;
161-
162-
for (uint256 i = 0; i < newSynthDeductionPriority.length; i++) {
163-
self.synthDeductionPriority.push(newSynthDeductionPriority[i]);
164-
}
165-
}
166-
167157
function collectFees(
168158
Data storage self,
169159
uint256 orderFees,

0 commit comments

Comments
 (0)