Skip to content

Commit d7d37da

Browse files
Sachincgewecke
andauthored
fix(PerpV2BasisTradingModule): External audit fixes for low-risk findings and Internal audit fixes (#232)
* Design Improvement: Cache storage slot values * Design Improvements: Spelling and grammar * Fix typo * Fix revert statement * Design improvement: Return early if no funding will be withdrawn * Fix coverage: Add test case for when notionalFunding is zero * Fix failing test * Modify calculateExternalPositionUnit logic used by deposit and withdraw; Reduce bytecodesize * Override withdraw function to track settled funding * Fix deposit; Add check to ensure deposit unit is <= current unit; Add tests * Refactor update external position unit logic; Add tests * Fix tests; And skip 2 getRedemptionAdjustments tests * Add internal audit fixes and suggestions * Fix division by zero error * fix(tests): Small fixes for BasisTradingModule audit changes (#243) * Add fix for _updateExternalPositionUnit * Add test for negative balance check in _calculateNetFeesPositionUnit * Fix externalPositionUnit test in perpV2LeverageV2SlippageIssuance tests * Use `tradeAndTrackFunding` everywhere in BasisTrading integration tests * Remove .skip; Fix javadocs Co-authored-by: cgewecke <[email protected]>
1 parent 509eddf commit d7d37da

File tree

11 files changed

+526
-194
lines changed

11 files changed

+526
-194
lines changed

contracts/interfaces/external/perp-v2/IAccountBalance.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ interface IAccountBalance {
2828
function getBase(address trader, address baseToken) external view returns (int256);
2929
function getQuote(address trader, address baseToken) external view returns (int256);
3030
function getNetQuoteBalanceAndPendingFee(address trader) external view returns (int256, uint256);
31-
function getPositionSize(address trader, address baseToken) external view returns (int256);
32-
function getPositionValue(address trader, address baseToken) external view returns (int256);
31+
function getTotalPositionSize(address trader, address baseToken) external view returns (int256);
32+
function getTotalPositionValue(address trader, address baseToken) external view returns (int256);
3333
function getTotalAbsPositionValue(address trader) external view returns (uint256);
3434
function getClearingHouseConfig() external view returns (address);
3535
function getExchange() external view returns (address);

contracts/protocol/lib/PositionV2.sol

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { PreciseUnitMath } from "../../lib/PreciseUnitMath.sol";
3737
* CHANGELOG:
3838
* - `Position` library has all internal functions which are inlined to the module contract during compilation.
3939
* Inlining functions increases bytecode size of the module contract. This library contains the same functions
40-
* as `Position` library but all the functions have public/external access modifier. Thus, making this version
40+
* as `Position` library but all the functions have public/external access modifier. Thus, making this version
4141
* linkable which helps in reducing bytecode size of the module contract.
4242
*/
4343
library PositionV2 {
@@ -62,7 +62,7 @@ library PositionV2 {
6262
function hasExternalPosition(ISetToken _setToken, address _component) public view returns(bool) {
6363
return _setToken.getExternalPositionModules(_component).length > 0;
6464
}
65-
65+
6666
/**
6767
* Returns whether the SetToken component default position real unit is greater than or equal to units passed in.
6868
*/
@@ -71,7 +71,7 @@ library PositionV2 {
7171
}
7272

7373
/**
74-
* Returns whether the SetToken component external position is greater than or equal to the real units passed in.
74+
* Returns whether the SetToken component's external position is greater than or equal to the real units passed in.
7575
*/
7676
function hasSufficientExternalUnits(
7777
ISetToken _setToken,
@@ -83,12 +83,12 @@ library PositionV2 {
8383
view
8484
returns(bool)
8585
{
86-
return _setToken.getExternalPositionRealUnit(_component, _positionModule) >= _unit.toInt256();
86+
return _setToken.getExternalPositionRealUnit(_component, _positionModule) >= _unit.toInt256();
8787
}
8888

8989
/**
9090
* If the position does not exist, create a new Position and add to the SetToken. If it already exists,
91-
* then set the position units. If the new units is 0, remove the position. Handles adding/removing of
91+
* then set the position units. If the new units is 0, remove the position. Handles adding/removing of
9292
* components where needed (in light of potential external positions).
9393
*
9494
* @param _setToken Address of SetToken being modified
@@ -114,7 +114,7 @@ library PositionV2 {
114114

115115
/**
116116
* Update an external position and remove and external positions or components if necessary. The logic flows as follows:
117-
* 1) If component is not already added then add component and external position.
117+
* 1) If component is not already added then add component and external position.
118118
* 2) If component is added but no existing external position using the passed module exists then add the external position.
119119
* 3) If the existing position is being added to then just update the unit and data
120120
* 4) If the position is being closed and no other external positions or default positions are associated with the component
@@ -191,7 +191,7 @@ library PositionV2 {
191191
* @return Notional tracked balance
192192
*/
193193
function getDefaultTrackedBalance(ISetToken _setToken, address _component) external view returns(uint256) {
194-
int256 positionUnit = _setToken.getDefaultPositionRealUnit(_component);
194+
int256 positionUnit = _setToken.getDefaultPositionRealUnit(_component);
195195
return _setToken.totalSupply().preciseMul(positionUnit.toUint256());
196196
}
197197

contracts/protocol/modules/v2/PerpV2BasisTradingModule.sol

Lines changed: 109 additions & 49 deletions
Large diffs are not rendered by default.

contracts/protocol/modules/v2/PerpV2LeverageModuleV2.sol

Lines changed: 32 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { PreciseUnitMath } from "../../../lib/PreciseUnitMath.sol";
4747
import { AddressArrayUtils } from "../../../lib/AddressArrayUtils.sol";
4848
import { UnitConversionUtils } from "../../../lib/UnitConversionUtils.sol";
4949

50+
5051
/**
5152
* @title PerpV2LeverageModuleV2
5253
* @author Set Protocol
@@ -60,8 +61,8 @@ import { UnitConversionUtils } from "../../../lib/UnitConversionUtils.sol";
6061
* Any pending funding costs or PnL is carried by the current token holders. To be used safely this module MUST issue using the
6162
* SlippageIssuanceModule or else issue and redeem transaction could be sandwich attacked.
6263
*
63-
* NOTE: The external position unit is only updated on an as-needed basis during issuance/redemption. It does not reflect the current
64-
* value of the Set's perpetual position. The current value can be calculated from getPositionNotionalInfo.
64+
* NOTE: The external position unit is only updated on an as-needed basis during issuance, redemption, deposit and withdraw. It does not
65+
* reflect the current value of the Set's perpetual position. The current value can be calculated from getPositionNotionalInfo.
6566
*
6667
* CHANGELOG:
6768
* - This contract has the same functionality as `PerpV2LeverageModule` but smaller bytecode size. It extends ModuleBaseV2 (which uses
@@ -352,6 +353,10 @@ contract PerpV2LeverageModuleV2 is ModuleBaseV2, ReentrancyGuard, Ownable, SetTo
352353
onlyManagerAndValidSet(_setToken)
353354
{
354355
require(_collateralQuantityUnits > 0, "Deposit amount is 0");
356+
require(
357+
_collateralQuantityUnits <= _setToken.getDefaultPositionRealUnit(address(collateralToken)).toUint256(),
358+
"Amount too high"
359+
);
355360

356361
uint256 notionalDepositedQuantity = _depositAndUpdatePositions(_setToken, _collateralQuantityUnits);
357362

@@ -374,6 +379,7 @@ contract PerpV2LeverageModuleV2 is ModuleBaseV2, ReentrancyGuard, Ownable, SetTo
374379
uint256 _collateralQuantityUnits
375380
)
376381
public
382+
virtual
377383
nonReentrant
378384
onlyManagerAndValidSet(_setToken)
379385
{
@@ -385,7 +391,7 @@ contract PerpV2LeverageModuleV2 is ModuleBaseV2, ReentrancyGuard, Ownable, SetTo
385391
}
386392

387393
/**
388-
* @dev MANAGER ONLY: Removes this module from the SetToken, via call by the SetToken. Deletes
394+
* @dev SETTOKEN ONLY: Removes this module from the SetToken, via call by the SetToken. Deletes
389395
* position mappings associated with SetToken.
390396
*
391397
* NOTE: Function will revert if there is greater than a position unit amount of USDC of account value.
@@ -555,7 +561,7 @@ contract PerpV2LeverageModuleV2 is ModuleBaseV2, ReentrancyGuard, Ownable, SetTo
555561
/**
556562
* @dev GOVERNANCE ONLY: Update max perpetual positions per SetToken. Only callable by governance.
557563
*
558-
* @param _maxPerpPositionsPerSet New max perpetual positons per set
564+
* @param _maxPerpPositionsPerSet New max perpetual positions per set
559565
*/
560566
function updateMaxPerpPositionsPerSet(uint256 _maxPerpPositionsPerSet) external onlyOwner {
561567
maxPerpPositionsPerSet = _maxPerpPositionsPerSet;
@@ -830,12 +836,7 @@ contract PerpV2LeverageModuleV2 is ModuleBaseV2, ReentrancyGuard, Ownable, SetTo
830836
initialCollateralPositionBalance
831837
);
832838

833-
_setToken.editExternalPosition(
834-
address(collateralToken),
835-
address(this),
836-
_calculateExternalPositionUnit(_setToken),
837-
""
838-
);
839+
_updateExternalPositionUnit(_setToken);
839840

840841
return collateralNotionalQuantity;
841842
}
@@ -885,12 +886,7 @@ contract PerpV2LeverageModuleV2 is ModuleBaseV2, ReentrancyGuard, Ownable, SetTo
885886
initialCollateralPositionBalance
886887
);
887888

888-
_setToken.editExternalPosition(
889-
address(collateralToken),
890-
address(this),
891-
_calculateExternalPositionUnit(_setToken),
892-
""
893-
);
889+
_updateExternalPositionUnit(_setToken);
894890

895891
return collateralNotionalQuantity;
896892
}
@@ -1058,44 +1054,32 @@ contract PerpV2LeverageModuleV2 is ModuleBaseV2, ReentrancyGuard, Ownable, SetTo
10581054
}
10591055

10601056
/**
1061-
* @dev Gets the mid-point price of a virtual asset from UniswapV3 markets maintained by Perp Protocol
1057+
* @dev Sets the external position unit based on PerpV2 Vault collateral balance. If there is >= 1
1058+
* position unit of USDC of collateral value, add the component and/or the external position to the
1059+
* SetToken. Else, untracks the component and/or removes external position from the SetToken. Refer
1060+
* to PositionV2#editExternalPosition for detailed flow.
10621061
*
1063-
* @param _baseToken Address of virtual token to price
1064-
* @return price Mid-point price of virtual token in UniswapV3 AMM market
1065-
*/
1066-
function _calculateAMMSpotPrice(address _baseToken) internal view returns (uint256 price) {
1067-
address pool = perpMarketRegistry.getPool(_baseToken);
1068-
(uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(pool).slot0();
1069-
uint256 priceX96 = sqrtPriceX96.formatSqrtPriceX96ToPriceX96();
1070-
return priceX96.formatX96ToX10_18();
1071-
}
1072-
1073-
/**
1074-
* @dev Calculates the sum of collateralToken denominated market-prices of assets and debt for the Perp account per
1075-
* SetToken
1062+
* NOTE: Setting external position unit to the collateral balance is acceptable because, as noted above, the
1063+
* external position unit is only updated on an as-needed basis during issuance, redemption, deposit and
1064+
* withdraw. It does not reflect the current value of the Set's perpetual position. The true current value can
1065+
* be calculated from getPositionNotionalInfo.
10761066
*
10771067
* @param _setToken Instance of SetToken
1078-
* @return int256 External position unit
10791068
*/
1080-
function _calculateExternalPositionUnit(ISetToken _setToken) internal view returns (int256) {
1081-
PerpV2Positions.PositionNotionalInfo[] memory positionInfo = getPositionNotionalInfo(_setToken);
1082-
uint256 positionLength = positionInfo.length;
1083-
int256 totalPositionValue = 0;
1084-
1085-
for (uint i = 0; i < positionLength; i++ ) {
1086-
int256 spotPrice = _calculateAMMSpotPrice(positionInfo[i].baseToken).toInt256();
1087-
totalPositionValue = totalPositionValue.add(
1088-
positionInfo[i].baseBalance.preciseMul(spotPrice)
1089-
);
1090-
}
1069+
function _updateExternalPositionUnit(ISetToken _setToken) internal {
1070+
int256 collateralValueUnit = perpVault.getBalance(address(_setToken))
1071+
.toPreciseUnitsFromDecimals(collateralDecimals)
1072+
.preciseDiv(_setToken.totalSupply().toInt256())
1073+
.fromPreciseUnitToDecimals(collateralDecimals);
10911074

1092-
int256 externalPositionUnitInPreciseUnits = _calculatePartialAccountValuePositionUnit(_setToken)
1093-
.add(totalPositionValue.preciseDiv(_setToken.totalSupply().toInt256()));
1094-
1095-
return externalPositionUnitInPreciseUnits.fromPreciseUnitToDecimals(collateralDecimals);
1075+
_setToken.editExternalPosition(
1076+
address(collateralToken),
1077+
address(this),
1078+
collateralValueUnit,
1079+
""
1080+
);
10961081
}
10971082

1098-
10991083
/**
11001084
* @dev Returns issuance or redemption adjustments in the format expected by `SlippageIssuanceModule`.
11011085
* The last recorded externalPositionUnit (current) is subtracted from a dynamically generated
@@ -1107,7 +1091,7 @@ contract PerpV2LeverageModuleV2 is ModuleBaseV2, ReentrancyGuard, Ownable, SetTo
11071091
* @param _setToken Instance of the SetToken
11081092
* @param _newExternalPositionUnit Dynamically calculated externalPositionUnit
11091093
* @return int256[] Components-length array with equity adjustment value at appropriate index
1110-
* @return int256[] Components-length array of zeroes (debt adjustements)
1094+
* @return int256[] Components-length array of zeroes (debt adjustments)
11111095
*/
11121096
function _formatAdjustments(
11131097
ISetToken _setToken,

deprecated/PerpV2LeverageModule.sol

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
271271
* token market prices and needs to be generated on the fly to be meaningful.
272272
*
273273
* In the tables below, basePositionUnit = baseTokenBalance / setTotalSupply.
274-
*
274+
*
275275
* As a user when levering, e.g increasing the magnitude of your position, you'd trade as below
276276
* | ----------------------------------------------------------------------------------------------- |
277277
* | Type | Action | Goal | `quoteBoundQuantity` | `baseQuantityUnits` |
@@ -318,7 +318,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
318318
public
319319
nonReentrant
320320
onlyManagerAndValidSet(_setToken)
321-
{
321+
{
322322
ActionInfo memory actionInfo = _createAndValidateActionInfo(
323323
_setToken,
324324
_baseToken,
@@ -413,7 +413,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
413413
// `positions[setToken]` mapping stores an array of addresses. The base token addresses are removed from the array when the
414414
// corresponding base token positions are zeroed out. Since no positions exist when removing the module, the stored array should
415415
// already be empty, and the mapping can be deleted directly.
416-
delete positions[setToken];
416+
delete positions[setToken];
417417

418418
// Try if unregister exists on any of the modules
419419
address[] memory modules = setToken.getModules();
@@ -563,8 +563,8 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
563563

564564
/**
565565
* @dev GOVERNANCE ONLY: Update max perpetual positions per SetToken. Only callable by governance.
566-
*
567-
* @param _maxPerpPositionsPerSet New max perpetual positons per set
566+
*
567+
* @param _maxPerpPositionsPerSet New max perpetual positions per set
568568
*/
569569
function updateMaxPerpPositionsPerSet(uint256 _maxPerpPositionsPerSet) external onlyOwner {
570570
maxPerpPositionsPerSet = _maxPerpPositionsPerSet;
@@ -1052,8 +1052,8 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
10521052

10531053
int256 baseBalance = perpAccountBalance.getBase(address(_setToken), _baseToken);
10541054
int256 basePositionUnit = baseBalance.preciseDiv(totalSupply.toInt256());
1055-
1056-
int256 baseNotional = _baseQuantityUnits == basePositionUnit.neg()
1055+
1056+
int256 baseNotional = _baseQuantityUnits == basePositionUnit.neg()
10571057
? baseBalance.neg() // To close position completely
10581058
: _baseQuantityUnits.preciseMul(totalSupply.toInt256());
10591059

@@ -1134,7 +1134,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
11341134
function _syncPositionList(ISetToken _setToken) internal {
11351135
address[] memory positionList = positions[_setToken];
11361136
uint256 positionLength = positionList.length;
1137-
1137+
11381138
for (uint256 i = 0; i < positionLength; i++) {
11391139
address currPosition = positionList[i];
11401140
if (!_hasBaseBalance(_setToken, currPosition)) {
@@ -1236,7 +1236,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenA
12361236
* @param _components Array of components held by the SetToken
12371237
* @param _newExternalPositionUnit Dynamically calculated externalPositionUnit
12381238
* @return int256[] Components-length array with equity adjustment value at appropriate index
1239-
* @return int256[] Components-length array of zeroes (debt adjustements)
1239+
* @return int256[] Components-length array of zeroes (debt adjustments)
12401240
*/
12411241
function _formatAdjustments(
12421242
ISetToken _setToken,

test/integration/perpV2BasisTradingSlippageIssuance.spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => {
254254
setToken.address,
255255
{
256256
feeRecipient: owner.address,
257-
performanceFeePercentage: ether(.0),
257+
performanceFeePercentage: ether(.1),
258258
maxPerformanceFeePercentage: ether(.2)
259259
}
260260
);
@@ -294,6 +294,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => {
294294
baseToken,
295295
2,
296296
ether(.02),
297+
true,
297298
true
298299
);
299300

@@ -508,6 +509,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => {
508509
baseToken,
509510
2,
510511
ether(.02),
512+
true,
511513
true
512514
);
513515

@@ -542,7 +544,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => {
542544
// initialExternalPositionUnit = 10_000_000
543545
// finalExternalPositionUnit = 9_597_857
544546

545-
const expectedExternalPositionUnit = preciseDiv(usdcTransferOutQuantity, subjectQuantity);
547+
const expectedExternalPositionUnit = preciseDiv(usdcTransferOutQuantity, subjectQuantity);;
546548
expect(initialExternalPositionUnit).eq(usdcDefaultPositionUnit);
547549
expect(finalExternalPositionUnit).to.be.closeTo(expectedExternalPositionUnit, 1);
548550
});
@@ -855,6 +857,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => {
855857
baseToken,
856858
6,
857859
ether(.02),
860+
true,
858861
true
859862
);
860863

@@ -909,7 +912,12 @@ describe("PerpV2BasisTradingSlippageIssuance", () => {
909912

910913
describe("when liquidation results in negative account value", () => {
911914
beforeEach(async () => {
915+
// Move oracle price down, wait one day
916+
await perpSetup.setBaseTokenOraclePrice(vETH, usdcUnits(10.5));
917+
await increaseTimeAsync(ONE_DAY_IN_SECONDS);
918+
912919
// Calculated leverage = ~8.5X = 8_654_438_822_995_683_587
920+
// Lever again to track funding as `settled`
913921
await leverUp(
914922
setToken,
915923
perpBasisTradingModule,
@@ -918,9 +926,13 @@ describe("PerpV2BasisTradingSlippageIssuance", () => {
918926
baseToken,
919927
6,
920928
ether(.02),
929+
true,
921930
true
922931
);
923932

933+
// Freeze funding changes
934+
await perpSetup.clearingHouseConfig.setMaxFundingRate(ZERO);
935+
924936
// Move oracle price down to 5 USDC to enable liquidation
925937
await perpSetup.setBaseTokenOraclePrice(vETH, usdcUnits(5.0));
926938

test/integration/perpV2LeverageV2SlippageIssuance.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -991,7 +991,7 @@ describe("PerpV2LeverageSlippageIssuance", () => {
991991
// initialExternalPositionUnit = 10_000_000
992992
// finalExternalPositionUnit = 9_597_857
993993

994-
const expectedExternalPositionUnit = preciseDiv(usdcTransferOutQuantity, subjectQuantity);
994+
const expectedExternalPositionUnit = preciseDiv(usdcTransferOutQuantity, subjectQuantity);;
995995
expect(initialExternalPositionUnit).eq(usdcDefaultPositionUnit);
996996
expect(finalExternalPositionUnit).to.be.closeTo(expectedExternalPositionUnit, 1);
997997
});

0 commit comments

Comments
 (0)