Skip to content

Commit

Permalink
Limit number of positions and collaterals per account (#1774)
Browse files Browse the repository at this point in the history
* Limit number of positions and collaterals per account

* pr comments

* introduce updateValid (kind of createValid)

* Rename to xPerAccountCaps and not passing self to validators
  • Loading branch information
leomassazza authored Aug 16, 2023
1 parent 5e6e736 commit 1c5ab0c
Show file tree
Hide file tree
Showing 12 changed files with 503 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ interface IGlobalPerpsMarketModule {
*/
event ReferrerShareUpdated(address referrer, uint256 shareRatioD18);

/**
* @notice Gets fired when the max number of Positions and Collaterals per Account are set by owner.
* @param maxPositionsPerAccount The max number of concurrent Positions per Account
* @param maxCollateralsPerAccount The max number of concurrent Collaterals per Account
*/
event PerAccountCapsSet(uint128 maxPositionsPerAccount, uint128 maxCollateralsPerAccount);

/**
* @notice Thrown when the fee collector does not implement the IFeeCollector interface
*/
Expand Down Expand Up @@ -118,6 +125,25 @@ interface IGlobalPerpsMarketModule {
*/
function getFeeCollector() external view returns (address feeCollector);

/**
* @notice Set or update the max number of Positions and Collaterals per Account
* @param maxPositionsPerAccount The max number of concurrent Positions per Account
* @param maxCollateralsPerAccount The max number of concurrent Collaterals per Account
*/
function setPerAccountCaps(
uint128 maxPositionsPerAccount,
uint128 maxCollateralsPerAccount
) external;

/**
* @notice get the max number of Positions and Collaterals per Account
* @param maxPositionsPerAccount The max number of concurrent Positions per Account
* @param maxCollateralsPerAccount The max number of concurrent Collaterals per Account
*/
function getPerAccountCaps()
external
returns (uint128 maxPositionsPerAccount, uint128 maxCollateralsPerAccount);

/**
* @notice Update the referral share percentage for a referrer
* @param referrer The address of the referrer
Expand Down
7 changes: 2 additions & 5 deletions markets/perps-market/contracts/modules/AsyncOrderModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ contract AsyncOrderModule is IAsyncOrderModule {
SettlementStrategy.Data storage strategy = PerpsMarketConfiguration
.loadValidSettlementStrategy(commitment.marketId, commitment.settlementStrategyId);

// pull order from storage and check if it is valid (not pending unexpired order)
AsyncOrder.Data storage order = AsyncOrder.checkPendingOrder(commitment.accountId);
AsyncOrder.Data storage order = AsyncOrder.load(commitment.accountId);

// if order (previous) sizeDelta is not zero and didn't revert while checking, it means the previous order expired
if (order.request.sizeDelta != 0) {
Expand All @@ -71,9 +70,7 @@ contract AsyncOrderModule is IAsyncOrderModule {
);
}

// Replace previous (or empty) order with the commitment request
order.settlementTime = block.timestamp + strategy.settlementDelay;
order.request = commitment;
order.updateValid(commitment, strategy);

(, uint feesAccrued, , ) = order.validateRequest(
strategy,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ contract AsyncOrderSettlementModule is IAsyncOrderSettlementModule, IMarketEvent
runtime.marketId = asyncOrder.request.marketId;
// check if account is flagged
GlobalPerpsMarket.load().checkLiquidation(runtime.accountId);

Position.Data storage oldPosition;
(runtime.newPosition, runtime.totalFees, runtime.fillPrice, oldPosition) = asyncOrder
.validateRequest(settlementStrategy, price);
Expand Down
29 changes: 29 additions & 0 deletions markets/perps-market/contracts/modules/GlobalPerpsMarketModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,33 @@ contract GlobalPerpsMarketModule is IGlobalPerpsMarketModule {
function getMarkets() external view override returns (uint256[] memory marketIds) {
marketIds = GlobalPerpsMarket.load().activeMarkets.values();
}

/**
* @inheritdoc IGlobalPerpsMarketModule
*/
function setPerAccountCaps(
uint128 maxPositionsPerAccount,
uint128 maxCollateralsPerAccount
) external override {
OwnableStorage.onlyOwner();
GlobalPerpsMarketConfiguration.Data storage store = GlobalPerpsMarketConfiguration.load();
store.maxPositionsPerAccount = maxPositionsPerAccount;
store.maxCollateralsPerAccount = maxCollateralsPerAccount;

emit PerAccountCapsSet(maxPositionsPerAccount, maxCollateralsPerAccount);
}

/**
* @inheritdoc IGlobalPerpsMarketModule
*/
function getPerAccountCaps()
external
view
override
returns (uint128 maxPositionsPerAccount, uint128 maxCollateralsPerAccount)
{
GlobalPerpsMarketConfiguration.Data storage store = GlobalPerpsMarketConfiguration.load();
maxPositionsPerAccount = store.maxPositionsPerAccount;
maxCollateralsPerAccount = store.maxCollateralsPerAccount;
}
}
2 changes: 2 additions & 0 deletions markets/perps-market/contracts/modules/PerpsAccountModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ contract PerpsAccountModule is IPerpsAccountModule {
PerpsAccount.Data storage account = PerpsAccount.create(accountId);
uint128 perpsMarketId = perpsMarketFactory.perpsMarketId;

PerpsAccount.validateMaxCollaterals(accountId, synthMarketId);

AsyncOrder.checkPendingOrder(account.id);

if (amountDelta > 0) {
Expand Down
19 changes: 19 additions & 0 deletions markets/perps-market/contracts/storage/AsyncOrder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,25 @@ library AsyncOrder {
checkWithinSettlementWindow(order, strategy);
}

/**
* @dev Updates the order with the new commitment request data and settlement time.
* @dev Reverts if there's a pending order.
* @dev Reverts if accont cannot open a new position (due to max allowed reached).
*/
function updateValid(
Data storage self,
OrderCommitmentRequest memory newRequest,
SettlementStrategy.Data storage strategy
) internal {
checkPendingOrder(newRequest.accountId);

PerpsAccount.validateMaxPositions(newRequest.accountId, newRequest.marketId);

// Replace previous (or empty) order with the commitment request
self.settlementTime = block.timestamp + strategy.settlementDelay;
self.request = newRequest;
}

/**
* @dev Reverts if there is a pending order.
* @dev A pending order is one that has a sizeDelta and isn't expired yet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ library GlobalPerpsMarketConfiguration {
* @dev maximum configured liquidation reward for the sender who liquidates the account
*/
uint maxLiquidationRewardUsd;
/**
* @dev maximum configured number of concurrent positions per account.
* @notice If set to zero it means no new positions can be opened, but existing positions can be increased or decreased.
* @notice If set to a larger number (larger than number of markets created) it means is unlimited.
*/
uint128 maxPositionsPerAccount;
/**
* @dev maximum configured number of concurrent collaterals per account.
* @notice If set to zero it means no new collaterals can be added accounts, but existing collaterals can be increased or decreased.
* @notice If set to a larger number (larger than number of collaterals enabled) it means is unlimited.
*/
uint128 maxCollateralsPerAccount;
}

function load() internal pure returns (Data storage globalMarketConfig) {
Expand Down
28 changes: 28 additions & 0 deletions markets/perps-market/contracts/storage/PerpsAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ library PerpsAccount {

error AccountLiquidatable(uint128 accountId);

error MaxPositionsPerAccountReached(uint128 maxPositionsPerAccount);

error MaxCollateralsPerAccountReached(uint128 maxCollateralsPerAccount);

function load(uint128 id) internal pure returns (Data storage account) {
bytes32 s = keccak256(abi.encode("io.synthetix.perps-market.Account", id));

Expand All @@ -69,6 +73,30 @@ library PerpsAccount {
}
}

function validateMaxPositions(uint128 accountId, uint128 marketId) internal view {
if (PerpsMarket.accountPosition(marketId, accountId).size == 0) {
uint128 maxPositionsPerAccount = GlobalPerpsMarketConfiguration
.load()
.maxPositionsPerAccount;
if (maxPositionsPerAccount <= load(accountId).openPositionMarketIds.length()) {
revert MaxPositionsPerAccountReached(maxPositionsPerAccount);
}
}
}

function validateMaxCollaterals(uint128 accountId, uint128 synthMarketId) internal view {
Data storage account = load(accountId);

if (account.collateralAmounts[synthMarketId] == 0) {
uint128 maxCollateralsPerAccount = GlobalPerpsMarketConfiguration
.load()
.maxCollateralsPerAccount;
if (maxCollateralsPerAccount <= account.activeCollateralTypes.length()) {
revert MaxCollateralsPerAccountReached(maxCollateralsPerAccount);
}
}
}

function isEligibleForLiquidation(
Data storage self
)
Expand Down
66 changes: 53 additions & 13 deletions markets/perps-market/test/integration/Market/CreateMarket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,22 +139,62 @@ describe('Create Market test', () => {
.setFundingParameters(marketId, bn(100_000), bn(0));
});

before('add collateral', async () => {
await systems().PerpsMarket.connect(trader1()).modifyCollateral(2, 0, bn(10_000));
before('ensure per account max is set to zero', async () => {
await systems().PerpsMarket.connect(owner()).setPerAccountCaps(0, 0);
});

it('should be able to use the market', async () => {
await systems()
.PerpsMarket.connect(trader1())
.commitOrder({
marketId: marketId,
accountId: 2,
sizeDelta: bn(1),
settlementStrategyId: 0,
acceptablePrice: bn(1050), // 5% slippage
referrer: ethers.constants.AddressZero,
trackingCode: ethers.constants.HashZero,
it('reverts when trying add collateral if max collaterals per account is zero', async () => {
await assertRevert(
systems().PerpsMarket.connect(trader1()).modifyCollateral(2, 0, bn(10_000)),
'MaxCollateralsPerAccountReached("0")'
);
});

describe('when max collaterals per account is set to non-zero', () => {
before('set max collaterals per account', async () => {
await systems().PerpsMarket.connect(owner()).setPerAccountCaps(0, 1000);
});

before('add collateral', async () => {
await systems().PerpsMarket.connect(trader1()).modifyCollateral(2, 0, bn(10_000));
});

it('reverts when trying to add position if max positions per account is zero', async () => {
await assertRevert(
systems()
.PerpsMarket.connect(trader1())
.commitOrder({
marketId: marketId,
accountId: 2,
sizeDelta: bn(1),
settlementStrategyId: 0,
acceptablePrice: bn(1050), // 5% slippage
referrer: ethers.constants.AddressZero,

trackingCode: ethers.constants.HashZero,
}),
'MaxPositionsPerAccountReached("0")'
);
});

describe('when max positions per account is set to non-zero', () => {
before('set max positions per account', async () => {
await systems().PerpsMarket.connect(owner()).setPerAccountCaps(1000, 1000);
});
it('should be able to use the market', async () => {
await systems()
.PerpsMarket.connect(trader1())
.commitOrder({
marketId: marketId,
accountId: 2,
sizeDelta: bn(1),
settlementStrategyId: 0,
acceptablePrice: bn(1050), // 5% slippage
referrer: ethers.constants.AddressZero,
trackingCode: ethers.constants.HashZero,
});
});
});
});
});
});
Expand Down
Loading

0 comments on commit 1c5ab0c

Please sign in to comment.