Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit number of positions and collaterals per account #1774

Merged
merged 4 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 MaxPerAccountSet(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 setMaxPerAccount(
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 getMaxPerAccount()
external
returns (uint128 maxPositionsPerAccount, uint128 maxCollateralsPerAccount);

/**
* @notice Update the referral share percentage for a referrer
* @param referrer The address of the referrer
Expand Down
6 changes: 6 additions & 0 deletions markets/perps-market/contracts/modules/AsyncOrderModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ contract AsyncOrderModule is IAsyncOrderModule {
);
}

// Check if there's a previous position open on this market, if not check if the account can open a new position
if (PerpsMarket.accountPosition(commitment.marketId, commitment.accountId).size == 0) {
//is a new position, check if not exceeding max positions per account
PerpsAccount.load(commitment.accountId).canOpenNewPosition();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would move the if condition into the validation itself. maybe something like this:

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

and load the position and account in the validation itself. just cleans up the modules a bit more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wish we still had createValid, then this call for validating max positions can just live there.

i have some ideas on this, but can leave it to a separate PR 👍🏽


// Replace previous (or empty) order with the commitment request
order.settlementTime = block.timestamp + strategy.settlementDelay;
order.request = commitment;
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 All @@ -140,6 +141,11 @@ contract AsyncOrderSettlementModule is IAsyncOrderSettlementModule, IMarketEvent
PerpsMarketFactory.Data storage factory = PerpsMarketFactory.load();
PerpsAccount.Data storage perpsAccount = PerpsAccount.load(runtime.accountId);

if (oldPosition.size == 0) {
//is a new position, check if not exceeding max positions per account
perpsAccount.canOpenNewPosition();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think you need this validation in settle. since you can't have more than one order at a time, i don't see a scenario where validation passes commitment then fails here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes... I was wondering how that can happen, and keep it just in case. But, yeah, I'll get rid of it.


// use fill price to calculate realized pnl
(runtime.pnl, , , runtime.accruedFunding, ) = oldPosition.getPnl(runtime.fillPrice);
runtime.pnlUint = MathUtil.abs(runtime.pnl);
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 setMaxPerAccount(
uint128 maxPositionsPerAccount,
uint128 maxCollateralsPerAccount
) external override {
OwnableStorage.onlyOwner();
GlobalPerpsMarketConfiguration.Data storage store = GlobalPerpsMarketConfiguration.load();
store.maxPositionsPerAccount = maxPositionsPerAccount;
store.maxCollateralsPerAccount = maxCollateralsPerAccount;

emit MaxPerAccountSet(maxPositionsPerAccount, maxCollateralsPerAccount);
}

/**
* @inheritdoc IGlobalPerpsMarketModule
*/
function getMaxPerAccount()
external
view
override
returns (uint128 maxPositionsPerAccount, uint128 maxCollateralsPerAccount)
{
GlobalPerpsMarketConfiguration.Data storage store = GlobalPerpsMarketConfiguration.load();
maxPositionsPerAccount = store.maxPositionsPerAccount;
maxCollateralsPerAccount = store.maxCollateralsPerAccount;
}
}
5 changes: 5 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,11 @@ contract PerpsAccountModule is IPerpsAccountModule {
PerpsAccount.Data storage account = PerpsAccount.create(accountId);
uint128 perpsMarketId = perpsMarketFactory.perpsMarketId;

if (account.collateralAmounts[synthMarketId] == 0) {
// if the account has no collateral in this market, we need to check if it can add a new collateral
account.canAddNewCollateral();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, move the if condition into the validation


AsyncOrder.checkPendingOrder(account.id);

if (amountDelta > 0) {
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
22 changes: 22 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,24 @@ library PerpsAccount {
}
}

function canOpenNewPosition(Data storage self) internal view {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would rename this to validateMaxPositions. can implies it will return a boolean.

uint128 maxPositionsPerAccount = GlobalPerpsMarketConfiguration
.load()
.maxPositionsPerAccount;
if (maxPositionsPerAccount <= self.openPositionMarketIds.length()) {
revert MaxPositionsPerAccountReached(maxPositionsPerAccount);
}
}

function canAddNewCollateral(Data storage self) internal view {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, maybe validateMaxCollaterals

uint128 maxCollateralsPerAccount = GlobalPerpsMarketConfiguration
.load()
.maxCollateralsPerAccount;
if (maxCollateralsPerAccount <= self.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()).setMaxPerAccount(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()).setMaxPerAccount(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()).setMaxPerAccount(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
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { bn, bootstrapMarkets } from '../bootstrap';
import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert';
import { BigNumber, ethers } from 'ethers';
import { snapshotCheckpoint } from '@synthetixio/core-utils/utils/mocha/snapshot';

describe('Markets - Max Collaterals per account', () => {
const traderAccountIds = [2];
const _MARKET_PRICE = bn(100);
const _UNLIMMITED = bn(100);
const { systems, synthMarkets, provider, trader1, owner } = bootstrapMarkets({
synthMarkets: [
{
name: 'Collateral1',
token: 'snxCL1',
buyPrice: _MARKET_PRICE,
sellPrice: _MARKET_PRICE,
},
{
name: 'Collateral2',
token: 'snxCL2',
buyPrice: _MARKET_PRICE,
sellPrice: _MARKET_PRICE,
},
],
perpsMarkets: [
{
requestedMarketId: bn(25),
name: 'Market1',
token: 'snxMK1',
price: _MARKET_PRICE,
lockedOiRatioD18: bn(0.01),
},
],
traderAccountIds,
});

let market1Id: BigNumber, market2Id: BigNumber;
before('identify actors', async () => {
market1Id = synthMarkets()[0].marketId();
market2Id = synthMarkets()[1].marketId();
});

before('ensure account has enough balance of synths and market is approved', async () => {
const usdAmount = _MARKET_PRICE.mul(100);
const minAmountReceived = bn(100);
const referrer = ethers.constants.AddressZero;
await systems()
.SpotMarket.connect(trader1())
.buy(market1Id, usdAmount, minAmountReceived, referrer);
await synthMarkets()[0]
.synth()
.connect(trader1())
.approve(systems().PerpsMarket.address, _UNLIMMITED);

await systems()
.SpotMarket.connect(trader1())
.buy(market2Id, usdAmount, minAmountReceived, referrer);
await synthMarkets()[1]
.synth()
.connect(trader1())
.approve(systems().PerpsMarket.address, _UNLIMMITED);
});

before('ensure max collaterals is set to 0', async () => {
await systems().PerpsMarket.connect(owner()).setMaxPerAccount(_UNLIMMITED, 0);
});

const restore = snapshotCheckpoint(provider);

it('Collaterals: reverts if attempting to add collateral and is set to zero', async () => {
await assertRevert(
systems().PerpsMarket.connect(trader1()).modifyCollateral(2, 0, bn(10)),
'MaxCollateralsPerAccountReached("0")'
);
});

describe('Collaterals: when max collaterals per account is 1', () => {
before(restore);

before('set max collaterals per account', async () => {
await systems().PerpsMarket.connect(owner()).setMaxPerAccount(_UNLIMMITED, 1);
});

it('should be able to add collateral', async () => {
await systems().PerpsMarket.connect(trader1()).modifyCollateral(2, 0, bn(10));
});

it('should revert when attempting to add a 2nd collateral', async () => {
await assertRevert(
systems().PerpsMarket.connect(trader1()).modifyCollateral(2, market1Id, bn(10)),
'MaxCollateralsPerAccountReached("1")'
);
});

it('can increase and decrease margin size for first collateral', async () => {
await systems().PerpsMarket.connect(trader1()).modifyCollateral(2, 0, bn(+1));
await systems().PerpsMarket.connect(trader1()).modifyCollateral(2, 0, bn(-1));
});
});

describe('Collaterals: when max collaterals per account is unlimmited', () => {
before(restore);

before('set max collaterals per account', async () => {
await systems().PerpsMarket.connect(owner()).setMaxPerAccount(_UNLIMMITED, _UNLIMMITED);
});

it('should be able to add more than one collateral type (two)', async () => {
await systems().PerpsMarket.connect(trader1()).modifyCollateral(2, 0, bn(10));

await systems().PerpsMarket.connect(trader1()).modifyCollateral(2, market1Id, bn(10));
});

describe('when reducing the max collaterals per account', () => {
before('reduce max collaterals per account', async () => {
await systems().PerpsMarket.connect(owner()).setMaxPerAccount(_UNLIMMITED, 2);
});

it('should revert when attempting to add a 3rd collateral', async () => {
await assertRevert(
systems().PerpsMarket.connect(trader1()).modifyCollateral(2, market2Id, bn(10)),
'MaxCollateralsPerAccountReached("2")'
);
});

it('should allow a new collateral if another is depleted', async () => {
await systems().PerpsMarket.connect(trader1()).modifyCollateral(2, 0, bn(-10));
await systems().PerpsMarket.connect(trader1()).modifyCollateral(2, market2Id, bn(10));
});
});
});
});
Loading
Loading