Skip to content

Commit e749052

Browse files
authored
PerpV2 module fixes and optimizations (#192)
* Add maxPerpPositionsPerSet to perpV2 module * Fix failing tests * Add functionality to completely exit a position using trade() This implementation also prevents the user from entering into a state where the module treats the base token position as closed while perp still considers it to be open because the module didn't close the entire position and a small non-negligible dust amount exists in perp protocol * Fix bug and add gas optimization * Add tests for reversing an open position * Improve maxPositionsPerSet javadoc * Bring back _createAndValidateActionInfo and move close position logic to it * Add test for close position completely * Rebase to master & add comment in PreciseUnitMath
1 parent d39e86c commit e749052

File tree

7 files changed

+229
-37
lines changed

7 files changed

+229
-37
lines changed

contracts/lib/PreciseUnitMath.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ library PreciseUnitMath {
149149
int256 c = a.div(b);
150150

151151
if (a % b != 0) {
152+
// a ^ b == 0 case is covered by the previous if statement, hence it won't resolve to --c
152153
(a ^ b > 0) ? ++c : --c;
153154
}
154155

contracts/protocol/modules/PerpV2LeverageModule.sol

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
183183
// PerpV2 contract which provides a getter for baseToken UniswapV3 pools
184184
IMarketRegistry public immutable perpMarketRegistry;
185185

186+
// PerpV2 operations are very gas intensive and there is a limit on the number of positions that can be opened in a single transaction
187+
// during issuance/redemption. `maxPerpPositionsPerSet` is a safe limit set by governance taking Optimism's block gas limit into account.
188+
uint256 public maxPerpPositionsPerSet;
189+
186190
// Mapping of SetTokens to an array of virtual token addresses the Set has open positions for.
187191
// Array is updated when new positions are opened or old positions are zeroed out.
188192
mapping(ISetToken => address[]) internal positions;
@@ -202,7 +206,8 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
202206
IController _controller,
203207
IVault _perpVault,
204208
IQuoter _perpQuoter,
205-
IMarketRegistry _perpMarketRegistry
209+
IMarketRegistry _perpMarketRegistry,
210+
uint256 _maxPerpPositionsPerSet
206211
)
207212
public
208213
ModuleBase(_controller)
@@ -219,6 +224,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
219224
perpVault = _perpVault;
220225
perpQuoter = _perpQuoter;
221226
perpMarketRegistry = _perpMarketRegistry;
227+
maxPerpPositionsPerSet = _maxPerpPositionsPerSet;
222228
}
223229

224230
/* ============ External Functions ============ */
@@ -268,6 +274,8 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
268274
* NOTE: This method doesn't update the externalPositionUnit because it is a function of UniswapV3 virtual
269275
* token market prices and needs to be generated on the fly to be meaningful.
270276
*
277+
* In the tables below, basePositionUnit = baseTokenBalance / setTotalSupply.
278+
*
271279
* As a user when levering, e.g increasing the magnitude of your position, you'd trade as below
272280
* | ----------------------------------------------------------------------------------------------- |
273281
* | Type | Action | Goal | `quoteBoundQuantity` | `baseQuantityUnits` |
@@ -276,13 +284,29 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
276284
* | Short | Sell | get most amt. of vQuote | lower bound of output quote | negative |
277285
* | ----------------------------------------------------------------------------------------------- |
278286
*
279-
* As a user when delevering, e.g decreasing the magnitude of your position, you'd trade as below
280-
* | ----------------------------------------------------------------------------------------------- |
281-
* | Type | Action | Goal | `quoteBoundQuantity` | `baseQuantityUnits` |
282-
* | ----- |-------- | ------------------------- | --------------------------- | ------------------- |
283-
* | Long | Sell | get most amt. of vQuote | upper bound of input quote | negative |
284-
* | Short | Buy | pay least amt. of vQuote | lower bound of output quote | positive |
285-
* | ----------------------------------------------------------------------------------------------- |
287+
* As a user when delevering by partially closing your position, you'd trade as below
288+
* -----------------------------------------------------------------------------------------------------------------------------------
289+
* | Type | Action | Goal | `quoteBoundQuantity` | `baseQuantityUnits` |
290+
* | ----- |-------- | ------------------------- | --------------------------- | ----------------------------------------------------|
291+
* | Long | Sell | get most amt. of vQuote | upper bound of input quote | negative, |baseQuantityUnits| < |basePositionUnit| |
292+
* | Short | Buy | pay least amt. of vQuote | lower bound of output quote | positive, |baseQuantityUnits| < |basePositionUnit| |
293+
* -----------------------------------------------------------------------------------------------------------------------------------
294+
*
295+
* As a user when completely closing a position, you'd trade as below
296+
* -------------------------------------------------------------------------------------------------------------------------------------------
297+
* | Type | Action | Goal | `quoteBoundQuantity` | `baseQuantityUnits` |
298+
* | ----- |-----------------| ------------------------- | --------------------------- | ----------------------------------------------------|
299+
* | Long | Sell to close | get most amt. of vQuote | upper bound of input quote | negative, baseQuantityUnits = -1 * basePositionUnit |
300+
* | Short | Buy to close | pay least amt. of vQuote | lower bound of output quote | positive, baseQuantityUnits = -1 * basePositionUnit |
301+
* -------------------------------------------------------------------------------------------------------------------------------------------
302+
*
303+
* As a user when reversing a position, e.g going from a long position to a short position in a single trade, you'd trade as below
304+
* -------------------------------------------------------------------------------------------------------------------------------------------
305+
* | Type | Action | Goal | `quoteBoundQuantity` | `baseQuantityUnits` |
306+
* | ----- |-----------------|---------------------------| --------------------------- | ----------------------------------------------------|
307+
* | Long | Sell to reverse | get most amt. of vQuote | upper bound of input quote | negative, |baseQuantityUnits| > |basePositionUnit| |
308+
* | Short | Buy to reverse | pay least amt. of vQuote | lower bound of output quote | positive, |baseQuantityUnits| > |basePositionUnit| |
309+
* -------------------------------------------------------------------------------------------------------------------------------------------
286310
*
287311
* @param _setToken Instance of the SetToken
288312
* @param _baseToken Address virtual token being traded
@@ -537,6 +561,17 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
537561
}
538562
}
539563

564+
/* ============ External Setter Functions ============ */
565+
566+
/**
567+
* @dev GOVERNANCE ONLY: Update max perpetual positions per SetToken. Only callable by governance.
568+
*
569+
* @param _maxPerpPositionsPerSet New max perpetual positons per set
570+
*/
571+
function updateMaxPerpPositionsPerSet(uint256 _maxPerpPositionsPerSet) external onlyOwner {
572+
maxPerpPositionsPerSet = _maxPerpPositionsPerSet;
573+
}
574+
540575
/* ============ External Getter Functions ============ */
541576

542577
/**
@@ -991,35 +1026,43 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
9911026

9921027
/**
9931028
* @dev Construct the ActionInfo struct for trading. This method takes POSITION UNIT amounts and passes to
994-
* _createActionInfoNotional to create the struct. If the _baseTokenQuantity is zero then revert. This
995-
* method is only called from `trade` - the issue/redeem flow uses createActionInfoNotional directly.
1029+
* _createActionInfoNotional to create the struct. If the _baseTokenQuantity is zero then revert. If
1030+
* the _baseTokenQuantity = -(baseBalance/setSupply) then close the position entirely. This method is
1031+
* only called from `trade` - the issue/redeem flow uses createActionInfoNotional directly.
9961032
*
9971033
* @param _setToken Instance of the SetToken
9981034
* @param _baseToken Address of base token being traded into/out of
999-
* @param _baseTokenUnits Quantity of baseToken to trade in PositionUnits
1035+
* @param _baseQuantityUnits Quantity of baseToken to trade in PositionUnits
10001036
* @param _quoteReceiveUnits Quantity of quote to receive if selling base and pay if buying, in PositionUnits
10011037
*
10021038
* @return ActionInfo Instance of constructed ActionInfo struct
10031039
*/
10041040
function _createAndValidateActionInfo(
10051041
ISetToken _setToken,
10061042
address _baseToken,
1007-
int256 _baseTokenUnits,
1043+
int256 _baseQuantityUnits,
10081044
uint256 _quoteReceiveUnits
10091045
)
10101046
internal
10111047
view
10121048
returns(ActionInfo memory)
10131049
{
1014-
require(_baseTokenUnits != 0, "Amount is 0");
1050+
require(_baseQuantityUnits != 0, "Amount is 0");
10151051
require(perpMarketRegistry.hasPool(_baseToken), "Base token does not exist");
10161052

10171053
uint256 totalSupply = _setToken.totalSupply();
10181054

1055+
int256 baseBalance = perpAccountBalance.getBase(address(_setToken), _baseToken);
1056+
int256 basePositionUnit = baseBalance.preciseDiv(totalSupply.toInt256());
1057+
1058+
int256 baseNotional = _baseQuantityUnits == basePositionUnit.neg()
1059+
? baseBalance.neg() // To close position completely
1060+
: _baseQuantityUnits.preciseMul(totalSupply.toInt256());
1061+
10191062
return _createActionInfoNotional(
10201063
_setToken,
10211064
_baseToken,
1022-
_baseTokenUnits.preciseMul(totalSupply.toInt256()),
1065+
baseNotional,
10231066
_quoteReceiveUnits.preciseMul(totalSupply)
10241067
);
10251068
}
@@ -1078,6 +1121,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
10781121
positions[_setToken].removeStorage(_baseToken);
10791122
}
10801123
} else {
1124+
require(positions[_setToken].length < maxPerpPositionsPerSet, "Exceeds max perpetual positions per set");
10811125
positions[_setToken].push(_baseToken);
10821126
}
10831127
}

test/integration/perpV2LeverageSlippageIssuance.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ describe("PerpV2LeverageSlippageIssuance", () => {
9393
perpSetup.vault.address,
9494
perpSetup.quoter.address,
9595
perpSetup.marketRegistry.address,
96+
BigNumber.from(3),
9697
"contracts/protocol/integration/lib/PerpV2.sol:PerpV2",
9798
perpLib.address,
9899
);

test/protocol-viewers/perpV2LeverageModuleViewer.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ describe("PerpV2LeverageModuleViewer", () => {
106106
perpSetup.vault.address,
107107
perpSetup.quoter.address,
108108
perpSetup.marketRegistry.address,
109+
BigNumber.from(3),
109110
"contracts/protocol/integration/lib/PerpV2.sol:PerpV2",
110111
perpLib.address,
111112
);

0 commit comments

Comments
 (0)