Skip to content

Commit d15c9c7

Browse files
committed
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 20108e9 commit d15c9c7

27 files changed

+687
-533
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 & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ contract AsyncOrderSettlementPythModule is
7979
.validateRequest(settlementStrategy, price);
8080
asyncOrder.validateAcceptablePrice(runtime.fillPrice);
8181

82-
runtime.amountToDeduct = runtime.totalFees;
8382
runtime.sizeDelta = asyncOrder.request.sizeDelta;
8483

8584
PerpsMarketFactory.Data storage factory = PerpsMarketFactory.load();
@@ -89,7 +88,10 @@ contract AsyncOrderSettlementPythModule is
8988
(runtime.pnl, , runtime.chargedInterest, runtime.accruedFunding, , ) = oldPosition.getPnl(
9089
runtime.fillPrice
9190
);
92-
perpsAccount.applyPnl(runtime.pnl);
91+
92+
runtime.chargedAmount = runtime.pnl - runtime.totalFees.toInt();
93+
perpsAccount.charge(runtime.chargedAmount);
94+
emit AccountCharged(runtime.accountId, runtime.chargedAmount, perpsAccount.debt);
9395

9496
// after pnl is realized, update position
9597
runtime.updateData = PerpsMarket.loadValid(runtime.marketId).updatePositionData(
@@ -109,29 +111,7 @@ contract AsyncOrderSettlementPythModule is
109111
runtime.updateData.interestRate
110112
);
111113

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

136116
if (runtime.settlementReward > 0) {
137117
// pay keeper

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

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

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

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
@@ -130,7 +130,9 @@ contract PerpsMarketFactoryModule is IPerpsMarketFactoryModule {
130130
);
131131
}
132132

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

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ 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";
1312
import {PerpsPrice} from "./PerpsPrice.sol";
@@ -23,6 +22,7 @@ library GlobalPerpsMarket {
2322
using SafeCastU128 for uint128;
2423
using DecimalMath for uint256;
2524
using SetUtil for SetUtil.UintSet;
25+
using PerpsCollateralConfiguration for PerpsCollateralConfiguration.Data;
2626

2727
bytes32 private constant _SLOT_GLOBAL_PERPS_MARKET =
2828
keccak256(abi.encode("io.synthetix.perps-market.GlobalPerpsMarket"));
@@ -67,6 +67,10 @@ library GlobalPerpsMarket {
6767
mapping(uint128 => uint256) collateralAmounts;
6868
SetUtil.UintSet activeCollateralTypes;
6969
SetUtil.UintSet activeMarkets;
70+
/**
71+
* @dev Total debt that hasn't been paid across all accounts.
72+
*/
73+
uint256 totalAccountsDebt;
7074
}
7175

7276
function load() internal pure returns (Data storage marketData) {
@@ -131,11 +135,14 @@ library GlobalPerpsMarket {
131135
if (collateralId == SNX_USD_MARKET_ID) {
132136
total += self.collateralAmounts[collateralId];
133137
} else {
134-
(uint256 collateralValue, ) = spotMarket.quoteSellExactIn(
135-
collateralId,
136-
self.collateralAmounts[collateralId],
137-
Price.Tolerance.DEFAULT
138-
);
138+
(uint256 collateralValue, ) = PerpsCollateralConfiguration
139+
.load(collateralId)
140+
.valueInUsd(
141+
self.collateralAmounts[collateralId],
142+
spotMarket,
143+
PerpsPrice.Tolerance.DEFAULT,
144+
false
145+
);
139146
total += collateralValue;
140147
}
141148
}
@@ -157,6 +164,11 @@ library GlobalPerpsMarket {
157164
}
158165
}
159166

167+
function updateDebt(Data storage self, int256 debtDelta) internal {
168+
int256 newTotalAccountsDebt = self.totalAccountsDebt.toInt() + debtDelta;
169+
self.totalAccountsDebt = newTotalAccountsDebt < 0 ? 0 : newTotalAccountsDebt.toUint();
170+
}
171+
160172
/**
161173
* @notice Check if the account is set as liquidatable.
162174
*/

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)