From ac8a53eef3dfca751f451303d7664174015a5623 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 14 Dec 2021 15:02:20 -0800 Subject: [PATCH] PerpV2LeverageModule Audit Fixes (#177) Co-authored-by: bweick --- .../interfaces/IPerpV2LeverageModule.sol | 292 +++++++++ contracts/lib/PreciseUnitMath.sol | 27 + contracts/lib/UnitConversionUtils.sol | 70 ++ contracts/mocks/PreciseUnitMathMock.sol | 8 + contracts/mocks/UnitConversionUtilsMock.sol | 52 ++ .../integration/lib/UniswapV3MathMock.sol | 52 ++ .../protocol/lib/SetTokenAccessibleMock.sol | 41 ++ .../integration/lib/UniswapV3Math.sol | 62 ++ contracts/protocol/lib/SetTokenAccessible.sol | 112 ++++ .../protocol/modules/PerpV2LeverageModule.sol | 620 ++++++++---------- .../perpV2LeverageSlippageIssuance.spec.ts | 298 +++++++-- test/lib/preciseUnitMath.spec.ts | 135 ++++ test/lib/unitConversionUtils.spec.ts | 111 ++++ .../integration/lib/uniswapV3Math.spec.ts | 74 +++ test/protocol/lib/setTokenAccessible.spec.ts | 222 +++++++ .../modules/perpV2LeverageModule.spec.ts | 306 ++++----- utils/contracts/index.ts | 3 + utils/contracts/uniswapV3.ts | 2 +- utils/deploys/deployMocks.ts | 21 +- utils/fixtures/perpV2Fixture.ts | 4 + 20 files changed, 1919 insertions(+), 593 deletions(-) create mode 100644 contracts/interfaces/IPerpV2LeverageModule.sol create mode 100644 contracts/lib/UnitConversionUtils.sol create mode 100644 contracts/mocks/UnitConversionUtilsMock.sol create mode 100644 contracts/mocks/protocol/integration/lib/UniswapV3MathMock.sol create mode 100644 contracts/mocks/protocol/lib/SetTokenAccessibleMock.sol create mode 100644 contracts/protocol/integration/lib/UniswapV3Math.sol create mode 100644 contracts/protocol/lib/SetTokenAccessible.sol create mode 100644 test/lib/unitConversionUtils.spec.ts create mode 100644 test/protocol/integration/lib/uniswapV3Math.spec.ts create mode 100644 test/protocol/lib/setTokenAccessible.spec.ts diff --git a/contracts/interfaces/IPerpV2LeverageModule.sol b/contracts/interfaces/IPerpV2LeverageModule.sol new file mode 100644 index 000000000..673ba86e4 --- /dev/null +++ b/contracts/interfaces/IPerpV2LeverageModule.sol @@ -0,0 +1,292 @@ +/* + Copyright 2021 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { ISetToken } from "./ISetToken.sol"; +import { IDebtIssuanceModule } from "./IDebtIssuanceModule.sol"; +import { IAccountBalance } from "./external/perp-v2/IAccountBalance.sol"; +import { IClearingHouse } from "./external/perp-v2/IClearingHouse.sol"; +import { IExchange } from "./external/perp-v2/IExchange.sol"; +import { IVault } from "./external/perp-v2/IVault.sol"; +import { IQuoter } from "./external/perp-v2/IQuoter.sol"; +import { IMarketRegistry } from "./external/perp-v2/IMarketRegistry.sol"; + + +/** + * @title IPerpV2LeverageModule + * @author Set Protocol + * + * Interface for the PerpV2LeverageModule. Only specifies Manager permissioned functions, events + * and getters. PerpV2LeverageModule also inherits from ModuleBase and SetTokenAccessible which support + * additional methods. + */ +interface IPerpV2LeverageModule { + + /* ============ Structs ============ */ + + struct PositionNotionalInfo { + address baseToken; // Virtual token minted by the Perp protocol + int256 baseBalance; // Base position notional quantity in 10**18 decimals. When negative, position is short + int256 quoteBalance; // vUSDC "debt" notional quantity minted to open position. When positive, position is short + } + + struct PositionUnitInfo { + address baseToken; // Virtual token minted by the Perp protocol + int256 baseUnit; // Base position unit. When negative, position is short + int256 quoteUnit; // vUSDC "debt" position unit. When positive, position is short + } + + // Note: when `pendingFundingPayments` is positive it will be credited to account on settlement, + // when negative it's a debt owed that will be repaid on settlement. (PerpProtocol.Exchange returns the value + // with the opposite meaning, e.g positively signed payments are owed by account to system). + struct AccountInfo { + int256 collateralBalance; // Quantity of collateral deposited in Perp vault in 10**18 decimals + int256 owedRealizedPnl; // USDC quantity of profit and loss in 10**18 decimals not yet settled to vault + int256 pendingFundingPayments; // USDC quantity of pending funding payments in 10**18 decimals + int256 netQuoteBalance; // USDC quantity of net quote balance for all open positions in Perp account + } + + /* ============ Events ============ */ + + /** + * @dev Emitted on trade + * @param _setToken Instance of SetToken + * @param _baseToken Virtual token minted by the Perp protocol + * @param _deltaBase Change in baseToken position size resulting from trade + * @param _deltaQuote Change in vUSDC position size resulting from trade + * @param _protocolFee Quantity in collateral decimals sent to fee recipient during lever trade + * @param _isBuy True when baseToken is being bought, false when being sold + */ + event PerpTraded( + ISetToken indexed _setToken, + address indexed _baseToken, + uint256 _deltaBase, + uint256 _deltaQuote, + uint256 _protocolFee, + bool _isBuy + ); + + /** + * @dev Emitted on deposit (not issue or redeem) + * @param _setToken Instance of SetToken + * @param _collateralToken Token being deposited as collateral (USDC) + * @param _amountDeposited Amount of collateral being deposited into Perp + */ + event CollateralDeposited( + ISetToken indexed _setToken, + IERC20 _collateralToken, + uint256 _amountDeposited + ); + + /** + * @dev Emitted on withdraw (not issue or redeem) + * @param _setToken Instance of SetToken + * @param _collateralToken Token being withdrawn as collateral (USDC) + * @param _amountWithdrawn Amount of collateral being withdrawn from Perp + */ + event CollateralWithdrawn( + ISetToken indexed _setToken, + IERC20 _collateralToken, + uint256 _amountWithdrawn + ); + + /* ============ State Variable Getters ============ */ + + // PerpV2 contract which provides getters for base, quote, and owedRealizedPnl balances + function perpAccountBalance() external view returns(IAccountBalance); + + // PerpV2 contract which provides a trading API + function perpClearingHouse() external view returns(IClearingHouse); + + // PerpV2 contract which manages trading logic. Provides getters for UniswapV3 pools and pending funding balances + function perpExchange() external view returns(IExchange); + + // PerpV2 contract which handles deposits and withdrawals. Provides getter for collateral balances + function perpVault() external view returns(IVault); + + // PerpV2 contract which makes it possible to simulate a trade before it occurs + function perpQuoter() external view returns(IQuoter); + + // PerpV2 contract which provides a getter for baseToken UniswapV3 pools + function perpMarketRegistry() external view returns(IMarketRegistry); + + // Token (USDC) used as a vault deposit, Perp currently only supports USDC as it's settlement and collateral token + function collateralToken() external view returns(IERC20); + + // Decimals of collateral token. We set this in the constructor for later reading + function collateralDecimals() external view returns(uint8); + + /* ============ External Functions ============ */ + + /** + * @dev MANAGER ONLY: Initializes this module to the SetToken. Either the SetToken needs to be on the + * allowed list or anySetAllowed needs to be true. + * + * @param _setToken Instance of the SetToken to initialize + */ + function initialize(ISetToken _setToken) external; + + /** + * @dev MANAGER ONLY: Allows manager to buy or sell perps to change exposure to the underlying baseToken. + * Providing a positive value for `_baseQuantityUnits` buys vToken on UniswapV3 via Perp's ClearingHouse, + * Providing a negative value sells the token. `_quoteBoundQuantityUnits` defines a min-receive-like slippage + * bound for the amount of vUSDC quote asset the trade will either pay or receive as a result of the action. + * + * NOTE: This method doesn't update the externalPositionUnit because it is a function of UniswapV3 virtual + * token market prices and needs to be generated on the fly to be meaningful. + * + * As a user when levering, e.g increasing the magnitude of your position, you'd trade as below + * | ----------------------------------------------------------------------------------------------- | + * | Type | Action | Goal | `quoteBoundQuantity` | `baseQuantityUnits` | + * | ----- |-------- | ------------------------- | --------------------------- | ------------------- | + * | Long | Buy | pay least amt. of vQuote | upper bound of input quote | positive | + * | Short | Sell | get most amt. of vQuote | lower bound of output quote | negative | + * | ----------------------------------------------------------------------------------------------- | + * + * As a user when delevering, e.g decreasing the magnitude of your position, you'd trade as below + * | ----------------------------------------------------------------------------------------------- | + * | Type | Action | Goal | `quoteBoundQuantity` | `baseQuantityUnits` | + * | ----- |-------- | ------------------------- | --------------------------- | ------------------- | + * | Long | Sell | get most amt. of vQuote | upper bound of input quote | negative | + * | Short | Buy | pay least amt. of vQuote | lower bound of output quote | positive | + * | ----------------------------------------------------------------------------------------------- | + * + * @param _setToken Instance of the SetToken + * @param _baseToken Address virtual token being traded + * @param _baseQuantityUnits Quantity of virtual token to trade in position units + * @param _quoteBoundQuantityUnits Max/min of vQuote asset to pay/receive when buying or selling + */ + function trade( + ISetToken _setToken, + address _baseToken, + int256 _baseQuantityUnits, + uint256 _quoteBoundQuantityUnits + ) + external; + + /** + * @dev MANAGER ONLY: Deposits default position collateral token into the PerpV2 Vault, increasing + * the size of the Perp account external position. This method is useful for establishing initial + * collateralization ratios, e.g the flow when setting up a 2X external position would be to deposit + * 100 units of USDC and execute a lever trade for ~200 vUSDC worth of vToken with the difference + * between these made up as automatically "issued" margin debt in the PerpV2 system. + * + * @param _setToken Instance of the SetToken + * @param _collateralQuantityUnits Quantity of collateral to deposit in position units + */ + function deposit(ISetToken _setToken, uint256 _collateralQuantityUnits) external; + + + /** + * @dev MANAGER ONLY: Withdraws collateral token from the PerpV2 Vault to a default position on + * the SetToken. This method is useful when adjusting the overall composition of a Set which has + * a Perp account external position as one of several components. + * + * NOTE: Within PerpV2, `withdraw` settles `owedRealizedPnl` and any pending funding payments + * to the Perp vault prior to transfer. + * + * @param _setToken Instance of the SetToken + * @param _collateralQuantityUnits Quantity of collateral to withdraw in position units + */ + function withdraw(ISetToken _setToken, uint256 _collateralQuantityUnits) external; + + + /* ============ External Getter Functions ============ */ + + /** + * @dev Gets the positive equity collateral externalPositionUnit that would be calculated for + * issuing a quantity of SetToken, representing the amount of collateral that would need to + * be transferred in per SetToken. Values in the returned arrays map to the same index in the + * SetToken's components array + * + * @param _setToken Instance of SetToken + * @param _setTokenQuantity Number of sets to issue + * + * @return equityAdjustments array containing a single element and an empty debtAdjustments array + */ + function getIssuanceAdjustments(ISetToken _setToken, uint256 _setTokenQuantity) + external + returns (int256[] memory, int256[] memory); + + + /** + * @dev Gets the positive equity collateral externalPositionUnit that would be calculated for + * redeeming a quantity of SetToken representing the amount of collateral returned per SetToken. + * Values in the returned arrays map to the same index in the SetToken's components array. + * + * @param _setToken Instance of SetToken + * @param _setTokenQuantity Number of sets to issue + * + * @return equityAdjustments array containing a single element and an empty debtAdjustments array + */ + function getRedemptionAdjustments(ISetToken _setToken, uint256 _setTokenQuantity) + external + returns (int256[] memory, int256[] memory); + + /** + * @dev Returns a PositionUnitNotionalInfo array representing all positions open for the SetToken. + * + * @param _setToken Instance of SetToken + * + * @return PositionUnitInfo array, in which each element has properties: + * + * + baseToken: address, + * + baseBalance: baseToken balance as notional quantity (10**18) + * + quoteBalance: USDC quote asset balance as notional quantity (10**18) + */ + function getPositionNotionalInfo(ISetToken _setToken) external view returns (PositionNotionalInfo[] memory); + + /** + * @dev Returns a PositionUnitInfo array representing all positions open for the SetToken. + * + * @param _setToken Instance of SetToken + * + * @return PositionUnitInfo array, in which each element has properties: + * + * + baseToken: address, + * + baseUnit: baseToken balance as position unit (10**18) + * + quoteUnit: USDC quote asset balance as position unit (10**18) + */ + function getPositionUnitInfo(ISetToken _setToken) external view returns (PositionUnitInfo[] memory); + + /** + * @dev Gets Perp account info for SetToken. Returns an AccountInfo struct containing account wide + * (rather than position specific) balance info + * + * @param _setToken Instance of the SetToken + * + * @return accountInfo struct with properties for: + * + * + collateral balance (10**18, regardless of underlying collateral decimals) + * + owed realized Pnl` (10**18) + * + pending funding payments (10**18) + * + net quote balance (10**18) + */ + function getAccountInfo(ISetToken _setToken) external view returns (AccountInfo memory accountInfo); + + /** + * @dev Gets the mid-point price of a virtual asset from UniswapV3 markets maintained by Perp Protocol + * + * @param _baseToken) Address of virtual token to price + * @return price Mid-point price of virtual token in UniswapV3 AMM market + */ + function getAMMSpotPrice(address _baseToken) external view returns (uint256 price); +} \ No newline at end of file diff --git a/contracts/lib/PreciseUnitMath.sol b/contracts/lib/PreciseUnitMath.sol index 456fbb0c9..ebf02454b 100644 --- a/contracts/lib/PreciseUnitMath.sol +++ b/contracts/lib/PreciseUnitMath.sol @@ -19,6 +19,7 @@ pragma solidity 0.6.10; pragma experimental ABIEncoderV2; +import { SafeCast } from "@openzeppelin/contracts/utils/SafeCast.sol"; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { SignedSafeMath } from "@openzeppelin/contracts/math/SignedSafeMath.sol"; @@ -33,10 +34,13 @@ import { SignedSafeMath } from "@openzeppelin/contracts/math/SignedSafeMath.sol" * CHANGELOG: * - 9/21/20: Added safePower function * - 4/21/21: Added approximatelyEquals function + * - 12/13/21: Added preciseDivCeil (int overloads) function + * - 12/13/21: Added abs function */ library PreciseUnitMath { using SafeMath for uint256; using SignedSafeMath for int256; + using SafeCast for int256; // The number One in precise units. uint256 constant internal PRECISE_UNIT = 10 ** 18; @@ -134,6 +138,22 @@ library PreciseUnitMath { return a > 0 ? a.mul(PRECISE_UNIT).sub(1).div(b).add(1) : 0; } + /** + * @dev Divides value a by value b (result is rounded up or away from 0). When `a` is 0, 0 is + * returned. When `b` is 0, method reverts with divide-by-zero error. + */ + function preciseDivCeil(int256 a, int256 b) internal pure returns (int256) { + require(b != 0, "Cant divide by 0"); + + if (a == 0 ) { + return 0; + } else if ((a > 0 && b > 0) || (a < 0 && b < 0)) { + return a.mul(PRECISE_UNIT_INT).sub(1).div(b).add(1); + } else { + return a.mul(PRECISE_UNIT_INT).add(1).div(b).sub(1); + } + } + /** * @dev Divides value a by value b (result is rounded down - positive numbers toward 0 and negative away from 0). */ @@ -195,4 +215,11 @@ library PreciseUnitMath { function approximatelyEquals(uint256 a, uint256 b, uint256 range) internal pure returns (bool) { return a <= b.add(range) && a >= b.sub(range); } + + /** + * Returns the absolute value of int256 `a` as a uint256 + */ + function abs(int256 a) internal pure returns (uint) { + return a >= 0 ? a.toUint256() : a.mul(-1).toUint256(); + } } diff --git a/contracts/lib/UnitConversionUtils.sol b/contracts/lib/UnitConversionUtils.sol new file mode 100644 index 000000000..b597eb88e --- /dev/null +++ b/contracts/lib/UnitConversionUtils.sol @@ -0,0 +1,70 @@ +/* + Copyright 2021 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { SignedSafeMath } from "@openzeppelin/contracts/math/SignedSafeMath.sol"; + +/** + * @title UnitConversionUtils + * @author Set Protocol + * + * Utility functions to convert PRECISE_UNIT values to and from other decimal units + */ +library UnitConversionUtils { + using SafeMath for uint256; + using SignedSafeMath for int256; + + /** + * @dev Converts a uint256 PRECISE_UNIT quote quantity into an alternative decimal format. + * + * This method is borrowed from PerpProtocol's `lushan` repo in lib/SettlementTokenMath + * + * @param _amount PRECISE_UNIT amount to convert from + * @param _decimals Decimal precision format to convert to + * @return Input converted to alternative decimal precision format + */ + function fromPreciseUnitToDecimals(uint256 _amount, uint8 _decimals) internal pure returns (uint256) { + return _amount.div(10**(18 - uint(_decimals))); + } + + /** + * @dev Converts an int256 PRECISE_UNIT quote quantity into an alternative decimal format. + * + * This method is borrowed from PerpProtocol's `lushan` repo in lib/SettlementTokenMath + * + * @param _amount PRECISE_UNIT amount to convert from + * @param _decimals Decimal precision format to convert to + * @return Input converted to alternative decimal precision format + */ + function fromPreciseUnitToDecimals(int256 _amount, uint8 _decimals) internal pure returns (int256) { + return _amount.div(int256(10**(18 - uint(_decimals)))); + } + + /** + * @dev Converts an arbitrarily decimalized quantity into a PRECISE_UNIT quantity. + * + * @param _amount Non-PRECISE_UNIT amount to convert + * @param _decimals Decimal precision of amount being converted to PRECISE_UNIT + * @return Input converted to PRECISE_UNIT decimal format + */ + function toPreciseUnitsFromDecimals(int256 _amount, uint8 _decimals) internal pure returns (int256) { + return _amount.mul(int256(10**(18 - (uint(_decimals))))); + } +} diff --git a/contracts/mocks/PreciseUnitMathMock.sol b/contracts/mocks/PreciseUnitMathMock.sol index d9dcbc1ba..ca24217e3 100644 --- a/contracts/mocks/PreciseUnitMathMock.sol +++ b/contracts/mocks/PreciseUnitMathMock.sol @@ -66,6 +66,10 @@ contract PreciseUnitMathMock { return a.preciseDivCeil(b); } + function preciseDivCeilInt(int256 a, int256 b) external pure returns(int256) { + return a.preciseDivCeil(b); + } + function divDown(int256 a, int256 b) external pure returns(int256) { return a.divDown(b); } @@ -85,4 +89,8 @@ contract PreciseUnitMathMock { function approximatelyEquals(uint256 a, uint256 b, uint256 range) external pure returns (bool) { return a.approximatelyEquals(b, range); } + + function abs(int256 a) external pure returns (uint256) { + return a.abs(); + } } diff --git a/contracts/mocks/UnitConversionUtilsMock.sol b/contracts/mocks/UnitConversionUtilsMock.sol new file mode 100644 index 000000000..ca00aef23 --- /dev/null +++ b/contracts/mocks/UnitConversionUtilsMock.sol @@ -0,0 +1,52 @@ +/* + Copyright 2020 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; + +import { UnitConversionUtils } from "../lib/UnitConversionUtils.sol"; + +contract UnitConversionUtilsMock { + using UnitConversionUtils for int256; + using UnitConversionUtils for uint256; + + /* ============ External ============ */ + + function testFromPreciseUnitToDecimalsUint(uint256 _amount, uint8 _decimals) + public + pure + returns (uint256) + { + return _amount.fromPreciseUnitToDecimals(_decimals); + } + + function testFromPreciseUnitToDecimalsInt(int256 _amount, uint8 _decimals) + public + pure + returns (int256) + { + return _amount.fromPreciseUnitToDecimals(_decimals); + } + + function testToPreciseUnitsFromDecimalsInt(int256 _amount, uint8 _decimals) + public + pure + returns (int256) + { + return _amount.toPreciseUnitsFromDecimals(_decimals); + } +} diff --git a/contracts/mocks/protocol/integration/lib/UniswapV3MathMock.sol b/contracts/mocks/protocol/integration/lib/UniswapV3MathMock.sol new file mode 100644 index 000000000..2cf74aacb --- /dev/null +++ b/contracts/mocks/protocol/integration/lib/UniswapV3MathMock.sol @@ -0,0 +1,52 @@ +/* + Copyright 2021 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental ABIEncoderV2; + +import { UniswapV3Math } from "../../../../protocol/integration/lib/UniswapV3Math.sol"; + +/** + * @title UniswapV3MathMock + * @author Set Protocol + * + * Mock for UniswapV3Math Library contract. Used for testing UniswapV3Math Library contract, as the library + * contract can't be tested directly using ethers.js + */ +contract UniswapV3MathMock { + using UniswapV3Math for uint160; + using UniswapV3Math for uint256; + + /* ============ External ============ */ + + function testFormatSqrtPriceX96ToPriceX96(uint160 _sqrtPriceX96) + public + pure + returns (uint256) + { + return _sqrtPriceX96.formatSqrtPriceX96ToPriceX96(); + } + + function testFormatX96ToX10_18(uint256 _valueX96) + public + pure + returns (uint256) + { + return _valueX96.formatX96ToX10_18(); + } +} diff --git a/contracts/mocks/protocol/lib/SetTokenAccessibleMock.sol b/contracts/mocks/protocol/lib/SetTokenAccessibleMock.sol new file mode 100644 index 000000000..a00fdce6c --- /dev/null +++ b/contracts/mocks/protocol/lib/SetTokenAccessibleMock.sol @@ -0,0 +1,41 @@ +/* + Copyright 2021 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; + +import { IController } from "../../../interfaces/IController.sol"; +import { ISetToken } from "../../../interfaces/ISetToken.sol"; +import { SetTokenAccessible } from "../../../protocol/lib/SetTokenAccessible.sol"; + +contract SetTokenAccessibleMock is SetTokenAccessible { + + constructor(IController _controller) public SetTokenAccessible(_controller) {} + + /* ============ External Functions ============ */ + + function testOnlyAllowedSet(ISetToken _setToken) + external + view + onlyAllowedSet(_setToken) {} + + /* ============ Helper Functions ============ */ + + function initializeModuleOnSet(ISetToken _setToken) external { + _setToken.initializeModule(); + } +} diff --git a/contracts/protocol/integration/lib/UniswapV3Math.sol b/contracts/protocol/integration/lib/UniswapV3Math.sol new file mode 100644 index 000000000..96c86242c --- /dev/null +++ b/contracts/protocol/integration/lib/UniswapV3Math.sol @@ -0,0 +1,62 @@ +/* + Copyright 2021 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental ABIEncoderV2; + +import { FixedPoint96 } from "@uniswap/v3-core/contracts/libraries/FixedPoint96.sol"; +import { FullMath } from "@uniswap/v3-core/contracts/libraries/FullMath.sol"; + +import { PreciseUnitMath } from "../../../lib/PreciseUnitMath.sol"; + +/** + * @title UniswapV3Math + * @author Set Protocol + * + * Helper functions for managing UniswapV3 math. + */ +library UniswapV3Math { + + /** + * @dev Converts a UniswapV3 sqrtPriceX96 value to a priceX96 value. This method is borrowed from + * PerpProtocol's `lushan` repo, in lib/PerpMath. + * + * For more info about the sqrtPriceX96 format see: + * https://docs.uniswap.org/sdk/guides/fetching-prices#understanding-sqrtprice + * + * @param _sqrtPriceX96 Square root of a UniswapV3 encoded fixed-point pool price. + * @return _sqrtPriceX96 converted to a priceX96 value + */ + function formatSqrtPriceX96ToPriceX96(uint160 _sqrtPriceX96) internal pure returns (uint256) { + return FullMath.mulDiv(_sqrtPriceX96, _sqrtPriceX96, FixedPoint96.Q96); + } + + /** + * @dev Converts a UniswapV3 X96 format price into a PRECISE_UNIT price. This method is borrowed from + * PerpProtocol's `lushan` repo, in lib/PerpMath + * + * For more info about the priceX96 format see: + * https://docs.uniswap.org/sdk/guides/fetching-prices#understanding-sqrtprice + * + * @param _valueX96 UniswapV3 encoded fixed-point pool price + * @return _priceX96 as a PRECISE_UNIT value + */ + function formatX96ToX10_18(uint256 _valueX96) internal pure returns (uint256) { + return FullMath.mulDiv(_valueX96, PreciseUnitMath.preciseUnit(), FixedPoint96.Q96); + } +} diff --git a/contracts/protocol/lib/SetTokenAccessible.sol b/contracts/protocol/lib/SetTokenAccessible.sol new file mode 100644 index 000000000..8b5c9dd87 --- /dev/null +++ b/contracts/protocol/lib/SetTokenAccessible.sol @@ -0,0 +1,112 @@ +/* + Copyright 2021 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +import { IController } from "../../interfaces/IController.sol"; +import { ISetToken } from "../../interfaces/ISetToken.sol"; + +/** + * @title SetTokenAccessible + * @author Set Protocol + * + * Abstract class that houses permissioning of module for SetTokens. + */ +abstract contract SetTokenAccessible is Ownable { + + /* ============ Events ============ */ + + /** + * @dev Emitted on updateAllowedSetToken() + * @param _setToken SetToken being whose allowance to initialize this module is being updated + * @param _added true if added false if removed + */ + event SetTokenStatusUpdated( + ISetToken indexed _setToken, + bool indexed _added + ); + + /** + * @dev Emitted on updateAnySetAllowed() + * @param _anySetAllowed true if any set is allowed to initialize this module, false otherwise + */ + event AnySetAllowedUpdated( + bool indexed _anySetAllowed + ); + + /* ============ Modifiers ============ */ + + // @dev If anySetAllowed is true or _setToken is registered in allowedSetTokens, modifier succeeds. + // Reverts otherwise. + modifier onlyAllowedSet(ISetToken _setToken) { + if (!anySetAllowed) { + require(allowedSetTokens[_setToken], "Not allowed SetToken"); + } + _; + } + + /* ============ State Variables ============ */ + + // Address of the controller + IController private controller; + + // Mapping of SetToken to boolean indicating if SetToken is on allow list. Updateable by governance + mapping(ISetToken => bool) public allowedSetTokens; + + // Boolean that returns if any SetToken can initialize this module. If false, then subject to allow list. + // Updateable by governance. + bool public anySetAllowed; + + + /* ============ Constructor ============ */ + + /** + * Set controller state variable + * + * @param _controller Address of controller contract + */ + constructor(IController _controller) public { + controller = _controller; + } + + /* ============ External Functions ============ */ + + /** + * @dev GOVERNANCE ONLY: Enable/disable ability of a SetToken to initialize this module. + * + * @param _setToken Instance of the SetToken + * @param _status Bool indicating if _setToken is allowed to initialize this module + */ + function updateAllowedSetToken(ISetToken _setToken, bool _status) public onlyOwner { + require(controller.isSet(address(_setToken)) || allowedSetTokens[_setToken], "Invalid SetToken"); + allowedSetTokens[_setToken] = _status; + emit SetTokenStatusUpdated(_setToken, _status); + } + + /** + * @dev GOVERNANCE ONLY: Toggle whether ANY SetToken is allowed to initialize this module. + * + * @param _anySetAllowed Bool indicating if ANY SetToken is allowed to initialize this module + */ + function updateAnySetAllowed(bool _anySetAllowed) public onlyOwner { + anySetAllowed = _anySetAllowed; + emit AnySetAllowedUpdated(_anySetAllowed); + } +} diff --git a/contracts/protocol/modules/PerpV2LeverageModule.sol b/contracts/protocol/modules/PerpV2LeverageModule.sol index 1ca8a4552..d236c5417 100644 --- a/contracts/protocol/modules/PerpV2LeverageModule.sol +++ b/contracts/protocol/modules/PerpV2LeverageModule.sol @@ -25,10 +25,9 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; -import { FixedPoint96 } from "@uniswap/v3-core/contracts/libraries/FixedPoint96.sol"; -import { FullMath } from "@uniswap/v3-core/contracts/libraries/FullMath.sol"; import { PerpV2 } from "../integration/lib/PerpV2.sol"; +import { UniswapV3Math } from "../integration/lib/UniswapV3Math.sol"; import { IAccountBalance } from "../../interfaces/external/perp-v2/IAccountBalance.sol"; import { IClearingHouse } from "../../interfaces/external/perp-v2/IClearingHouse.sol"; import { IExchange } from "../../interfaces/external/perp-v2/IExchange.sol"; @@ -40,17 +39,18 @@ import { IDebtIssuanceModule } from "../../interfaces/IDebtIssuanceModule.sol"; import { IModuleIssuanceHook } from "../../interfaces/IModuleIssuanceHook.sol"; import { ISetToken } from "../../interfaces/ISetToken.sol"; import { ModuleBase } from "../lib/ModuleBase.sol"; +import { SetTokenAccessible } from "../lib/SetTokenAccessible.sol"; import { PreciseUnitMath } from "../../lib/PreciseUnitMath.sol"; import { AddressArrayUtils } from "../../lib/AddressArrayUtils.sol"; - +import { UnitConversionUtils } from "../../lib/UnitConversionUtils.sol"; /** - * @title PerpLeverageModule + * @title PerpV2LeverageModule * @author Set Protocol * @notice Smart contract that enables leveraged trading using the PerpV2 protocol. Each SetToken can only manage a single Perp account * represented as a positive equity external position whose value is the net Perp account value denominated in the collateral token * deposited into the Perp Protocol. This module only allows Perp positions to be collateralized by one asset, USDC, set on deployment of - * this contract (see collateralToken) however it can take positions simultaneuosly in multiple base assets. + * this contract (see collateralToken) however it can take positions simultaneously in multiple base assets. * * Upon issuance and redemption positions are not EXACTLY replicated like for other position types since a trade is necessary to enter/exit * the position on behalf of the issuer/redeemer. Any cost of entering/exiting the position (slippage) is carried by the issuer/redeemer. @@ -60,10 +60,14 @@ import { AddressArrayUtils } from "../../lib/AddressArrayUtils.sol"; * NOTE: The external position unit is only updated on an as-needed basis during issuance/redemption. It does not reflect the current * value of the Set's perpetual position. The current value can be calculated from getPositionNotionalInfo. */ -contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIssuanceHook { +contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, SetTokenAccessible, IModuleIssuanceHook { using PerpV2 for ISetToken; using PreciseUnitMath for int256; using SignedSafeMath for int256; + using UnitConversionUtils for int256; + using UniswapV3Math for uint160; + using UniswapV3Math for uint256; + using UnitConversionUtils for uint256; using AddressArrayUtils for address[]; /* ============ Structs ============ */ @@ -71,10 +75,9 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs struct ActionInfo { ISetToken setToken; address baseToken; // Virtual token minted by the Perp protocol - bool isBaseToQuote; // When true, `baseToken` is being sold, when false, bought - bool isExactInput; // When true, `amount` is the swap input, when false, the swap output - int256 amount; // Quantity in 10**18 decimals - uint256 oppositeAmountBound; // vUSDC pay or receive quantity bound (see `_createAndValidateActionInfoNotionalNotional` for details) + bool isBuy; // When true, `baseToken` is being bought, when false, sold + uint256 baseTokenAmount; // Base token quantity in 10**18 decimals + uint256 oppositeAmountBound; // vUSDC pay or receive quantity bound (see `_createActionInfoNotional` for details) } struct PositionNotionalInfo { @@ -89,6 +92,9 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs int256 quoteUnit; // vUSDC "debt" position unit. When positive, position is short } + // Note: when `pendingFundingPayments` is positive it will be credited to account on settlement, + // when negative it's a debt owed that will be repaid on settlement. (PerpProtocol.Exchange returns the value + // with the opposite meaning, e.g positively signed payments are owed by account to system). struct AccountInfo { int256 collateralBalance; // Quantity of collateral deposited in Perp vault in 10**18 decimals int256 owedRealizedPnl; // USDC quantity of profit and loss in 10**18 decimals not yet settled to vault @@ -107,7 +113,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs * @param _protocolFee Quantity in collateral decimals sent to fee recipient during lever trade * @param _isBuy True when baseToken is being bought, false when being sold */ - event PerpTrade( + event PerpTraded( ISetToken indexed _setToken, address indexed _baseToken, uint256 _deltaBase, @@ -117,7 +123,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs ); /** - * @dev Emitted on deposit (not issue or redeeem) + * @dev Emitted on deposit (not issue or redeem) * @param _setToken Instance of SetToken * @param _collateralToken Token being deposited as collateral (USDC) * @param _amountDeposited Amount of collateral being deposited into Perp @@ -129,7 +135,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs ); /** - * @dev Emitted on withdraw (not issue or redeeem) + * @dev Emitted on withdraw (not issue or redeem) * @param _setToken Instance of SetToken * @param _collateralToken Token being withdrawn as collateral (USDC) * @param _amountWithdrawn Amount of collateral being withdrawn from Perp @@ -140,24 +146,6 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs uint256 _amountWithdrawn ); - /** - * @dev Emitted on updateAllowedSetToken() - * @param _setToken SetToken being whose allowance to initialize this module is being updated - * @param _added true if added false if removed - */ - event SetTokenStatusUpdated( - ISetToken indexed _setToken, - bool indexed _added - ); - - /** - * @dev Emitted on updateAnySetAllowed() - * @param _anySetAllowed true if any set is allowed to initialize this module, false otherwise - */ - event AnySetAllowedUpdated( - bool indexed _anySetAllowed - ); - /* ============ Constants ============ */ // String identifying the DebtIssuanceModule in the IntegrationRegistry. Note: Governance must add DefaultIssuanceModule as @@ -187,7 +175,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs // PerpV2 contract which provides a getter for baseToken UniswapV3 pools IMarketRegistry public immutable perpMarketRegistry; - // Token (USDC) used as a vault deposit, Perp currently only supports USDC as it's setllement and collateral token + // Token (USDC) used as a vault deposit, Perp currently only supports USDC as it's settlement and collateral token IERC20 public immutable collateralToken; // Decimals of collateral token. We set this in the constructor for later reading @@ -195,20 +183,14 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs // Mapping of SetTokens to an array of virtual token addresses the Set has open positions for. // Array is automatically updated when new positions are opened or old positions are zeroed out. - mapping(ISetToken => address[]) public positions; - - // Mapping of SetToken to boolean indicating if SetToken is on allow list. Updateable by governance - mapping(ISetToken => bool) public allowedSetTokens; - - // Boolean that returns if any SetToken can initialize this module. If false, then subject to allow list. - // Updateable by governance. - bool public anySetAllowed; - + mapping(ISetToken => address[]) internal positions; /* ============ Constructor ============ */ /** - * @dev Sets external PerpV2 Protocol addresses. + * @dev Sets external PerpV2 Protocol contract addresses. Sets `collateralToken` and `collateralDecimals` + * to the Perp vault's settlement token (USDC) and its decimals, respectively. + * * @param _controller Address of controller contract * @param _perpVault Address of Perp Vault contract * @param _perpQuoter Address of Perp Quoter contract @@ -222,6 +204,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs ) public ModuleBase(_controller) + SetTokenAccessible(_controller) { // Use temp variables to initialize immutables address tempCollateralToken = IVault(_perpVault).getSettlementToken(); @@ -238,10 +221,40 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs /* ============ External Functions ============ */ + /** + * @dev MANAGER ONLY: Initializes this module to the SetToken. Either the SetToken needs to be on the + * allowed list or anySetAllowed needs to be true. + * + * @param _setToken Instance of the SetToken to initialize + */ + function initialize( + ISetToken _setToken + ) + external + onlySetManager(_setToken, msg.sender) + onlyValidAndPendingSet(_setToken) + onlyAllowedSet(_setToken) + { + // Initialize module before trying register + _setToken.initializeModule(); + + // Get debt issuance module registered to this module and require that it is initialized + require(_setToken.isInitializedModule( + getAndValidateAdapter(DEFAULT_ISSUANCE_MODULE_NAME)), + "Issuance not initialized" + ); + + // Try if register exists on any of the modules including the debt issuance module + address[] memory modules = _setToken.getModules(); + for(uint256 i = 0; i < modules.length; i++) { + try IDebtIssuanceModule(modules[i]).registerToIssuanceModule(_setToken) {} catch {} + } + } + /** * @dev MANAGER ONLY: Allows manager to buy or sell perps to change exposure to the underlying baseToken. * Providing a positive value for `_baseQuantityUnits` buys vToken on UniswapV3 via Perp's ClearingHouse, - * Providing a negative value sells the token. `_receiveQuoteQuantityUnits` defines a min-receive-like slippage + * Providing a negative value sells the token. `_quoteBoundQuantityUnits` defines a min-receive-like slippage * bound for the amount of vUSDC quote asset the trade will either pay or receive as a result of the action. * * NOTE: This method doesn't update the externalPositionUnit because it is a function of UniswapV3 virtual @@ -249,7 +262,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs * * As a user when levering, e.g increasing the magnitude of your position, you'd trade as below * | ----------------------------------------------------------------------------------------------- | - * | Type | Action | Goal | `receiveQuoteQuantity` | `baseQuantityUnits` | + * | Type | Action | Goal | `quoteBoundQuantity` | `baseQuantityUnits` | * | ----- |-------- | ------------------------- | --------------------------- | ------------------- | * | Long | Buy | pay least amt. of vQuote | upper bound of input quote | positive | * | Short | Sell | get most amt. of vQuote | lower bound of output quote | negative | @@ -257,7 +270,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs * * As a user when delevering, e.g decreasing the magnitude of your position, you'd trade as below * | ----------------------------------------------------------------------------------------------- | - * | Type | Action | Goal | `receiveQuoteQuantity` | `baseQuantityUnits` | + * | Type | Action | Goal | `quoteBoundQuantity` | `baseQuantityUnits` | * | ----- |-------- | ------------------------- | --------------------------- | ------------------- | * | Long | Sell | get most amt. of vQuote | upper bound of input quote | negative | * | Short | Buy | pay least amt. of vQuote | lower bound of output quote | positive | @@ -266,13 +279,13 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs * @param _setToken Instance of the SetToken * @param _baseToken Address virtual token being traded * @param _baseQuantityUnits Quantity of virtual token to trade in position units - * @param _receiveQuoteQuantityUnits Max/min of vQuote asset to pay/receive when buying or selling + * @param _quoteBoundQuantityUnits Max/min of vQuote asset to pay/receive when buying or selling */ function trade( ISetToken _setToken, address _baseToken, int256 _baseQuantityUnits, - uint256 _receiveQuoteQuantityUnits + uint256 _quoteBoundQuantityUnits ) external nonReentrant @@ -282,7 +295,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs _setToken, _baseToken, _baseQuantityUnits, - _receiveQuoteQuantityUnits + _quoteBoundQuantityUnits ); (uint256 deltaBase, uint256 deltaQuote) = _executeTrade(actionInfo); @@ -291,13 +304,13 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs _updatePositionList(_setToken, _baseToken); - emit PerpTrade( + emit PerpTraded( _setToken, _baseToken, deltaBase, deltaQuote, protocolFee, - _baseQuantityUnits > 0 + actionInfo.isBuy ); } @@ -354,52 +367,21 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs emit CollateralWithdrawn(_setToken, collateralToken, notionalWithdrawnQuantity); } - /** - * @dev MANAGER ONLY: Initializes this module to the SetToken. Either the SetToken needs to be on the - * allowed list or anySetAllowed needs to be true. - * - * @param _setToken Instance of the SetToken to initialize - */ - function initialize( - ISetToken _setToken - ) - external - onlySetManager(_setToken, msg.sender) - onlyValidAndPendingSet(_setToken) - { - if (!anySetAllowed) { - require(allowedSetTokens[_setToken], "Not allowed SetToken"); - } - - // Initialize module before trying register - _setToken.initializeModule(); - - // Get debt issuance module registered to this module and require that it is initialized - require(_setToken.isInitializedModule( - getAndValidateAdapter(DEFAULT_ISSUANCE_MODULE_NAME)), - "Issuance not initialized" - ); - - // Try if register exists on any of the modules including the debt issuance module - address[] memory modules = _setToken.getModules(); - for(uint256 i = 0; i < modules.length; i++) { - try IDebtIssuanceModule(modules[i]).registerToIssuanceModule(_setToken) {} catch {} - } - } - /** * @dev MANAGER ONLY: Removes this module from the SetToken, via call by the SetToken. Deletes * position mappings associated with SetToken. * - * NOTE: Function will revert if there is greater than a position unit amount of USDC left in the PerpV2 vault. + * NOTE: Function will revert if there is greater than a position unit amount of USDC of account value. */ function removeModule() external override onlyValidAndInitializedSet(ISetToken(msg.sender)) { ISetToken setToken = ISetToken(msg.sender); - // We can end up with a dust amount of USDC in the Perp account that we should ignore. + // Check that there is less than 1 position unit of USDC of account value (to tolerate PRECISE_UNIT math rounding errors). + // Account value is checked here because liquidation may result in a positive vault balance while net value is below zero. + int256 accountValueUnit = perpClearingHouse.getAccountValue(address(setToken)).preciseDiv(setToken.totalSupply().toInt256()); require( - _fromPreciseUnitToDecimals(_getCollateralBalance(setToken), collateralDecimals) <= 1, - "Collateral balance remaining" + accountValueUnit.fromPreciseUnitToDecimals(collateralDecimals) <= 1, + "Account balance is positive" ); delete positions[setToken]; // Should already be empty @@ -427,28 +409,6 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs _debtIssuanceModule.registerToIssuanceModule(_setToken); } - /** - * @dev GOVERNANCE ONLY: Enable/disable ability of a SetToken to initialize this module. - * - * @param _setToken Instance of the SetToken - * @param _status Bool indicating if _setToken is allowed to initialize this module - */ - function updateAllowedSetToken(ISetToken _setToken, bool _status) external onlyOwner { - require(controller.isSet(address(_setToken)) || allowedSetTokens[_setToken], "Invalid SetToken"); - allowedSetTokens[_setToken] = _status; - emit SetTokenStatusUpdated(_setToken, _status); - } - - /** - * @dev GOVERNANCE ONLY: Toggle whether ANY SetToken is allowed to initialize this module. - * - * @param _anySetAllowed Bool indicating if ANY SetToken is allowed to initialize this module - */ - function updateAnySetAllowed(bool _anySetAllowed) external onlyOwner { - anySetAllowed = _anySetAllowed; - emit AnySetAllowedUpdated(_anySetAllowed); - } - /** * @dev MODULE ONLY: Hook called prior to issuance. Only callable by valid module. Should only be called ONCE * during issue. Trades into current positions and sets the collateralToken's externalPositionUnit so that @@ -468,8 +428,9 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs onlyModule(_setToken) { if (_setToken.totalSupply() == 0) return; + if (!_setToken.hasExternalPosition(address(collateralToken))) return; - int256 newExternalPositionUnit = _executeModuleIssuanceHook(_setToken, _setTokenQuantity, false); + int256 newExternalPositionUnit = _executePositionTrades(_setToken, _setTokenQuantity, true, false); // Set collateralToken externalPositionUnit such that DIM can use it for transfer calculation _setToken.editExternalPositionUnit( @@ -498,8 +459,9 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs onlyModule(_setToken) { if (_setToken.totalSupply() == 0) return; + if (!_setToken.hasExternalPosition(address(collateralToken))) return; - int256 newExternalPositionUnit = _executeModuleRedemptionHook(_setToken, _setTokenQuantity, false); + int256 newExternalPositionUnit = _executePositionTrades(_setToken, _setTokenQuantity, false, false); // Set USDC externalPositionUnit such that DIM can use it for transfer calculation _setToken.editExternalPositionUnit( @@ -515,6 +477,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs * @param _setToken Instance of the SetToken * @param _setTokenQuantity Quantity of SetToken to issue * @param _component Address of deposit collateral component + * @param _isEquity True if componentHook called from issuance module for equity flow, false otherwise */ function componentIssueHook( ISetToken _setToken, @@ -545,6 +508,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs * @param _setToken Instance of the SetToken * @param _setTokenQuantity Quantity of SetToken to redeem * @param _component Address of deposit collateral component + * @param _isEquity True if componentHook called from issuance module for equity flow, false otherwise */ function componentRedeemHook( ISetToken _setToken, @@ -569,7 +533,8 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs /** * @dev Gets the positive equity collateral externalPositionUnit that would be calculated for * issuing a quantity of SetToken, representing the amount of collateral that would need to - * be transferred in per SetToken. + * be transferred in per SetToken. Values in the returned arrays map to the same index in the + * SetToken's components array * * @param _setToken Instance of SetToken * @param _setTokenQuantity Number of sets to issue @@ -586,7 +551,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs address[] memory components = _setToken.getComponents(); if (positions[_setToken].length > 0) { - int256 newExternalPositionUnit = _executeModuleIssuanceHook(_setToken, _setTokenQuantity, true); + int256 newExternalPositionUnit = _executePositionTrades(_setToken, _setTokenQuantity, true, true); return _formatAdjustments(_setToken, components, newExternalPositionUnit); } else { return _formatAdjustments(_setToken, components, 0); @@ -596,6 +561,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs /** * @dev Gets the positive equity collateral externalPositionUnit that would be calculated for * redeeming a quantity of SetToken representing the amount of collateral returned per SetToken. + * Values in the returned arrays map to the same index in the SetToken's components array. * * @param _setToken Instance of SetToken * @param _setTokenQuantity Number of sets to issue @@ -612,7 +578,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs address[] memory components = _setToken.getComponents(); if (positions[_setToken].length > 0) { - int256 newExternalPositionUnit = _executeModuleRedemptionHook(_setToken, _setTokenQuantity, true); + int256 newExternalPositionUnit = _executePositionTrades(_setToken, _setTokenQuantity, false, true); return _formatAdjustments(_setToken, components, newExternalPositionUnit); } else { return _formatAdjustments(_setToken, components, 0); @@ -634,15 +600,16 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs PositionNotionalInfo[] memory positionInfo = new PositionNotionalInfo[](positions[_setToken].length); for(uint i = 0; i < positions[_setToken].length; i++){ + address baseToken = positions[_setToken][i]; positionInfo[i] = PositionNotionalInfo({ - baseToken: positions[_setToken][i], + baseToken: baseToken, baseBalance: perpAccountBalance.getBase( address(_setToken), - positions[_setToken][i] + baseToken ), quoteBalance: perpAccountBalance.getQuote( address(_setToken), - positions[_setToken][i] + baseToken ) }); } @@ -666,15 +633,16 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs PositionUnitInfo[] memory positionInfo = new PositionUnitInfo[](positions[_setToken].length); for(uint i = 0; i < positions[_setToken].length; i++){ + address baseToken = positions[_setToken][i]; positionInfo[i] = PositionUnitInfo({ - baseToken: positions[_setToken][i], + baseToken: baseToken, baseUnit: perpAccountBalance.getBase( address(_setToken), - positions[_setToken][i] + baseToken ).preciseDiv(totalSupply), quoteUnit: perpAccountBalance.getQuote( address(_setToken), - positions[_setToken][i] + baseToken ).preciseDiv(totalSupply) }); } @@ -694,6 +662,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs * + collateral balance (10**18, regardless of underlying collateral decimals) * + owed realized Pnl` (10**18) * + pending funding payments (10**18) + * + net quote balance (10**18) */ function getAccountInfo(ISetToken _setToken) public view returns (AccountInfo memory accountInfo) { (int256 owedRealizedPnl,, ) = perpAccountBalance.getPnlAndPendingFee(address(_setToken)); @@ -718,180 +687,108 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs function getAMMSpotPrice(address _baseToken) public view returns (uint256 price) { address pool = perpMarketRegistry.getPool(_baseToken); (uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(pool).slot0(); - uint256 priceX96 = _formatSqrtPriceX96ToPriceX96(sqrtPriceX96); - return _formatX96ToX10_18(priceX96); + uint256 priceX96 = sqrtPriceX96.formatSqrtPriceX96ToPriceX96(); + return priceX96.formatX96ToX10_18(); } /* ============ Internal Functions ============ */ /** - * @dev MODULE ONLY: Hook called prior to issuance. Only callable by valid module. - * - * NOTE: OwedRealizedPnl and PendingFunding values can be either positive or negative - * - * OwedRealizedPnl - * --------------- - * Accrues when trades execute and result in a profit or loss per the table - * below. Each withdrawal zeros out `owedRealizedPnl`, settling it to the vault. + * @dev MODULE ONLY: Hook called prior to issuance or redemption. Only callable by valid module. + * This method implements the core logic to replicate positions during issuance and redemption. Syncs + * the `positions` list before starting (because positions may have liquidated). Cycles through + * each position, trading `basePositionUnit * issueOrRedeemQuantity` and calculates the amount of + * USDC to transfer in/out for exchange, ensuring that issuer/redeemer pays slippage and that any + * pending payments like funding or owedRealizedPnl are socialized among existing Set holders + * appropriately. The hook which invokes this method sets the SetToken's externalPositionUnit using + * the positionUnit value returned here. Subsequent transfers in/out are managed by the issuance module + * which reads this value. * - * | -------------------------------------------------- | - * | Position Type | AMM Spot Price | Action | Value | - * | ------------- | -------------- | ------ | ------- | - * | Long | Rises | Sell | Positive | - * | Long | Falls | Sell | Negative | - * | Short | Rises | Buy | Negative | - * | Short | Falls | Buy | Positive | - * | -------------------------------------------------- | + * The general formula for determining `accountValue` per Set is: * + * `accountValue = collateral <--- + * + owedRealizedPnl } totalCollateralValue + * + pendingFundingPayment <--- + * + netQuoteBalance neg. when long, pos. when short + * +/- sum( |deltaQuoteResultingFromTrade| ) add when long, subtract when short * - * PendingFunding - * -------------- - * The direction of this flow is determined by the difference between virtual asset UniV3 spot prices and - * their parent asset's broader market price (as represented by a Chainlink oracle), per the table below. - * Each trade zeroes out `pendingFunding`, settling it to owedRealizedPnl. + * (See docs for `_calculatePartialAccountValuePositionUnit` below for more detail about the + * account value components). * - * | --------------------------------------- | - * | Position Type | Oracle Price | Value | - * | ------------- | ------------ | -------- | - * | Long | Below AMM | Negative | - * | Long | Above AMM | Positive | - * | Short | Below AMM | Positive | - * | Short | Above AMM | Negative | - * | --------------------------------------- | + * NOTE: On issuance, this hook is run *BEFORE* USDC is transferred in and deposited to the Perp + * vault to pay for the issuer's Sets. This trading temporarily spikes the Perp account's + * margin ratio (capped at ~9X) and limits the amount of Set that can issued at once to + * a multiple of the current Perp account value (will vary depending on Set's leverage ratio). * * @param _setToken Instance of the SetToken * @param _setTokenQuantity Quantity of Set to issue + * @param _isIssue If true, invocation is for issuance, redemption otherwise * @param _isSimulation If true, trading is only simulated (to return issuance adjustments) + * @return int256 Amount of collateral to transfer in/out in position units */ - function _executeModuleIssuanceHook( + function _executePositionTrades( ISetToken _setToken, uint256 _setTokenQuantity, + bool _isIssue, bool _isSimulation ) internal returns (int256) { - // From perp: - // accountValue = collateral <--- - // + owedRealizedPnl } totalCollateralValue - // + pendingFundingPayment <--- - // + sum_over_market(positionValue_market) - // + netQuoteBalance + _syncPositionList(_setToken); + int256 setTokenQuantityInt = _setTokenQuantity.toInt256(); - AccountInfo memory accountInfo = getAccountInfo(_setToken); - - int256 usdcAmountIn = accountInfo.collateralBalance - .add(accountInfo.owedRealizedPnl) - .add(accountInfo.pendingFundingPayments) - .add(accountInfo.netQuoteBalance) - .preciseDiv(_setToken.totalSupply().toInt256()) - .preciseMul(_setTokenQuantity.toInt256()); + // Note: `issued` naming convention used here for brevity. This logic is also run on redemption + // and variable may refer to the value which will be redeemed. + int256 accountValueIssued = _calculatePartialAccountValuePositionUnit(_setToken).preciseMul(setTokenQuantityInt); PositionUnitInfo[] memory positionInfo = getPositionUnitInfo(_setToken); for(uint i = 0; i < positionInfo.length; i++) { - // baseUnit, +ve existing long position, -ve for existing short position - int256 baseTradeNotionalQuantity = positionInfo[i].baseUnit.preciseMul(_setTokenQuantity.toInt256()); + int256 baseTradeNotionalQuantity = positionInfo[i].baseUnit.preciseMul(setTokenQuantityInt); - ActionInfo memory actionInfo = _createAndValidateActionInfoNotional( + // When redeeming, we flip the sign of baseTradeNotionalQuantity because we are reducing the size of the position, + // e.g selling base when long, buying base when short + ActionInfo memory actionInfo = _createActionInfoNotional( _setToken, positionInfo[i].baseToken, - baseTradeNotionalQuantity, + _isIssue ? baseTradeNotionalQuantity : baseTradeNotionalQuantity.mul(-1), 0 ); - int256 spotPrice = getAMMSpotPrice(positionInfo[i].baseToken).toInt256(); - - // idealDeltaQuote, +ve for existing long position, -ve for existing short position - int256 idealDeltaQuote = baseTradeNotionalQuantity.preciseMul(spotPrice); - // Execute or simulate trade. // `deltaQuote` is always a positive number (, uint256 deltaQuote) = _isSimulation ? _simulateTrade(actionInfo) : _executeTrade(actionInfo); - // Calculate slippage quantity as a positive value - // When long, trade slippage results in more quote required, deltaQuote > idealDeltaQuote - // When short, trade slippage results in less quote receivied, abs(idealDeltaQuote) > abs(deltaQuote) - int256 slippageQuantity = baseTradeNotionalQuantity >= 0 - ? deltaQuote.toInt256().sub(idealDeltaQuote) - : _abs(idealDeltaQuote).sub(deltaQuote.toInt256()); - // slippage is borne by the issuer - usdcAmountIn = usdcAmountIn.add(idealDeltaQuote).add(slippageQuantity); + accountValueIssued = baseTradeNotionalQuantity >= 0 ? accountValueIssued.add(deltaQuote.toInt256()) : + accountValueIssued.sub(deltaQuote.toInt256()); } // Return value in collateral decimals (e.g USDC = 6) - return _fromPreciseUnitToDecimals( - usdcAmountIn.preciseDiv(_setTokenQuantity.toInt256()), - collateralDecimals - ); + // Use preciseDivCeil when issuing to ensure we don't under-collateralize due to rounding error + return (_isIssue) + ? accountValueIssued.preciseDivCeil(setTokenQuantityInt).fromPreciseUnitToDecimals(collateralDecimals) + : accountValueIssued.preciseDiv(setTokenQuantityInt).fromPreciseUnitToDecimals(collateralDecimals); } /** - * @dev Hook called prior to redemption. Only callable by valid module. + * Calculates the "partial account value" position unit. This is the sum of the vault collateral balance, + * the net quote balance for all positions, and any pending funding or owed realized Pnl balances, + * as a position unit. It forms the base to which traded position values are added during issuance or redemption, + * and to which existing position values are added when calculating the externalPositionUnit. + * * @param _setToken Instance of the SetToken - * @param _setTokenQuantity Quantity of Set to redeem - * @param _isSimulation If true, trading is only simulated (to return issuance adjustments) + * @return accountValue Partial account value in position units */ - function _executeModuleRedemptionHook( - ISetToken _setToken, - uint256 _setTokenQuantity, - bool _isSimulation - ) - internal - returns (int256) - { - int256 realizedPnl = 0; - - PositionNotionalInfo[] memory positionInfo = getPositionNotionalInfo(_setToken); + function _calculatePartialAccountValuePositionUnit(ISetToken _setToken) internal view returns (int256 accountValue) { AccountInfo memory accountInfo = getAccountInfo(_setToken); - // Calculate already accrued PnL from non-issuance/redemption sources (ex: levering) - int256 totalFundingAndCarriedPnL = accountInfo.pendingFundingPayments.add(accountInfo.owedRealizedPnl); - int256 owedRealizedPnlPositionUnit = totalFundingAndCarriedPnL.preciseDiv(_setToken.totalSupply().toInt256()); - - for (uint256 i = 0; i < positionInfo.length; i++) { - // Calculate amount to trade - int256 basePositionUnit = positionInfo[i].baseBalance.preciseDiv(_setToken.totalSupply().toInt256()); - int256 baseTradeNotionalQuantity = basePositionUnit.preciseMul(_setTokenQuantity.toInt256()); - - // Calculate amount quote debt will be reduced by - int256 reducedOpenNotional = _getReducedOpenNotional( - _setTokenQuantity.toInt256(), - basePositionUnit, - positionInfo[i] - ); - - // Trade, inverting notional quantity sign because we are reducing position - ActionInfo memory actionInfo = _createAndValidateActionInfoNotional( - _setToken, - positionInfo[i].baseToken, - baseTradeNotionalQuantity.mul(-1), - 0 - ); - - // Execute or simulate trade. - // `deltaQuote` is always a positive number - (,uint256 deltaQuote) = _isSimulation ? _simulateTrade(actionInfo) : _executeTrade(actionInfo); - - // Calculate realized PnL for and add to running total. - // When basePositionUnit is positive, position is long. - realizedPnl = basePositionUnit >= 0 ? realizedPnl.add(reducedOpenNotional.add(deltaQuote.toInt256())) : - realizedPnl.add(reducedOpenNotional.sub(deltaQuote.toInt256())); - } - - // Calculate amount of collateral to withdraw - int256 collateralPositionUnit = _getCollateralBalance(_setToken).preciseDiv(_setToken.totalSupply().toInt256()); - - int256 usdcToWithdraw = - collateralPositionUnit.preciseMul(_setTokenQuantity.toInt256()) - .add(owedRealizedPnlPositionUnit.preciseMul(_setTokenQuantity.toInt256())) - .add(realizedPnl); - - return _fromPreciseUnitToDecimals( - usdcToWithdraw.preciseDiv(_setTokenQuantity.toInt256()), - collateralDecimals - ); + accountValue = accountInfo.collateralBalance + .add(accountInfo.owedRealizedPnl) + .add(accountInfo.pendingFundingPayments) + .add(accountInfo.netQuoteBalance) + .preciseDiv(_setToken.totalSupply().toInt256()); } /** @@ -899,6 +796,9 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs * Updates the collateral token default position unit. This function is called directly by * the componentIssue hook, skipping external position unit setting because that method is assumed * to be the end of a call sequence (e.g manager will not need to read the updated value) + * + * @param _setToken Instance of SetToken + * @param _collateralNotionalQuantity Notional collateral quantity to deposit */ function _deposit(ISetToken _setToken, uint256 _collateralNotionalQuantity) internal { _setToken.invokeApprove( @@ -916,6 +816,10 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs * * NOTE: This flow is only used when invoking the external `deposit` function - it converts collateral * quantity units into a notional quantity. + * + * @param _setToken Instance of SetToken + * @param _collateralQuantityUnits Collateral quantity in position units to deposit + * @return uint256 Notional quantity deposited */ function _depositAndUpdatePositions( ISetToken _setToken, @@ -950,6 +854,9 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs * into a default position. This function is called directly by _accrueFee and _moduleRedeemHook, * skipping position unit state updates because the funds withdrawn to SetToken are immediately * forwarded to `feeRecipient` and SetToken owner respectively. + * + * @param _setToken Instance of SetToken + * @param _collateralNotionalQuantity Notional collateral quantity to withdraw */ function _withdraw(ISetToken _setToken, uint256 _collateralNotionalQuantity) internal { if (_collateralNotionalQuantity == 0) return; @@ -964,6 +871,10 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs * * NOTE: This flow is only used when invoking the external `withdraw` function - it converts * a collateral units quantity into a notional quantity before invoking withdraw. + * + * @param _setToken Instance of SetToken + * @param _collateralQuantityUnits Collateral quantity in position units to withdraw + * @return uint256 Notional quantity withdrawn */ function _withdrawAndUpdatePositions( ISetToken _setToken, @@ -996,16 +907,28 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs /** * @dev Formats Perp Protocol openPosition call and executes via SetToken (and PerpV2 lib) + * + * `isBaseToQuote`, `isExactInput` and `oppositeAmountBound` are configured as below: + * | ---------------------------------------------------|---------------------------- | + * | Action | isBuy | isB2Q | Exact In / Out | Opposite Bound Description | + * | ------- |-------- |--------|-----------------------|---------------------------- | + * | Buy | true | false | exact output (false) | Max quote to pay | + * | Sell | false | true | exact input (true) | Min quote to receive | + * |----------------------------------------------------|---------------------------- | + * + * @param _actionInfo ActionInfo object * @return uint256 The base position delta resulting from the trade * @return uint256 The quote asset position delta resulting from the trade */ function _executeTrade(ActionInfo memory _actionInfo) internal returns (uint256, uint256) { + // When isBaseToQuote is true, `baseToken` is being sold, when false, bought + // When isExactInput is true, `amount` is the swap input, when false, the swap output IClearingHouse.OpenPositionParams memory params = IClearingHouse.OpenPositionParams({ baseToken: _actionInfo.baseToken, - isBaseToQuote: _actionInfo.isBaseToQuote, - isExactInput: _actionInfo.isExactInput, - amount: _actionInfo.amount.toUint256(), + isBaseToQuote: !_actionInfo.isBuy, + isExactInput: !_actionInfo.isBuy, + amount: _actionInfo.baseTokenAmount, oppositeAmountBound: _actionInfo.oppositeAmountBound, deadline: PreciseUnitMath.maxUint256(), sqrtPriceLimitX96: 0, @@ -1018,15 +941,19 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs /** * @dev Formats Perp Periphery Quoter.swap call and executes via SetToken (and PerpV2 lib) - * @return uint256 The base position delta resulting from the trade - * @return uint256 The quote asset position delta resulting from the trade + * + * See _executeTrade method comments for details about `isBaseToQuote` and `isExactInput` configuration. + * + * @param _actionInfo ActionInfo object + * @return uint256 The base position delta resulting from the trade + * @return uint256 The quote asset position delta resulting from the trade */ function _simulateTrade(ActionInfo memory _actionInfo) internal returns (uint256, uint256) { IQuoter.SwapParams memory params = IQuoter.SwapParams({ baseToken: _actionInfo.baseToken, - isBaseToQuote: _actionInfo.isBaseToQuote, - isExactInput: _actionInfo.isExactInput, - amount: _actionInfo.amount.toUint256(), + isBaseToQuote: !_actionInfo.isBuy, + isExactInput: !_actionInfo.isBuy, + amount: _actionInfo.baseTokenAmount, sqrtPriceLimitX96: 0 }); @@ -1036,7 +963,10 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs /** * @dev Calculates protocol fee on module and pays protocol fee from SetToken - * @return uint256 Total protocol fee paid in underlying collateral decimals e.g (USDC = 6) + * + * @param _setToken Instance of SetToken + * @param _exchangedQuantity Notional quantity of USDC exchanged in trade (e.g deltaQuote) + * @return uint256 Total protocol fee paid in underlying collateral decimals e.g (USDC = 6) */ function _accrueProtocolFee( ISetToken _setToken, @@ -1046,21 +976,19 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs returns(uint256) { uint256 protocolFee = getModuleFee(PROTOCOL_TRADE_FEE_INDEX, _exchangedQuantity); - uint256 protocolFeeInCollateralDecimals = _fromPreciseUnitToDecimals( - protocolFee, - collateralDecimals - ); + uint256 protocolFeeInPreciseUnits = protocolFee.fromPreciseUnitToDecimals(collateralDecimals); - _withdraw(_setToken, protocolFeeInCollateralDecimals); + _withdraw(_setToken, protocolFeeInPreciseUnits); - payProtocolFeeFromSetToken(_setToken, address(collateralToken), protocolFeeInCollateralDecimals); + payProtocolFeeFromSetToken(_setToken, address(collateralToken), protocolFeeInPreciseUnits); - return protocolFeeInCollateralDecimals; + return protocolFeeInPreciseUnits; } /** * @dev Construct the ActionInfo struct for trading. This method takes POSITION UNIT amounts and passes to - * _createAndValidateActionInfoNotional to create the struct. If the _baseTokenQuantity is zero then revert. + * _createActionInfoNotional to create the struct. If the _baseTokenQuantity is zero then revert. This + * method is only called from `trade` - the issue/redeem flow uses createActionInfoNotional directly. * * @param _setToken Instance of the SetToken * @param _baseToken Address of base token being traded into/out of @@ -1080,10 +1008,11 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs returns(ActionInfo memory) { require(_baseTokenUnits != 0, "Amount is 0"); + require(perpMarketRegistry.hasPool(_baseToken), "Base token does not exist"); uint256 totalSupply = _setToken.totalSupply(); - return _createAndValidateActionInfoNotional( + return _createActionInfoNotional( _setToken, _baseToken, _baseTokenUnits.preciseMul(totalSupply.toInt256()), @@ -1093,14 +1022,10 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs /** * @dev Construct the ActionInfo struct for trading. This method takes NOTIONAL token amounts and creates - * the struct. If the _baseTokenQuantity is less than zero then we are selling the baseToken. + * the struct. If the _baseTokenQuantity is greater than zero then we are buying the baseToken. This method + * is called during issue and redeem via `_executePositionTrades` and during trade via `_createAndValidateActionInfo`. * - * | ---------------------------------------------------------------------------------------------| - * | Action | isShort | isB2Q | Exact In / Out | Amount | Opposit Bound Description | - * | ------- |---------|--------|-----------------------|-----------|---------------------------- | - * | Sell | true | true | exact input (true) | baseToken | Min quote to receive | - * | Buy | false | false | exact output (false) | baseToken | Max quote to pay | - * |----------------------------------------------------------------------------------------------| + * (See _executeTrade method comments for details about `oppositeAmountBound` configuration) * * @param _setToken Instance of the SetToken * @param _baseToken Address of base token being traded into/out of @@ -1109,7 +1034,7 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs * * @return ActionInfo Instance of constructed ActionInfo struct */ - function _createAndValidateActionInfoNotional( + function _createActionInfoNotional( ISetToken _setToken, address _baseToken, int256 _baseTokenQuantity, @@ -1120,16 +1045,15 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs returns(ActionInfo memory) { // NOT checking that _baseTokenQuantity != 0 here because for places this is directly called - // (issue/redeem hooks) we know they position cannot be 0. We check in _createAndValidateActionInfo + // (issue/redeem hooks) we know the position cannot be 0. We check in _createAndValidateActionInfo // that quantity is 0 for inputs to trade. - bool isShort = _baseTokenQuantity < 0; + bool isBuy = _baseTokenQuantity > 0; return ActionInfo({ setToken: _setToken, baseToken: _baseToken, - isBaseToQuote: isShort, - isExactInput: isShort, - amount: _abs(_baseTokenQuantity), + isBuy: isBuy, + baseTokenAmount: _baseTokenQuantity.abs(), oppositeAmountBound: _quoteReceiveQuantity }); } @@ -1137,44 +1061,63 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs /** * @dev Update position address array if a token has been newly added or completely sold off * during lever/delever + * + * @param _setToken Instance of SetToken + * @param _baseToken Address of virtual base token */ function _updatePositionList(ISetToken _setToken, address _baseToken) internal { - int256 baseBalance = perpAccountBalance.getBase(address(_setToken), _baseToken); address[] memory positionList = positions[_setToken]; + bool hasBaseToken = positionList.contains(_baseToken); - if (positionList.contains(_baseToken) && baseBalance == 0) { + if (hasBaseToken && !_hasBaseBalance(_setToken, _baseToken)) { positions[_setToken].removeStorage(_baseToken); - } else if (!positionList.contains(_baseToken)) { + } else if (!hasBaseToken) { positions[_setToken].push(_baseToken); } } /** - * @dev Gets the ratio by which redemption will reduce open notional quote balance. This value - * is used to calculate realizedPnl of the asset sale in _executeModuleRedeemHook + * @dev Removes any zero balance positions from the positions array. This + * sync is done before issuance and redemption to account for positions that may have + * been liquidated. + * + * @param _setToken Instance of the SetToken */ - function _getReducedOpenNotional( - int256 _setTokenQuantity, - int256 _basePositionUnit, - PositionNotionalInfo memory _positionInfo - ) - internal - pure - returns (int256) - { - // From perp formulas: closeRatio = abs(baseTradeNotional) / abs(baseBalance) - int256 baseTradeNotionalQuantity = _setTokenQuantity.preciseMul(_basePositionUnit); - int256 closeRatio = _abs(baseTradeNotionalQuantity).preciseDiv(_abs(_positionInfo.baseBalance)); - return _positionInfo.quoteBalance.preciseMul(closeRatio); + function _syncPositionList(ISetToken _setToken) internal { + address[] memory positionList = positions[_setToken]; + + for (uint256 i = 0; i < positionList.length; i++) { + if (!_hasBaseBalance(_setToken, positionList[i])) { + positions[_setToken].removeStorage(positionList[i]); + } + } + } + + /** + * @dev Checks to see if we can make 1 positionUnit worth of a baseToken position, if not we consider the Set to have + * no balance and return false + * + * @param _setToken Instance of SetToken + * @param _baseToken Address of virtual base token + * @return bool True if a non-dust base token balance exists, false otherwise + */ + function _hasBaseBalance(ISetToken _setToken, address _baseToken) internal view returns(bool) { + int256 baseBalanceUnit = perpAccountBalance + .getBase(address(_setToken), _baseToken) + .preciseDiv(_setToken.totalSupply().toInt256()); + + return (baseBalanceUnit > 1) || (baseBalanceUnit < -1); } /** * @dev Calculates the sum of collateralToken denominated market-prices of assets and debt for the Perp account per * SetToken + * + * @param _setToken Instance of SetToken + * @return int256 External position unit */ function _calculateExternalPositionUnit(ISetToken _setToken) internal view returns (int256) { PositionNotionalInfo[] memory positionInfo = getPositionNotionalInfo(_setToken); - AccountInfo memory accountInfo = getAccountInfo(_setToken); int256 totalPositionValue = 0; for (uint i = 0; i < positionInfo.length; i++ ) { @@ -1184,26 +1127,24 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs ); } - int256 externalPositionUnitInPrecisionDecimals = totalPositionValue - .add(accountInfo.collateralBalance) - .add(accountInfo.netQuoteBalance) - .add(accountInfo.owedRealizedPnl) - .add(accountInfo.pendingFundingPayments) - .preciseDiv(_setToken.totalSupply().toInt256()); + int256 externalPositionUnitInPreciseUnits = _calculatePartialAccountValuePositionUnit(_setToken) + .add(totalPositionValue.preciseDiv(_setToken.totalSupply().toInt256())); - return _fromPreciseUnitToDecimals( - externalPositionUnitInPrecisionDecimals, - collateralDecimals - ); + return externalPositionUnitInPreciseUnits.fromPreciseUnitToDecimals(collateralDecimals); } - // @dev Retrieves collateral balance as an an 18 decimal vUSDC quote value + // @dev Retrieves collateral balance as an 18 decimal vUSDC quote value + // + // @param _setToken Instance of SetToken + // @return int256 Collateral balance as an 18 decimal vUSDC quote value function _getCollateralBalance(ISetToken _setToken) internal view returns (int256) { - int256 balance = perpVault.getBalance(address(_setToken)); - return _toPreciseUnitsFromDecimals(balance, collateralDecimals); + return perpVault.getBalance(address(_setToken)).toPreciseUnitsFromDecimals(collateralDecimals); } // @dev Retrieves net quote balance of all open positions + // + // @param _setToken Instance of SetToken + // @return int256 Net quote balance of all open positions function _getNetQuoteBalance(ISetToken _setToken) internal view returns (int256 netQuoteBalance) { for (uint256 i = 0; i < positions[_setToken].length; i++) { netQuoteBalance = netQuoteBalance.add( @@ -1219,6 +1160,12 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs * the SetToken's components array, at the same index the collateral token occupies in the components * array. All other values are left unset (0). An empty-value components length debtAdjustments * array is also returned. + * + * @param _setToken Instance of the SetToken + * @param _components Array of components held by the SetToken + * @param _newExternalPositionUnit Dynamically calculated externalPositionUnit + * @return int256[] Components-length array with equity adjustment value at appropriate index + * @return int256[] Components-length array of zeroes (debt adjustements) */ function _formatAdjustments( ISetToken _setToken, @@ -1245,57 +1192,4 @@ contract PerpV2LeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIs return (equityAdjustments, debtAdjustments); } - - /** - * @dev Converts a UniswapV3 sqrtPriceX96 value to a priceX96 value. This method is borrowed from - * PerpProtocol's `lushan` repo, in lib/PerpMath and used by `getAMMSpotPrice` while generating a - * PRECISE_UNIT vAsset market price - */ - function _formatSqrtPriceX96ToPriceX96(uint160 sqrtPriceX96) internal pure returns (uint256) { - return FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, FixedPoint96.Q96); - } - - /** - * @dev Converts a UniswapV3 X96 format price into a PRECISE_UNIT price. This method is borrowed from - * PerpProtocol's `lushan` repo, in lib/PerpMath and used by `getAMMSpotPrice` while generating a - * PRECISE_UNIT vAsset market price - */ - function _formatX96ToX10_18(uint256 valueX96) internal pure returns (uint256) { - return FullMath.mulDiv(valueX96, 1 ether, FixedPoint96.Q96); - } - - /** - * @dev Converts a uint256 PRECISE_UNIT quote quantity into an alternative decimal format. In Perp all - * assets are 18 decimal quantities we need to represent as 6 decimal USDC quantities when setting - * position units or withdrawing from Perp's Vault contract. - * - * This method is borrowed from PerpProtocol's `lushan` repo in lib/SettlementTokenMath - */ - function _fromPreciseUnitToDecimals(uint256 amount, uint8 decimals) internal pure returns (uint256) { - return amount.div(10**(18 - uint(decimals))); - } - - /** - * @dev Converts an int256 PRECISE_UNIT quote quantity into an alternative decimal format. In Perp all - * assets are 18 decimal quantities we need to represent as 6 decimal USDC quantities when setting - * position units or withdrawing from Perp's Vault contract. - * - * This method is borrowed from PerpProtocol's `lushan` repo in lib/SettlementTokenMath - */ - function _fromPreciseUnitToDecimals(int256 amount, uint8 decimals) internal pure returns (int256) { - return amount.div(int256(10**(18 - uint(decimals)))); - } - - /** - * @dev Converts an arbitrarily decimalized quantity into a PRECISE_UNIT quantity. In Perp the vault - * balance is represented as a 6 decimals USDC quantity which we need to consume in PRECISE_UNIT - * format when calculating values like the external position unit and current leverage. - */ - function _toPreciseUnitsFromDecimals(int256 amount, uint8 decimals) internal pure returns (int256) { - return amount.mul(int256(10**(18 - (uint(decimals))))); - } - - function _abs(int x) internal pure returns (int) { - return x >= 0 ? x : x.mul(-1); - } } diff --git a/test/integration/perpV2LeverageSlippageIssuance.spec.ts b/test/integration/perpV2LeverageSlippageIssuance.spec.ts index e1814b11d..14a48296c 100644 --- a/test/integration/perpV2LeverageSlippageIssuance.spec.ts +++ b/test/integration/perpV2LeverageSlippageIssuance.spec.ts @@ -32,7 +32,7 @@ import { } from "@utils/test/index"; import { PerpV2Fixture, SystemFixture } from "@utils/fixtures"; import { BigNumber } from "ethers"; -import { ADDRESS_ZERO, ZERO } from "@utils/constants"; +import { ADDRESS_ZERO, ZERO, MAX_UINT_256, ZERO_BYTES } from "@utils/constants"; const expect = getWaffleExpect(); @@ -162,6 +162,52 @@ describe("PerpV2LeverageSlippageIssuance", () => { return totalExpectedSlippage; } + async function calculateRedemptionData( + setToken: Address, + redeemQuantity: BigNumber, + usdcTransferOutQuantity: BigNumber + ) { + // Calculate fee adjusted usdcTransferOut + const redeemQuantityWithFees = (await slippageIssuanceModule.calculateTotalFees( + setToken, + redeemQuantity, + false + ))[0]; + + const feeAdjustedTransferOutUSDC = preciseMul(redeemQuantityWithFees, usdcTransferOutQuantity); + + // Calculate realizedPnl. The amount is debited from collateral returned to redeemer *and* + // debited from the Perp account collateral balance because withdraw performs a settlement. + let realizedPnlUSDC = BigNumber.from(0); + const positionUnitInfo = await await perpLeverageModule.getPositionUnitInfo(setToken); + + for (const info of positionUnitInfo) { + const baseTradeQuantityNotional = preciseMul(info.baseUnit, redeemQuantity); + + const { deltaQuote } = await perpSetup.getSwapQuote( + info.baseToken, + baseTradeQuantityNotional, + false + ); + + const { + baseBalance, + quoteBalance + } = (await perpLeverageModule.getPositionNotionalInfo(setToken))[0]; + + const closeRatio = preciseDiv(baseTradeQuantityNotional, baseBalance); + const reducedOpenNotional = preciseMul(quoteBalance, closeRatio); + + realizedPnlUSDC = realizedPnlUSDC.add(toUSDCDecimals(reducedOpenNotional.add(deltaQuote))); + } + + return { + feeAdjustedTransferOutUSDC, + realizedPnlUSDC, + redeemQuantityWithFees + }; + } + describe("#issuance", async () => { let setToken: SetToken; let issueFee: BigNumber; @@ -536,6 +582,67 @@ describe("PerpV2LeverageSlippageIssuance", () => { expect(flashLeverage).to.be.lt(ether(10)); }); }); + + describe("when issuing after a liquidation", async () => { + beforeEach(async () => { + subjectQuantity = ether(1); + + // Calculated leverage = ~8.5X = 8_654_438_822_995_683_587 + await leverUp( + setToken, + perpLeverageModule, + perpSetup, + owner, + baseToken, + 6, + ether(.02), + true + ); + + await perpSetup.setBaseTokenOraclePrice(vETH, usdcUnits(8.0)); + + await perpSetup + .clearingHouse + .connect(otherTrader.wallet) + .liquidate(subjectSetToken, baseToken); + }); + + it("should issue and transfer in the expected amount", async () => { + const initialTotalSupply = await setToken.totalSupply(); + const initialCollateralBalance = (await perpLeverageModule.getAccountInfo(subjectSetToken)).collateralBalance; + + await subject(); + + // We need to calculate this after the subject() fires because it will revert if the positionList + // isn't updated correctly... + const usdcTransferInQuantity = await calculateUSDCTransferIn( + setToken, + subjectQuantity, + perpLeverageModule, + perpSetup + ); + + const finalTotalSupply = await setToken.totalSupply(); + const finalPositionInfo = await perpLeverageModule.getPositionNotionalInfo(subjectSetToken); + const finalCollateralBalance = (await perpLeverageModule.getAccountInfo(subjectSetToken)).collateralBalance; + + const issueQuantityWithFees = (await slippageIssuanceModule.calculateTotalFees( + subjectSetToken, + subjectQuantity, + true + ))[0]; + + const externalPositionUnit = preciseDiv(usdcTransferInQuantity, subjectQuantity); + const feeAdjustedTransferIn = preciseMul(issueQuantityWithFees, externalPositionUnit); + + const expectedTotalSupply = initialTotalSupply.add(issueQuantityWithFees); + const expectedCollateralBalance = toUSDCDecimals(initialCollateralBalance).add(feeAdjustedTransferIn); + + expect(finalTotalSupply).eq(expectedTotalSupply); + expect(finalPositionInfo.length).eq(0); + expect(toUSDCDecimals(finalCollateralBalance)).to.be.closeTo(expectedCollateralBalance, 2); + }); + }); }); }); @@ -674,34 +781,14 @@ describe("PerpV2LeverageSlippageIssuance", () => { let realizedPnlUSDC: BigNumber; beforeEach(async() => { - // Calculate fee adjusted usdcTransferOut - const redeemQuantityWithFees = (await slippageIssuanceModule.calculateTotalFees( + ({ + feeAdjustedTransferOutUSDC, + realizedPnlUSDC + } = await calculateRedemptionData( subjectSetToken, subjectQuantity, - false - ))[0]; - - feeAdjustedTransferOutUSDC = preciseMul(redeemQuantityWithFees, usdcTransferOutQuantity); - - // Calculate realizedPnl, which is negative in this case. The amount is debited from collateral - // returned to redeemer *and* debited from the Perp account collateral balance because - // withdraw performs a settlement. - const baseUnit = (await perpLeverageModule.getPositionUnitInfo(subjectSetToken))[0].baseUnit; - const baseTradeQuantityNotional = preciseMul(baseUnit, subjectQuantity); - const { deltaQuote } = await perpSetup.getSwapQuote( - baseToken, - baseTradeQuantityNotional, - false - ); - - const { - baseBalance, - quoteBalance - } = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0]; - const closeRatio = preciseDiv(baseTradeQuantityNotional, baseBalance); - const reducedOpenNotional = preciseMul(quoteBalance, closeRatio); - - realizedPnlUSDC = toUSDCDecimals(reducedOpenNotional.add(deltaQuote)); + usdcTransferOutQuantity + )); }); it("should withdraw the expected amount from the Perp vault", async () => { @@ -865,34 +952,14 @@ describe("PerpV2LeverageSlippageIssuance", () => { let realizedPnlUSDC: BigNumber; beforeEach(async() => { - // Calculate fee adjusted usdcTransferOut - const redeemQuantityWithFees = (await slippageIssuanceModule.calculateTotalFees( + ({ + feeAdjustedTransferOutUSDC, + realizedPnlUSDC + } = await calculateRedemptionData( subjectSetToken, subjectQuantity, - false - ))[0]; - - feeAdjustedTransferOutUSDC = preciseMul(redeemQuantityWithFees, usdcTransferOutQuantity); - - // Calculate realizedPnl, which is negative in this case. The amount is debited from collateral - // returned to redeemer *and* debited from the Perp account collateral balance because - // withdraw performs a settlement. - const baseUnit = (await perpLeverageModule.getPositionUnitInfo(subjectSetToken))[0].baseUnit; - const baseTradeQuantityNotional = preciseMul(baseUnit, subjectQuantity); - const { deltaQuote } = await perpSetup.getSwapQuote( - baseToken, - baseTradeQuantityNotional, - false + usdcTransferOutQuantity) ); - - const { - baseBalance, - quoteBalance - } = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0]; - const closeRatio = preciseDiv(baseTradeQuantityNotional, baseBalance); - const reducedOpenNotional = preciseMul(quoteBalance, closeRatio); - - realizedPnlUSDC = toUSDCDecimals(reducedOpenNotional.add(deltaQuote)); }); it("should withdraw the expected amount from the Perp vault", async () => { @@ -950,9 +1017,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { ZERO ); - const { - baseUnit: finalBaseUnit - } = (await perpLeverageModule.getPositionUnitInfo(subjectSetToken))[0]; + const positionInfo = await perpLeverageModule.getPositionUnitInfo(subjectSetToken); // Withdraw remaining free collateral const freeCollateral = await perpSetup.vault.getFreeCollateral(subjectSetToken); @@ -975,7 +1040,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { const finalModules = await setToken.getModules(); expect(finalModules.includes(perpLeverageModule.address)).eq(false); - expect(finalBaseUnit).eq(ZERO); + expect(positionInfo.length).eq(0); expect(toUSDCDecimals(finalCollateralBalance)).eq(1); // <-- DUST // Restore module @@ -990,6 +1055,127 @@ describe("PerpV2LeverageSlippageIssuance", () => { await perpLeverageModule.deposit(setToken.address, usdcUnits(5)); }); }); + + describe("when redeeming after a liquidation", async () => { + beforeEach(async () => { + subjectQuantity = ether(1); + + // Calculated leverage = ~8.5X = 8_654_438_822_995_683_587 + await leverUp( + setToken, + perpLeverageModule, + perpSetup, + owner, + baseToken, + 6, + ether(.02), + true + ); + + await perpSetup.setBaseTokenOraclePrice(vETH, usdcUnits(8.0)); + + await perpSetup + .clearingHouse + .connect(otherTrader.wallet) + .liquidate(subjectSetToken, baseToken); + }); + + it("should redeem and transfer out the expected amount", async () => { + const initialTotalSupply = await setToken.totalSupply(); + const initialCollateralBalance = (await perpLeverageModule.getAccountInfo(subjectSetToken)).collateralBalance; + + // Total amount of owedRealizedPnl will be debited from collateral balance + const { owedRealizedPnl } = await perpLeverageModule.getAccountInfo(subjectSetToken); + const owedRealizedPnlUSDC = toUSDCDecimals(owedRealizedPnl); + + await subject(); + + const finalTotalSupply = await setToken.totalSupply(); + const finalPositionInfo = await perpLeverageModule.getPositionNotionalInfo(subjectSetToken); + const finalCollateralBalance = (await perpLeverageModule.getAccountInfo(subjectSetToken)).collateralBalance; + + const usdcTransferOutQuantity = await calculateUSDCTransferOut( + setToken, + subjectQuantity, + perpLeverageModule, + perpSetup + ); + + const { + feeAdjustedTransferOutUSDC, + redeemQuantityWithFees + } = await calculateRedemptionData( + subjectSetToken, + subjectQuantity, + usdcTransferOutQuantity + ); + + const expectedTotalSupply = initialTotalSupply.sub(redeemQuantityWithFees); + const expectedCollateralBalance = toUSDCDecimals(initialCollateralBalance) + .sub(feeAdjustedTransferOutUSDC) + .add(owedRealizedPnlUSDC); + + expect(finalTotalSupply).eq(expectedTotalSupply); + expect(finalPositionInfo.length).eq(0); + expect(toUSDCDecimals(finalCollateralBalance)).to.be.closeTo(expectedCollateralBalance, 2); + }); + }); + + describe("when liquidation results in negative account value", () => { + beforeEach(async () => { + // Calculated leverage = ~8.5X = 8_654_438_822_995_683_587 + await leverUp( + setToken, + perpLeverageModule, + perpSetup, + owner, + baseToken, + 6, + ether(.02), + true + ); + + // Move oracle price down to 5 USDC to enable liquidation + await perpSetup.setBaseTokenOraclePrice(vETH, usdcUnits(5.0)); + + // Move price down by maker selling 20k USDC of vETH + // Post trade spot price rises from ~10 USDC to 6_370_910_537_702_299_856 + await perpSetup.clearingHouse.connect(maker.wallet).openPosition({ + baseToken: vETH.address, + isBaseToQuote: true, // short + isExactInput: false, // `amount` is USDC + amount: ether(20000), + oppositeAmountBound: ZERO, + deadline: MAX_UINT_256, + sqrtPriceLimitX96: ZERO, + referralCode: ZERO_BYTES + }); + + await perpSetup + .clearingHouse + .connect(otherTrader.wallet) + .liquidate(subjectSetToken, baseToken); + }); + + it("should be possible to remove the module", async () => { + await subject(); + + const collateralBalance = await perpSetup.vault.getBalance(subjectSetToken); + const freeCollateral = await perpSetup.vault.getFreeCollateral(subjectSetToken); + const accountValue = await perpSetup.clearingHouse.getAccountValue(subjectSetToken); + + // collateralBalance: 20_100_000 (10^6) + // accountValue: -43_466_857_276_051_287_954 (10^18) + expect(collateralBalance).gt(1); + expect(freeCollateral).eq(0); + expect(accountValue).lt(-1); + + /// Remove module + await setToken.removeModule(perpLeverageModule.address); + const finalModules = await setToken.getModules(); + expect(finalModules.includes(perpLeverageModule.address)).eq(false); + }); + }); }); }); }); diff --git a/test/lib/preciseUnitMath.spec.ts b/test/lib/preciseUnitMath.spec.ts index 347a6bc9f..4db406deb 100644 --- a/test/lib/preciseUnitMath.spec.ts +++ b/test/lib/preciseUnitMath.spec.ts @@ -30,6 +30,7 @@ describe("PreciseUnitMath", () => { // Used to make sure rounding is done correctly, 1020408168544454473 const preciseNumber = BigNumber.from("0x0e2937d2abffc749"); + const negativePreciseNumber = preciseNumber.mul(-1); before(async () => { [ @@ -236,6 +237,93 @@ describe("PreciseUnitMath", () => { }); }); + describe("#preciseDivCeil: int256", async () => { + let subjectA: BigNumber; + let subjectB: BigNumber; + + async function subject(): Promise { + return mathMock.preciseDivCeilInt(subjectA, subjectB); + } + + describe("when a and b are positive", () => { + beforeEach(async () => { + subjectA = preciseNumber; + subjectB = ether(.3); + }); + + it("returns the correct number", async () => { + const division = await subject(); + + const expectedDivision = preciseDivCeilInt(subjectA, subjectB); + expect(division).to.eq(expectedDivision); + }); + }); + + describe("when a and b are negative", () => { + beforeEach(async () => { + subjectA = negativePreciseNumber; + subjectB = ether(-.3); + }); + + it("returns the correct number", async () => { + const division = await subject(); + + const expectedDivision = preciseDivCeilInt(subjectA, subjectB); + expect(division).to.eq(expectedDivision); + }); + }); + + describe("when a is positive and b is negative", () => { + beforeEach(async () => { + subjectA = preciseNumber; + subjectB = ether(-.3); + }); + + it("returns the correct number", async () => { + const division = await subject(); + + const expectedDivision = preciseDivCeilInt(subjectA, subjectB); + expect(division).to.eq(expectedDivision); + }); + }); + + describe("when a is negative and b is positive", () => { + beforeEach(async () => { + subjectA = negativePreciseNumber; + subjectB = ether(.3); + }); + + it("returns the correct number", async () => { + const division = await subject(); + + const expectedDivision = preciseDivCeilInt(subjectA, subjectB); + expect(division).to.eq(expectedDivision); + }); + }); + + describe("when a is 0", async () => { + beforeEach(async () => { + subjectA = ZERO; + }); + + it("should return 0", async () => { + const division = await subject(); + expect(division).to.eq(ZERO); + }); + }); + + describe("when b is 0", async () => { + beforeEach(async () => { + subjectA = ZERO; + subjectB = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Cant divide by 0"); + }); + }); + }); + describe("#divDown: int256", async () => { let subjectA: BigNumber; let subjectB: BigNumber; @@ -524,4 +612,51 @@ describe("PreciseUnitMath", () => { }); }); }); + + describe("#abs", async () => { + let subjectA: BigNumber; + + async function subject(): Promise { + return mathMock.abs(subjectA); + } + + describe("when a is positive", () => { + beforeEach(() => { + subjectA = BigNumber.from(5); + }); + + it("returns the correct number", async () => { + const absoluteValue = await subject(); + + const expectedAbsoluteValue = subjectA; + expect(absoluteValue).to.eq(expectedAbsoluteValue); + }); + }); + + describe("when a is negative", () => { + beforeEach(() => { + subjectA = BigNumber.from(-5); + }); + + it("returns the correct number", async () => { + const absoluteValue = await subject(); + + const expectedAbsoluteValue = subjectA.mul(-1); + expect(absoluteValue).to.eq(expectedAbsoluteValue); + }); + }); + + describe("when a is zero", () => { + beforeEach(() => { + subjectA = ZERO; + }); + + it("returns zero", async () => { + const absoluteValue = await subject(); + + const expectedAbsoluteValue = ZERO; + expect(absoluteValue).to.eq(expectedAbsoluteValue); + }); + }); + }); }); diff --git a/test/lib/unitConversionUtils.spec.ts b/test/lib/unitConversionUtils.spec.ts new file mode 100644 index 000000000..3542d4296 --- /dev/null +++ b/test/lib/unitConversionUtils.spec.ts @@ -0,0 +1,111 @@ +import "module-alias/register"; + +import { BigNumber } from "ethers"; + +import { Account } from "@utils/test/types"; +import { UnitConversionUtilsMock } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getWaffleExpect, +} from "@utils/test/index"; + +import { ether, usdc } from "@utils/index"; + +const expect = getWaffleExpect(); + +describe("UnitConversionUtils", () => { + let owner: Account; + let deployer: DeployHelper; + let quantity: number; + let usdcDecimals: number; + + let unitConversionUtilsMock: UnitConversionUtilsMock; + + before(async () => { + [ + owner, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + unitConversionUtilsMock = await deployer.mocks.deployUnitConversionUtilsMock(); + + quantity = 5; + usdcDecimals = 6; + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#fromPreciseUnitToDecimals (int)", async () => { + let subjectPreciseUnitQuantity: BigNumber; + let subjectDecimals: number; + + async function subject(): Promise{ + return await unitConversionUtilsMock.testFromPreciseUnitToDecimalsInt( + subjectPreciseUnitQuantity, + subjectDecimals + ); + }; + + beforeEach(() => { + subjectPreciseUnitQuantity = ether(quantity); + subjectDecimals = usdcDecimals; + }); + + it("should convert from precise unit value to decimals value correctly", async () => { + const expectedValue = usdc(quantity); + const actualValue = await subject(); + + expect(actualValue).eq(expectedValue); + }); + }); + + describe("#fromPreciseUnitToDecimals (uint)", async () => { + let subjectPreciseUnitQuantity: BigNumber; + let subjectDecimals: number; + + async function subject(): Promise{ + return await unitConversionUtilsMock.testFromPreciseUnitToDecimalsUint( + subjectPreciseUnitQuantity, + subjectDecimals + ); + }; + + beforeEach(() => { + subjectPreciseUnitQuantity = ether(quantity); + subjectDecimals = usdcDecimals; + }); + + it("should convert from precise unit value to decimals value correctly", async () => { + const expectedValue = usdc(quantity); + const actualValue = await subject(); + + expect(actualValue).eq(expectedValue); + }); + }); + + describe("#fromPreciseUnitToDecimals (int)", async () => { + let subjectDecimalsQuantity: BigNumber; + let subjectDecimals: number; + + async function subject(): Promise{ + return await unitConversionUtilsMock.testToPreciseUnitsFromDecimalsInt( + subjectDecimalsQuantity, + subjectDecimals + ); + }; + + beforeEach(() => { + subjectDecimalsQuantity = usdc(quantity); + subjectDecimals = usdcDecimals; + }); + + it("should convert to precise unit value from decimals value correctly", async () => { + const expectedValue = ether(quantity); + const actualValue = await subject(); + + expect(actualValue).eq(expectedValue); + }); + }); +}); diff --git a/test/protocol/integration/lib/uniswapV3Math.spec.ts b/test/protocol/integration/lib/uniswapV3Math.spec.ts new file mode 100644 index 000000000..86d39f5bf --- /dev/null +++ b/test/protocol/integration/lib/uniswapV3Math.spec.ts @@ -0,0 +1,74 @@ +import "module-alias/register"; + +import { BigNumber } from "ethers"; + +import { Account } from "@utils/test/types"; +import { UniswapV3MathMock } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getSystemFixture, + getWaffleExpect, + getUniswapV3Fixture, + getPerpV2Fixture +} from "@utils/test/index"; + +import { SystemFixture, UniswapV3Fixture, PerpV2Fixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("UniswapV3MathLib", () => { + let owner: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + let uniswapV3Fixture: UniswapV3Fixture; + let perpV2Fixture: PerpV2Fixture; + + let uniswapV3MathMock: UniswapV3MathMock; + + before(async () => { + [ + owner, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + perpV2Fixture = getPerpV2Fixture(owner.address); + uniswapV3Fixture = getUniswapV3Fixture(owner.address); + await uniswapV3Fixture.initialize( + owner, + setup.weth, + 2500, + setup.wbtc, + 35000, + setup.dai + ); + + uniswapV3MathMock = await deployer.mocks.deployUniswapV3MathMock(); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("integration test for #formatSqrtPriceX96ToPriceX96, #formatX96ToX10_18", async () => { + let subjectSqrtPriceX96: BigNumber; + + beforeEach(async () => { + subjectSqrtPriceX96 = (await uniswapV3Fixture.wethWbtcPool.slot0()).sqrtPriceX96; + }); + + async function subject(): Promise { + const priceX86 = await uniswapV3MathMock.testFormatSqrtPriceX96ToPriceX96(subjectSqrtPriceX96); + return uniswapV3MathMock.testFormatX96ToX10_18(priceX86); + }; + + it("should format UniswapV3 pool sqrt price correctly", async () => { + const expectedPrice = await perpV2Fixture.getPriceFromSqrtPriceX96(subjectSqrtPriceX96); + const price = await subject(); + + expect(price).eq(expectedPrice); + }); + }); +}); diff --git a/test/protocol/lib/setTokenAccessible.spec.ts b/test/protocol/lib/setTokenAccessible.spec.ts new file mode 100644 index 000000000..41be58f86 --- /dev/null +++ b/test/protocol/lib/setTokenAccessible.spec.ts @@ -0,0 +1,222 @@ +import "module-alias/register"; + +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { SetTokenAccessibleMock, SetToken } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { ether } from "@utils/index"; +import { + getAccounts, + getRandomAddress, + getRandomAccount, + getSystemFixture, + getWaffleExpect, + addSnapshotBeforeRestoreAfterEach, +} from "@utils/test/index"; +import { SystemFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("SetTokenAccessible", () => { + let owner: Account; + let deployer: DeployHelper; + let setToken: SetToken; + + let setup: SystemFixture; + let setTokenAccessible: SetTokenAccessibleMock; + + before(async () => { + [ owner ] = await getAccounts(); + + setup = getSystemFixture(owner.address); + await setup.initialize(); + deployer = new DeployHelper(owner.wallet); + + setTokenAccessible = await deployer.mocks.deploySetTokenAccessibleMock(setup.controller.address); + + await setup.controller.addModule(setTokenAccessible.address); + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [setTokenAccessible.address], + owner.address, + ); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#updateAllowedSetToken", async () => { + let subjectSetToken: Address; + let subjectStatus: boolean; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = await setToken.address; + subjectStatus = true; + subjectCaller = owner; + }); + + async function subject(): Promise { + return setTokenAccessible.connect(subjectCaller.wallet).updateAllowedSetToken( + subjectSetToken, + subjectStatus + ); + } + + it("should add Set to allow list", async () => { + await subject(); + + const isAllowed = await setTokenAccessible.allowedSetTokens(subjectSetToken); + + expect(isAllowed).to.be.true; + }); + + it("should emit the correct SetTokenStatusUpdated event", async () => { + await expect(subject()).to.emit(setTokenAccessible, "SetTokenStatusUpdated").withArgs( + subjectSetToken, + subjectStatus + ); + }); + + describe("when disabling a Set", async () => { + beforeEach(async () => { + await subject(); + subjectStatus = false; + }); + + it("should remove Set from allow list", async () => { + await subject(); + + const isAllowed = await setTokenAccessible.allowedSetTokens(subjectSetToken); + + expect(isAllowed).to.be.false; + }); + + it("should emit the correct SetTokenStatusUpdated event", async () => { + await expect(subject()).to.emit(setTokenAccessible, "SetTokenStatusUpdated").withArgs( + subjectSetToken, + subjectStatus + ); + }); + + describe("when Set Token is removed on controller", async () => { + beforeEach(async () => { + await setup.controller.removeSet(setToken.address); + }); + + it("should remove the Set from allow list", async () => { + await subject(); + + const isAllowed = await setTokenAccessible.allowedSetTokens(subjectSetToken); + + expect(isAllowed).to.be.false; + }); + }); + }); + + describe("when Set is removed on controller", async () => { + beforeEach(async () => { + await setup.controller.removeSet(setToken.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Invalid SetToken"); + }); + }); + + describe("when not called by owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + }); + + describe("#updateAnySetAllowed", async () => { + let subjectAnySetAllowed: boolean; + let subjectCaller: Account; + + beforeEach(async () => { + subjectAnySetAllowed = true; + subjectCaller = owner; + }); + + async function subject(): Promise { + return setTokenAccessible.connect(subjectCaller.wallet).updateAnySetAllowed(subjectAnySetAllowed); + } + + it("should update anySetAllowed to true", async () => { + await subject(); + + const anySetAllowed = await setTokenAccessible.anySetAllowed(); + + expect(anySetAllowed).to.be.true; + }); + + it("should emit the correct AnySetAllowedUpdated event", async () => { + await expect(subject()).to.emit(setTokenAccessible, "AnySetAllowedUpdated").withArgs( + subjectAnySetAllowed + ); + }); + + describe("when not called by owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + }); + + describe("#onlyAllowedSet", async () => { + let subjectCaller: Account; + let subjectSetToken: Address; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return setTokenAccessible.connect(subjectCaller.wallet).testOnlyAllowedSet(subjectSetToken); + } + + describe("when anySetAllowed is true", () => { + beforeEach(async () => { + await setTokenAccessible.connect(subjectCaller.wallet).updateAnySetAllowed(true); + }); + + it("should be ok", async () => { + await subject(); + }); + }); + + describe("when anySetAllowed is false and specific set is allowed", () => { + beforeEach(async () => { + await setTokenAccessible.connect(subjectCaller.wallet).updateAnySetAllowed(false); + await setTokenAccessible.updateAllowedSetToken(subjectSetToken, true); + }); + + it("should be ok", async () => { + await subject(); + }); + }); + + describe("when anySetAllowed is false and specific set is not allowed", () => { + beforeEach(async () => { + subjectSetToken = await getRandomAddress(); + await setTokenAccessible.connect(subjectCaller.wallet).updateAnySetAllowed(false); + await setTokenAccessible.allowedSetTokens(subjectSetToken); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Not allowed SetToken"); + }); + }); + }); +}); diff --git a/test/protocol/modules/perpV2LeverageModule.spec.ts b/test/protocol/modules/perpV2LeverageModule.spec.ts index 6596df9f7..52c78e158 100644 --- a/test/protocol/modules/perpV2LeverageModule.spec.ts +++ b/test/protocol/modules/perpV2LeverageModule.spec.ts @@ -383,7 +383,7 @@ describe("PerpV2LeverageModule", () => { let subjectCaller: Account; let subjectBaseToken: Address; let subjectBaseTradeQuantityUnits: BigNumber; - let subjectQuoteReceiveQuantityUnits: BigNumber; + let subjectQuoteBoundQuantityUnits: BigNumber; const initializeContracts = async () => { depositQuantity = usdcUnits(10); @@ -404,7 +404,7 @@ describe("PerpV2LeverageModule", () => { subjectSetToken, subjectBaseToken, subjectBaseTradeQuantityUnits, - subjectQuoteReceiveQuantityUnits + subjectQuoteBoundQuantityUnits ); } @@ -414,7 +414,7 @@ describe("PerpV2LeverageModule", () => { beforeEach(async () => { // Long ~10 USDC of vETH subjectBaseTradeQuantityUnits = ether(1); - subjectQuoteReceiveQuantityUnits = ether(10.15); + subjectQuoteBoundQuantityUnits = ether(10.15); }); it("should open the expected position", async () => { @@ -435,10 +435,10 @@ describe("PerpV2LeverageModule", () => { expect(finalPositionInfo.quoteBalance).lt(0); expect(finalPositionInfo.baseBalance).eq(expectedBaseBalance); expect(finalPositionInfo.quoteBalance).eq(expectedQuoteBalance.mul(-1)); - expect(finalPositionInfo.quoteBalance.mul(-1)).lt(subjectQuoteReceiveQuantityUnits); + expect(finalPositionInfo.quoteBalance.mul(-1)).lt(subjectQuoteBoundQuantityUnits); }); - it("should emit the correct PerpTrade event", async () => { + it("should emit the correct PerpTraded event", async () => { const { deltaBase: expectedDeltaBase, deltaQuote: expectedDeltaQuote @@ -447,7 +447,7 @@ describe("PerpV2LeverageModule", () => { const expectedProtocolFee = ether(0); const expectedIsBuy = true; - await expect(subject()).to.emit(perpLeverageModule, "PerpTrade").withArgs( + await expect(subject()).to.emit(perpLeverageModule, "PerpTraded").withArgs( subjectSetToken, subjectBaseToken, expectedDeltaBase, @@ -478,13 +478,13 @@ describe("PerpV2LeverageModule", () => { beforeEach(async () => { // Long ~20 USDC of vETH with 10 USDC collateral subjectBaseTradeQuantityUnits = ether(2); - subjectQuoteReceiveQuantityUnits = ether(20.3); + subjectQuoteBoundQuantityUnits = ether(20.3); }); it("should open expected position", async () => { const totalSupply = await setToken.totalSupply(); const collateralBalance = (await perpLeverageModule.getAccountInfo(subjectSetToken)).collateralBalance; - const quoteBalanceMin = preciseMul(subjectQuoteReceiveQuantityUnits, totalSupply); + const quoteBalanceMin = preciseMul(subjectQuoteBoundQuantityUnits, totalSupply); const expectedQuoteBalance = (await perpSetup.getSwapQuote(subjectBaseToken, subjectBaseTradeQuantityUnits, true)).deltaQuote; @@ -522,7 +522,7 @@ describe("PerpV2LeverageModule", () => { subjectSetToken = otherSetToken.address; subjectBaseTradeQuantityUnits = ether(1); - subjectQuoteReceiveQuantityUnits = ether(10.15); + subjectQuoteBoundQuantityUnits = ether(10.15); }); it("should open position for the expected amount", async () => { @@ -542,7 +542,7 @@ describe("PerpV2LeverageModule", () => { beforeEach(async () => { // Long ~10 USDC of vETH: slippage incurred as larger negative quote delta subjectBaseTradeQuantityUnits = ether(1); - subjectQuoteReceiveQuantityUnits = ether(10); + subjectQuoteBoundQuantityUnits = ether(10); }); it("should revert", async () => { @@ -554,13 +554,13 @@ describe("PerpV2LeverageModule", () => { describe("when an existing position is long", async () => { beforeEach(async () => { subjectBaseTradeQuantityUnits = ether(1); - subjectQuoteReceiveQuantityUnits = ether(10.15); + subjectQuoteBoundQuantityUnits = ether(10.15); await perpLeverageModule.connect(subjectCaller.wallet).trade( subjectSetToken, subjectBaseToken, subjectBaseTradeQuantityUnits, - subjectQuoteReceiveQuantityUnits + subjectQuoteBoundQuantityUnits ); }); @@ -590,7 +590,7 @@ describe("PerpV2LeverageModule", () => { ); subjectBaseTradeQuantityUnits = ether(.5); - subjectQuoteReceiveQuantityUnits = ether(5.15); + subjectQuoteBoundQuantityUnits = ether(5.15); }); it("long trade should reduce the position", async () => { @@ -624,7 +624,7 @@ describe("PerpV2LeverageModule", () => { describe("when the position is zeroed out", async () => { beforeEach(async () => { subjectBaseTradeQuantityUnits = ether(1); - subjectQuoteReceiveQuantityUnits = ether(10.15); + subjectQuoteBoundQuantityUnits = ether(10.15); }); it("should remove the position from the positions array", async () => { @@ -653,7 +653,7 @@ describe("PerpV2LeverageModule", () => { // Long ~10 USDC of vETH subjectBaseTradeQuantityUnits = ether(1); - subjectQuoteReceiveQuantityUnits = ether(10.15); + subjectQuoteBoundQuantityUnits = ether(10.15); }); it("should withdraw the expected collateral amount from the Perp vault", async () => { @@ -698,7 +698,7 @@ describe("PerpV2LeverageModule", () => { expect(initialUSDCDefaultPositionUnit).to.eq(finalUSDCDefaultPositionUnit); }); - it("should emit the correct PerpTrade event", async () => { + it("should emit the correct PerpTraded event", async () => { const { deltaBase: expectedDeltaBase, deltaQuote: expectedDeltaQuote @@ -707,7 +707,7 @@ describe("PerpV2LeverageModule", () => { const expectedProtocolFee = toUSDCDecimals(preciseMul(expectedDeltaQuote, feePercentage)); const expectedIsBuy = true; - await expect(subject()).to.emit(perpLeverageModule, "PerpTrade").withArgs( + await expect(subject()).to.emit(perpLeverageModule, "PerpTraded").withArgs( subjectSetToken, subjectBaseToken, expectedDeltaBase, @@ -727,13 +727,25 @@ describe("PerpV2LeverageModule", () => { await expect(subject()).to.be.revertedWith("Amount is 0"); }); }); + + describe("when baseToken does not exist in Perp system", async () => { + beforeEach(async () => { + subjectBaseTradeQuantityUnits = ether(1); + subjectQuoteBoundQuantityUnits = ether(10.15); + subjectBaseToken = await getRandomAddress(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Base token does not exist"); + }); + }); }); describe("when short", () => { beforeEach(async () => { // Short ~10 USDC of vETH subjectBaseTradeQuantityUnits = ether(-1); - subjectQuoteReceiveQuantityUnits = ether(9.85); + subjectQuoteBoundQuantityUnits = ether(9.85); }); it("should open the expected position", async () => { @@ -755,10 +767,10 @@ describe("PerpV2LeverageModule", () => { expect(finalPositionInfo.quoteBalance).gt(0); expect(finalPositionInfo.baseBalance).eq(expectedBaseBalance); expect(finalPositionInfo.quoteBalance).eq(expectedQuoteBalance); - expect(finalPositionInfo.quoteBalance).gt(subjectQuoteReceiveQuantityUnits); + expect(finalPositionInfo.quoteBalance).gt(subjectQuoteBoundQuantityUnits); }); - it("should emit the correct PerpTrade event", async () => { + it("should emit the correct PerpTraded event", async () => { const { deltaBase: expectedDeltaBase, deltaQuote: expectedDeltaQuote @@ -767,7 +779,7 @@ describe("PerpV2LeverageModule", () => { const expectedProtocolFee = ether(0); const expectedIsBuy = false; - await expect(subject()).to.emit(perpLeverageModule, "PerpTrade").withArgs( + await expect(subject()).to.emit(perpLeverageModule, "PerpTraded").withArgs( subjectSetToken, subjectBaseToken, expectedDeltaBase, @@ -788,7 +800,7 @@ describe("PerpV2LeverageModule", () => { // Partial close subjectBaseTradeQuantityUnits = ether(-.5); - subjectQuoteReceiveQuantityUnits = ether(4.85); + subjectQuoteBoundQuantityUnits = ether(4.85); }); it("short trade should reduce the position", async () => { @@ -822,7 +834,7 @@ describe("PerpV2LeverageModule", () => { describe("when the position is zeroed out", async () => { beforeEach(async () => { subjectBaseTradeQuantityUnits = ether(-1); - subjectQuoteReceiveQuantityUnits = ether(9.85); + subjectQuoteBoundQuantityUnits = ether(9.85); }); it("should remove the position from the positions array", async () => { @@ -877,7 +889,7 @@ describe("PerpV2LeverageModule", () => { // Short ~10 USDC of vETH subjectBaseTradeQuantityUnits = ether(-1); - subjectQuoteReceiveQuantityUnits = ether(9.85); + subjectQuoteBoundQuantityUnits = ether(9.85); }); it("should withdraw the expected collateral amount from the Perp vault", async () => { @@ -904,7 +916,7 @@ describe("PerpV2LeverageModule", () => { beforeEach(async () => { // Short ~10 USDC of vETH, slippage incurred as smaller positive quote delta subjectBaseTradeQuantityUnits = ether(-1); - subjectQuoteReceiveQuantityUnits = ether(10); + subjectQuoteBoundQuantityUnits = ether(10); }); it("should revert", async () => { @@ -3080,6 +3092,50 @@ describe("PerpV2LeverageModule", () => { }); }); + describe("when there is no external USDC position", () => { + let otherSetToken: SetToken; + + beforeEach(async () => { + otherSetToken = await setup.createSetToken( + [usdc.address], + [usdcUnits(10)], + [perpLeverageModule.address, debtIssuanceMock.address, setup.issuanceModule.address] + ); + + await debtIssuanceMock.initialize(otherSetToken.address); + await perpLeverageModule.updateAllowedSetToken(otherSetToken.address, true); + + await perpLeverageModule.connect(owner.wallet).initialize(otherSetToken.address); + + await otherSetToken.addModule(mockModule.address); + await otherSetToken.connect(mockModule.wallet).initializeModule(); + + // Issue to create some supply + await usdc.approve(setup.issuanceModule.address, usdcUnits(1000)); + await setup.issuanceModule.initialize(otherSetToken.address, ADDRESS_ZERO); + await setup.issuanceModule.issue(otherSetToken.address, ether(1), owner.address); + + subjectSetToken = otherSetToken.address; + }); + + it("should not update the externalPositionUnit", async () => { + const initialExternalPositionUnit = await otherSetToken.getExternalPositionRealUnit( + usdc.address, + perpLeverageModule.address + ); + + await subject(); + + const finalExternalPositionUnit = await otherSetToken.getExternalPositionRealUnit( + usdc.address, + perpLeverageModule.address + ); + + expect(initialExternalPositionUnit).eq(ZERO); + expect(initialExternalPositionUnit).eq(finalExternalPositionUnit); + }); + }); + describe("when caller is not module", async () => { beforeEach(async () => { subjectCaller = owner; @@ -4132,6 +4188,50 @@ describe("PerpV2LeverageModule", () => { }); }); + describe("when there is no external USDC position", () => { + let otherSetToken: SetToken; + + beforeEach(async () => { + otherSetToken = await setup.createSetToken( + [usdc.address], + [usdcUnits(10)], + [perpLeverageModule.address, debtIssuanceMock.address, setup.issuanceModule.address] + ); + + await debtIssuanceMock.initialize(otherSetToken.address); + await perpLeverageModule.updateAllowedSetToken(otherSetToken.address, true); + + await perpLeverageModule.connect(owner.wallet).initialize(otherSetToken.address); + + await otherSetToken.addModule(mockModule.address); + await otherSetToken.connect(mockModule.wallet).initializeModule(); + + // Issue to create some supply + await usdc.approve(setup.issuanceModule.address, usdcUnits(1000)); + await setup.issuanceModule.initialize(otherSetToken.address, ADDRESS_ZERO); + await setup.issuanceModule.issue(otherSetToken.address, ether(2), owner.address); + + subjectSetToken = otherSetToken.address; + }); + + it("should not update the externalPositionUnit", async () => { + const initialExternalPositionUnit = await otherSetToken.getExternalPositionRealUnit( + usdc.address, + perpLeverageModule.address + ); + + await subject(); + + const finalExternalPositionUnit = await otherSetToken.getExternalPositionRealUnit( + usdc.address, + perpLeverageModule.address + ); + + expect(initialExternalPositionUnit).eq(ZERO); + expect(initialExternalPositionUnit).eq(finalExternalPositionUnit); + }); + }); + describe("when caller is not module", async () => { beforeEach(async () => subjectCaller = owner); @@ -4591,149 +4691,13 @@ describe("PerpV2LeverageModule", () => { expect(isRegistered).to.be.false; }); - describe("when collateral balance exists", async () => { + describe("when the account balance is positive", async () => { beforeEach(async () => { await perpLeverageModule.deposit(setToken.address, usdcUnits(10)); }); it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Collateral balance remaining"); - }); - }); - }); - - describe("#updateAllowedSetToken", async () => { - let setToken: SetToken; - - let subjectSetToken: Address; - let subjectStatus: boolean; - let subjectCaller: Account; - - beforeEach(async () => { - setToken = setToken = await setup.createSetToken( - [usdc.address], - [ether(2)], - [perpLeverageModule.address, debtIssuanceMock.address] - ); - - subjectSetToken = setToken.address; - subjectStatus = true; - subjectCaller = owner; - }); - - async function subject(): Promise { - return perpLeverageModule.connect(subjectCaller.wallet).updateAllowedSetToken( - subjectSetToken, - subjectStatus - ); - } - - it("should add Set to allow list", async () => { - await subject(); - - const isAllowed = await perpLeverageModule.allowedSetTokens(subjectSetToken); - - expect(isAllowed).to.be.true; - }); - - it("should emit the correct SetTokenStatusUpdated event", async () => { - await expect(subject()).to.emit(perpLeverageModule, "SetTokenStatusUpdated").withArgs( - subjectSetToken, - subjectStatus - ); - }); - - describe("when disabling a Set", async () => { - beforeEach(async () => { - await subject(); - subjectStatus = false; - }); - - it("should remove Set from allow list", async () => { - await subject(); - - const isAllowed = await perpLeverageModule.allowedSetTokens(subjectSetToken); - - expect(isAllowed).to.be.false; - }); - - it("should emit the correct SetTokenStatusUpdated event", async () => { - await expect(subject()).to.emit(perpLeverageModule, "SetTokenStatusUpdated").withArgs( - subjectSetToken, - subjectStatus - ); - }); - - describe("when Set Token is removed on controller", async () => { - beforeEach(async () => { - await setup.controller.removeSet(setToken.address); - }); - - it("should remove the Set from allow list", async () => { - await subject(); - - const isAllowed = await perpLeverageModule.allowedSetTokens(subjectSetToken); - - expect(isAllowed).to.be.false; - }); - }); - }); - - describe("when Set is removed on controller", async () => { - beforeEach(async () => { - await setup.controller.removeSet(setToken.address); - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Invalid SetToken"); - }); - }); - - describe("when not called by owner", async () => { - beforeEach(async () => { - subjectCaller = await getRandomAccount(); - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - }); - }); - - describe("#updateAnySetAllowed", async () => { - let subjectAnySetAllowed: boolean; - let subjectCaller: Account; - - beforeEach(async () => { - subjectAnySetAllowed = true; - subjectCaller = owner; - }); - - async function subject(): Promise { - return perpLeverageModule.connect(subjectCaller.wallet).updateAnySetAllowed(subjectAnySetAllowed); - } - - it("should update anySetAllowed to true", async () => { - await subject(); - - const anySetAllowed = await perpLeverageModule.anySetAllowed(); - - expect(anySetAllowed).to.be.true; - }); - - it("should emit the correct AnySetAllowedUpdated event", async () => { - await expect(subject()).to.emit(perpLeverageModule, "AnySetAllowedUpdated").withArgs( - subjectAnySetAllowed - ); - }); - - describe("when not called by owner", async () => { - beforeEach(async () => { - subjectCaller = await getRandomAccount(); - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + await expect(subject()).to.be.revertedWith("Account balance is positive"); }); }); }); @@ -5047,21 +5011,21 @@ describe("PerpV2LeverageModule", () => { true )); - const vETHQuoteReceiveQuantityUnits = ether(10.15); - const vBTCQuoteReceiveQuantityUnits = ether(101); + const vETHQuoteBoundQuantityUnits = ether(10.15); + const vBTCQuoteBoundQuantityUnits = ether(101); await perpLeverageModule.connect(owner.wallet).trade( subjectSetToken, expectedVETHToken, vethTradeQuantityUnits, - vETHQuoteReceiveQuantityUnits + vETHQuoteBoundQuantityUnits ); await perpLeverageModule.connect(owner.wallet).trade( subjectSetToken, expectedVBTCToken, vbtcTradeQuantityUnits, - vBTCQuoteReceiveQuantityUnits + vBTCQuoteBoundQuantityUnits ); }); @@ -5111,21 +5075,21 @@ describe("PerpV2LeverageModule", () => { vethTradeQuantityUnits = preciseDiv(ether(1), issueQuantity); vbtcTradeQuantityUnits = preciseDiv(ether(1), issueQuantity); - const vETHQuoteReceiveQuantityUnits = preciseDiv(ether(10.15), issueQuantity); - const vBTCQuoteReceiveQuantityUnits = preciseDiv(ether(50.575), issueQuantity); + const vETHQuoteBoundQuantityUnits = preciseDiv(ether(10.15), issueQuantity); + const vBTCQuoteBoundQuantityUnits = preciseDiv(ether(50.575), issueQuantity); await perpLeverageModule.connect(owner.wallet).trade( subjectSetToken, expectedVETHToken, vethTradeQuantityUnits, - vETHQuoteReceiveQuantityUnits + vETHQuoteBoundQuantityUnits ); await perpLeverageModule.connect(owner.wallet).trade( subjectSetToken, expectedVBTCToken, vbtcTradeQuantityUnits, - vBTCQuoteReceiveQuantityUnits + vBTCQuoteBoundQuantityUnits ); }); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 35f37f1fa..c94395835 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -77,6 +77,7 @@ export { PriceOracle } from "../../typechain/PriceOracle"; export { ProtocolViewer } from "../../typechain/ProtocolViewer"; export { ResourceIdentifierMock } from "../../typechain/ResourceIdentifierMock"; export { SetToken } from "../../typechain/SetToken"; +export { SetTokenAccessibleMock } from "../../typechain/SetTokenAccessibleMock"; export { SetTokenCreator } from "../../typechain/SetTokenCreator"; export { SetValuer } from "../../typechain/SetValuer"; export { SingleIndexModule } from "../../typechain/SingleIndexModule"; @@ -111,6 +112,8 @@ export { UniswapV2Pair } from "../../typechain/UniswapV2Pair"; export { UniswapV2Router02 } from "../../typechain/UniswapV2Router02"; export { UniswapYieldHook } from "../../typechain/UniswapYieldHook"; export { UniswapV3ExchangeAdapter } from "../../typechain/UniswapV3ExchangeAdapter"; +export { UniswapV3MathMock } from "../../typechain/UniswapV3MathMock"; +export { UnitConversionUtilsMock } from "../../typechain/UnitConversionUtilsMock"; export { WETH9 } from "../../typechain/WETH9"; export { WrapAdapterMock } from "../../typechain/WrapAdapterMock"; export { WrapV2AdapterMock } from "../../typechain/WrapV2AdapterMock"; diff --git a/utils/contracts/uniswapV3.ts b/utils/contracts/uniswapV3.ts index 904750b23..6c6a39c69 100644 --- a/utils/contracts/uniswapV3.ts +++ b/utils/contracts/uniswapV3.ts @@ -4,4 +4,4 @@ export { UniswapV3Pool } from "../../typechain/UniswapV3Pool"; export { SwapRouter } from "../../typechain/SwapRouter"; export { NonfungiblePositionManager } from "../../typechain/NonfungiblePositionManager"; export { Quoter } from "../../typechain/Quoter"; -export { NFTDescriptor } from "../../typechain/NFTDescriptor"; \ No newline at end of file +export { NFTDescriptor } from "../../typechain/NFTDescriptor"; diff --git a/utils/deploys/deployMocks.ts b/utils/deploys/deployMocks.ts index 3099bec59..c70677c3e 100644 --- a/utils/deploys/deployMocks.ts +++ b/utils/deploys/deployMocks.ts @@ -46,7 +46,10 @@ import { WrapV2AdapterMock, ZeroExMock, YearnStrategyMock, - AaveV2Mock + AaveV2Mock, + UniswapV3MathMock, + UnitConversionUtilsMock, + SetTokenAccessibleMock } from "../contracts"; import { ether } from "../common"; @@ -97,7 +100,9 @@ import { SynthMock__factory } from "../../typechain/factories/SynthMock__factory import { SynthetixExchangerMock__factory } from "../../typechain/factories/SynthetixExchangerMock__factory"; import { YearnStrategyMock__factory } from "../../typechain/factories/YearnStrategyMock__factory"; import { AaveV2Mock__factory } from "../../typechain/factories/AaveV2Mock__factory"; - +import { UniswapV3MathMock__factory } from "../../typechain/factories/UniswapV3MathMock__factory"; +import { UnitConversionUtilsMock__factory } from "../../typechain/factories/UnitConversionUtilsMock__factory"; +import { SetTokenAccessibleMock__factory } from "../../typechain/factories/SetTokenAccessibleMock__factory"; export default class DeployMocks { private _deployerSigner: Signer; @@ -294,6 +299,18 @@ export default class DeployMocks { ).deploy(); } + public async deployUniswapV3MathMock(): Promise { + return await new UniswapV3MathMock__factory(this._deployerSigner).deploy(); + } + + public async deployUnitConversionUtilsMock(): Promise { + return await new UnitConversionUtilsMock__factory(this._deployerSigner).deploy(); + } + + public async deploySetTokenAccessibleMock(controller: Address): Promise { + return await new SetTokenAccessibleMock__factory(this._deployerSigner).deploy(controller); + } + public async deployClaimAdapterMock(): Promise { return await new ClaimAdapterMock__factory(this._deployerSigner).deploy(); } diff --git a/utils/fixtures/perpV2Fixture.ts b/utils/fixtures/perpV2Fixture.ts index 4e4dbeece..f6d7b69ea 100644 --- a/utils/fixtures/perpV2Fixture.ts +++ b/utils/fixtures/perpV2Fixture.ts @@ -350,6 +350,10 @@ export class PerpV2Fixture { const pool = this._pools[_baseToken]; const sqrtPriceX96 = (await pool.slot0()).sqrtPriceX96; + return this.getPriceFromSqrtPriceX96(sqrtPriceX96); + } + + public getPriceFromSqrtPriceX96(sqrtPriceX96: BigNumber): BigNumber { const priceX86 = JSBI.BigInt(sqrtPriceX96.toString()); const squaredPrice = JSBI.multiply(priceX86, priceX86); const decimalsRatio = 1e18;