diff --git a/contracts/protocol/modules/v1/SlippageIssuanceModule.sol b/contracts/protocol/modules/v1/SlippageIssuanceModule.sol index 9bd8bf0cc..183424509 100644 --- a/contracts/protocol/modules/v1/SlippageIssuanceModule.sol +++ b/contracts/protocol/modules/v1/SlippageIssuanceModule.sol @@ -42,6 +42,20 @@ contract SlippageIssuanceModule is DebtIssuanceModule { /* ============ External Functions ============ */ + /** + * @dev Reverts upon calling. Call `issueWithSlippage` instead. + */ + function issue(ISetToken /*_setToken*/, uint256 /*_quantity*/, address /*_to*/) external override(DebtIssuanceModule) { + revert("Call issueWithSlippage instead"); + } + + /** + * @dev Reverts upon calling. Call `redeemWithSlippage` instead. + */ + function redeem(ISetToken /*_setToken*/, uint256 /*_quantity*/, address /*_to*/) external override(DebtIssuanceModule) { + revert("Call redeemWithSlippage instead"); + } + /** * Deposits components to the SetToken, replicates any external module component positions and mints * the SetToken. If the token has a debt position all collateral will be transferred in first then debt @@ -76,8 +90,6 @@ contract SlippageIssuanceModule is DebtIssuanceModule { address hookContract = _callManagerPreIssueHooks(_setToken, _setQuantity, msg.sender, _to); - _callModulePreIssueHooks(_setToken, _setQuantity); - bool isIssue = true; ( @@ -86,6 +98,8 @@ contract SlippageIssuanceModule is DebtIssuanceModule { uint256 protocolFee ) = calculateTotalFees(_setToken, _setQuantity, isIssue); + _callModulePreIssueHooks(_setToken, quantityWithFees); + // Scoping logic to avoid stack too deep errors { ( @@ -148,11 +162,6 @@ contract SlippageIssuanceModule is DebtIssuanceModule { { _validateInputs(_setQuantity, _checkedComponents, _minTokenAmountsOut); - _callModulePreRedeemHooks(_setToken, _setQuantity); - - // Place burn after pre-redeem hooks because burning tokens may lead to false accounting of synced positions - _setToken.burn(msg.sender, _setQuantity); - bool isIssue = false; ( @@ -161,6 +170,11 @@ contract SlippageIssuanceModule is DebtIssuanceModule { uint256 protocolFee ) = calculateTotalFees(_setToken, _setQuantity, isIssue); + _callModulePreRedeemHooks(_setToken, quantityNetFees); + + // Place burn after pre-redeem hooks because burning tokens may lead to false accounting of synced positions + _setToken.burn(msg.sender, _setQuantity); + ( address[] memory components, uint256[] memory equityUnits, diff --git a/test/integration/perpV2BasisTradingSlippageIssuance.spec.ts b/test/integration/perpV2BasisTradingSlippageIssuance.spec.ts index a4d6a3c6a..cd24e486d 100644 --- a/test/integration/perpV2BasisTradingSlippageIssuance.spec.ts +++ b/test/integration/perpV2BasisTradingSlippageIssuance.spec.ts @@ -37,7 +37,7 @@ import { } from "@utils/test/index"; import { PerpV2Fixture, SystemFixture } from "@utils/fixtures"; import { BigNumber } from "ethers"; -import { ADDRESS_ZERO, ZERO, MAX_UINT_256, ZERO_BYTES, ONE_DAY_IN_SECONDS } from "@utils/constants"; +import { ADDRESS_ZERO, ZERO, MAX_UINT_256, ZERO_BYTES, ONE_DAY_IN_SECONDS, PRECISE_UNIT } from "@utils/constants"; const expect = getWaffleExpect(); @@ -159,17 +159,12 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { async function calculateRedemptionData( setToken: Address, - redeemQuantity: BigNumber, + redeemQuantityNetFees: BigNumber, usdcTransferOutQuantity: BigNumber ) { // Calculate fee adjusted usdcTransferOut - const redeemQuantityWithFees = (await slippageIssuanceModule.calculateTotalFees( - setToken, - redeemQuantity, - false - ))[0]; - - const feeAdjustedTransferOutUSDC = preciseMul(redeemQuantityWithFees, usdcTransferOutQuantity); + const externalPositionUnit = preciseDiv(usdcTransferOutQuantity, redeemQuantityNetFees); + const feeAdjustedTransferOutUSDC = preciseMul(redeemQuantityNetFees, externalPositionUnit); // Calculate realizedPnl. The amount is debited from collateral returned to redeemer *and* // debited from the Perp account collateral balance because withdraw performs a settlement. @@ -177,7 +172,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { const positionUnitInfo = await perpBasisTradingModule.getPositionUnitInfo(setToken); for (const info of positionUnitInfo) { - const baseTradeQuantityNotional = preciseMul(info.baseUnit, redeemQuantity); + const baseTradeQuantityNotional = preciseMul(info.baseUnit, redeemQuantityNetFees); const { deltaQuote } = await perpSetup.getSwapQuote( info.baseToken, @@ -198,11 +193,23 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { return { feeAdjustedTransferOutUSDC, - realizedPnlUSDC, - redeemQuantityWithFees + realizedPnlUSDC }; } + function calculateQuantityNetFees( + setQuantity: BigNumber, + issueFee: BigNumber, + redeemFee: BigNumber, + isIssue: boolean, + ): BigNumber { + if (isIssue) { + return preciseMul(setQuantity, PRECISE_UNIT.add(issueFee)); + } else { + return preciseMul(setQuantity, PRECISE_UNIT.sub(redeemFee)); + } + } + // PerpV2BasisTradingModule#moduleIssueHook implementation calls PerpV2LeverageModuleV2#moduleIssueHook to handle issuance // after updating tracked settled funding. The functionality to track settled funding when moduleIssueHook is called is // tested in the PerpV2BasisTradingModule unit tests (test/protocol/modules/perpV2BasisTradingModule.spec.ts). And the @@ -219,10 +226,12 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { describe("#redemption", async () => { let setToken: SetToken; let baseToken: Address; + let issueFee: BigNumber; let redeemFee: BigNumber; let depositQuantityUnit: BigNumber; let usdcDefaultPositionUnit: BigNumber; let usdcTransferOutQuantity: BigNumber; + let quantityNetFees: BigNumber; let subjectSetToken: Address; let subjectQuantity: BigNumber; @@ -239,11 +248,12 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { [usdcDefaultPositionUnit], [perpBasisTradingModule.address, slippageIssuanceModule.address] ); + issueFee = ether(0.005); redeemFee = ether(0.005); await slippageIssuanceModule.initialize( setToken.address, ether(0.02), - ether(0.005), + issueFee, redeemFee, feeRecipient.address, ADDRESS_ZERO @@ -279,7 +289,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { beforeEach(async () => { // Issue 1 SetToken issueQuantity = ether(1); - await slippageIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + await slippageIssuanceModule.issueWithSlippage(setToken.address, issueQuantity, [], [], owner.address); depositQuantityUnit = usdcUnits(10); await perpBasisTradingModule.deposit(setToken.address, depositQuantityUnit); @@ -305,15 +315,16 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { subjectTo = owner.address; subjectCaller = owner; + quantityNetFees = calculateQuantityNetFees(subjectQuantity, issueFee, redeemFee, false); usdcTransferOutQuantity = await calculateUSDCTransferOut( setToken, - subjectQuantity, + quantityNetFees, perpBasisTradingModule, perpSetup ); }); - it("should not update the USDC defaultPositionUnit", async () => { + it("should NOT update the USDC defaultPositionUnit", async () => { const initialDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address); await subject(); const finalDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address); @@ -321,15 +332,37 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { expect(initialDefaultPositionUnit).eq(finalDefaultPositionUnit); }); + it("should NOT update the USDC defaultPositionUnit", async () => { + const initialDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address); + await subject(); + const finalDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address);; + + expect(finalDefaultPositionUnit).to.eq(initialDefaultPositionUnit); + }); + + it("should NOT update the virtual quote token position unit", async () => { + const totalSupply = await setToken.totalSupply(); + const initialBaseBalance = (await perpBasisTradingModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; + const initialBasePositionUnit = preciseDiv(initialBaseBalance, totalSupply); + + await subject(); + + const newTotalSupply = await setToken.totalSupply(); + const finalBaseBalance = (await perpBasisTradingModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; + const finalBasePositionUnit = preciseDiv(finalBaseBalance, newTotalSupply); + + expect(initialBasePositionUnit).to.eq(finalBasePositionUnit); + }); + it("should have updated the USDC externalPositionUnit", async () => { const initialExternalPositionUnit = await setToken.getExternalPositionRealUnit(usdc.address, perpBasisTradingModule.address); await subject(); const finalExternalPositionUnit = await setToken.getExternalPositionRealUnit(usdc.address, perpBasisTradingModule.address); - const expectedExternalPositionUnit = usdcTransferOutQuantity; + const expectedExternalPositionUnit = preciseDiv(usdcTransferOutQuantity, quantityNetFees); expect(initialExternalPositionUnit).not.eq(finalExternalPositionUnit); - expect(finalExternalPositionUnit).eq(expectedExternalPositionUnit); + expect(finalExternalPositionUnit).closeTo(expectedExternalPositionUnit, 1); }); it("should have the expected virtual token balance", async () => { @@ -340,7 +373,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { const finalBaseBalance = (await perpBasisTradingModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; const basePositionUnit = preciseDiv(initialBaseBalance, totalSupply); - const baseTokenBoughtNotional = preciseMul(basePositionUnit, subjectQuantity); + const baseTokenBoughtNotional = preciseMul(basePositionUnit, quantityNetFees); const expectedBaseBalance = initialBaseBalance.sub(baseTokenBoughtNotional); expect(finalBaseBalance).eq(expectedBaseBalance); @@ -367,7 +400,6 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { expect(accountInfo.pendingFundingPayments).to.be.gt(ZERO); }); - it("should not update the USDC defaultPositionUnit", async () => { const initialDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address); await subject(); @@ -380,7 +412,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { const baseBalance = await perpSetup.accountBalance.getBase(setToken.address, vETH.address); usdcTransferOutQuantity = await calculateUSDCTransferOutPreciseUnits( setToken, - subjectQuantity, + quantityNetFees, perpBasisTradingModule, perpSetup ); @@ -401,7 +433,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { ); const expectedExternalPositionUnit = toUSDCDecimals( - preciseDiv(usdcTransferOutQuantity, subjectQuantity) + preciseDiv(usdcTransferOutQuantity, quantityNetFees) ).sub(performanceFeeUnit); const finalExternalPositionUnit = await setToken.getExternalPositionRealUnit(usdc.address, perpBasisTradingModule.address); @@ -418,7 +450,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { const finalBaseBalance = (await perpBasisTradingModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; const basePositionUnit = preciseDiv(initialBaseBalance, totalSupply); - const baseTokenBoughtNotional = preciseMul(basePositionUnit, subjectQuantity); + const baseTokenBoughtNotional = preciseMul(basePositionUnit, quantityNetFees); const expectedBaseBalance = initialBaseBalance.sub(baseTokenBoughtNotional); expect(finalBaseBalance).eq(expectedBaseBalance); @@ -444,7 +476,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { realizedPnlUSDC } = await calculateRedemptionData( subjectSetToken, - subjectQuantity, + quantityNetFees, usdcTransferOutQuantity )); }); @@ -479,7 +511,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { const finalOwnerUSDCBalance = await usdc.balanceOf(subjectCaller.address); const expectedUSDCBalance = initialOwnerUSDCBalance.add(feeAdjustedTransferOutUSDC); - expect(finalOwnerUSDCBalance).eq(expectedUSDCBalance); + expect(finalOwnerUSDCBalance).closeTo(expectedUSDCBalance, 1); }); }); }); @@ -493,7 +525,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { beforeEach(async () => { // Issue 2 SetTokens issueQuantity = ether(2); - await slippageIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + await slippageIssuanceModule.issueWithSlippage(setToken.address, issueQuantity, [], [], owner.address); // Deposit entire default position depositQuantityUnit = usdcDefaultPositionUnit; @@ -520,15 +552,16 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { subjectTo = owner.address; subjectCaller = owner; + quantityNetFees = calculateQuantityNetFees(subjectQuantity, issueFee, redeemFee, false); usdcTransferOutQuantity = await calculateUSDCTransferOut( setToken, - subjectQuantity, + quantityNetFees, perpBasisTradingModule, perpSetup ); }); - it("should not update the USDC defaultPositionUnit", async () => { + it("should NOT update the USDC defaultPositionUnit", async () => { const initialDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address); await subject(); const finalDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address); @@ -536,6 +569,28 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { expect(initialDefaultPositionUnit).eq(finalDefaultPositionUnit); }); + it("should NOT update the USDC defaultPositionUnit", async () => { + const initialDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address); + await subject(); + const finalDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address);; + + expect(finalDefaultPositionUnit).to.eq(initialDefaultPositionUnit); + }); + + it("should NOT update the virtual quote token position unit", async () => { + const totalSupply = await setToken.totalSupply(); + const initialBaseBalance = (await perpBasisTradingModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; + const initialBasePositionUnit = preciseDiv(initialBaseBalance, totalSupply); + + await subject(); + + const newTotalSupply = await setToken.totalSupply(); + const finalBaseBalance = (await perpBasisTradingModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; + const finalBasePositionUnit = preciseDiv(finalBaseBalance, newTotalSupply); + + expect(initialBasePositionUnit).to.eq(finalBasePositionUnit); + }); + it("should update the USDC externalPositionUnit", async () => { const initialExternalPositionUnit = await setToken.getExternalPositionRealUnit(usdc.address, perpBasisTradingModule.address); await subject(); @@ -544,7 +599,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { // initialExternalPositionUnit = 10_000_000 // finalExternalPositionUnit = 9_597_857 - const expectedExternalPositionUnit = preciseDiv(usdcTransferOutQuantity, subjectQuantity);; + const expectedExternalPositionUnit = preciseDiv(usdcTransferOutQuantity, quantityNetFees); expect(initialExternalPositionUnit).eq(usdcDefaultPositionUnit); expect(finalExternalPositionUnit).to.be.closeTo(expectedExternalPositionUnit, 1); }); @@ -557,21 +612,15 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { const finalBaseBalance = (await perpBasisTradingModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; const basePositionUnit = preciseDiv(initialBaseBalance, totalSupply); - const baseTokenSoldNotional = preciseMul(basePositionUnit, subjectQuantity); + const baseTokenSoldNotional = preciseMul(basePositionUnit, quantityNetFees); const expectedBaseBalance = initialBaseBalance.sub(baseTokenSoldNotional); expect(finalBaseBalance).eq(expectedBaseBalance); }); it("should get required component redemption units correctly", async () => { - const issueQuantityWithFees = (await slippageIssuanceModule.calculateTotalFees( - subjectSetToken, - subjectQuantity, - false - ))[0]; - - const externalPositionUnit = preciseDiv(usdcTransferOutQuantity, subjectQuantity); - const feeAdjustedTransferOut = preciseMul(issueQuantityWithFees, externalPositionUnit); + const externalPositionUnit = preciseDiv(usdcTransferOutQuantity, quantityNetFees); + const feeAdjustedTransferOut = preciseMul(quantityNetFees, externalPositionUnit); const [components, equityFlows, debtFlows] = await slippageIssuanceModule .callStatic @@ -675,7 +724,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { const baseBalance = await perpSetup.accountBalance.getBase(setToken.address, vETH.address); usdcTransferOutQuantity = await calculateUSDCTransferOutPreciseUnits( setToken, - subjectQuantity, + quantityNetFees, perpBasisTradingModule, perpSetup ); @@ -696,7 +745,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { ); const expectedExternalPositionUnit = toUSDCDecimals( - preciseDiv(usdcTransferOutQuantity, subjectQuantity) + preciseDiv(usdcTransferOutQuantity, quantityNetFees) ).sub(performanceFeeUnit); const finalExternalPositionUnit = await setToken.getExternalPositionRealUnit(usdc.address, perpBasisTradingModule.address); @@ -713,7 +762,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { const finalBaseBalance = (await perpBasisTradingModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; const basePositionUnit = preciseDiv(initialBaseBalance, totalSupply); - const baseTokenBoughtNotional = preciseMul(basePositionUnit, subjectQuantity); + const baseTokenBoughtNotional = preciseMul(basePositionUnit, quantityNetFees); const expectedBaseBalance = initialBaseBalance.sub(baseTokenBoughtNotional); expect(finalBaseBalance).eq(expectedBaseBalance); @@ -738,7 +787,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { realizedPnlUSDC } = await calculateRedemptionData( subjectSetToken, - subjectQuantity, + quantityNetFees, usdcTransferOutQuantity) ); }); @@ -841,6 +890,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { describe("when redeeming after a liquidation", async () => { beforeEach(async () => { subjectQuantity = ether(1); + quantityNetFees = calculateQuantityNetFees(subjectQuantity, issueFee, redeemFee, false); // Calculated leverage = ~8.5X = 8_654_438_822_995_683_587 await leverUp( @@ -879,21 +929,20 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { const usdcTransferOutQuantity = await calculateUSDCTransferOut( setToken, - subjectQuantity, + quantityNetFees, perpBasisTradingModule, perpSetup ); const { feeAdjustedTransferOutUSDC, - redeemQuantityWithFees } = await calculateRedemptionData( subjectSetToken, - subjectQuantity, + quantityNetFees, usdcTransferOutQuantity ); - const expectedTotalSupply = initialTotalSupply.sub(redeemQuantityWithFees); + const expectedTotalSupply = initialTotalSupply.sub(quantityNetFees); const expectedCollateralBalance = toUSDCDecimals(initialCollateralBalance) .sub(feeAdjustedTransferOutUSDC) .add(owedRealizedPnlUSDC); @@ -953,12 +1002,6 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { // collateralBalance = 10050000000000000000 // owedRealizedPnl = -31795534271984084912 it("should redeem without transferring any usdc (because account worth 0)", async () => { - const redeemQuantityWithFees = (await slippageIssuanceModule.calculateTotalFees( - subjectSetToken, - subjectQuantity, - false - ))[0]; - const initialRedeemerUSDCBalance = await usdc.balanceOf(subjectCaller.address); const initialTotalSupply = await setToken.totalSupply(); @@ -967,7 +1010,7 @@ describe("PerpV2BasisTradingSlippageIssuance", () => { const finalRedeemerUSDCBalance = await usdc.balanceOf(subjectCaller.address); const finalTotalSupply = await setToken.totalSupply(); - const expectedTotalSupply = initialTotalSupply.sub(redeemQuantityWithFees); + const expectedTotalSupply = initialTotalSupply.sub(quantityNetFees); expect(finalTotalSupply).eq(expectedTotalSupply); expect(finalRedeemerUSDCBalance).eq(initialRedeemerUSDCBalance); diff --git a/test/integration/perpV2LeverageV2SlippageIssuance.spec.ts b/test/integration/perpV2LeverageV2SlippageIssuance.spec.ts index 893d1ef69..4878d276e 100644 --- a/test/integration/perpV2LeverageV2SlippageIssuance.spec.ts +++ b/test/integration/perpV2LeverageV2SlippageIssuance.spec.ts @@ -34,7 +34,7 @@ import { } from "@utils/test/index"; import { PerpV2Fixture, SystemFixture } from "@utils/fixtures"; import { BigNumber } from "ethers"; -import { ADDRESS_ZERO, ZERO, MAX_UINT_256, ZERO_BYTES } from "@utils/constants"; +import { ADDRESS_ZERO, ZERO, MAX_UINT_256, ZERO_BYTES, PRECISE_UNIT } from "@utils/constants"; const expect = getWaffleExpect(); @@ -183,25 +183,20 @@ describe("PerpV2LeverageSlippageIssuance", () => { async function calculateRedemptionData( setToken: Address, - redeemQuantity: BigNumber, + redeemQuantityNetFees: BigNumber, usdcTransferOutQuantity: BigNumber ) { // Calculate fee adjusted usdcTransferOut - const redeemQuantityWithFees = (await slippageIssuanceModule.calculateTotalFees( - setToken, - redeemQuantity, - false - ))[0]; - - const feeAdjustedTransferOutUSDC = preciseMul(redeemQuantityWithFees, usdcTransferOutQuantity); + const externalPositionUnit = preciseDiv(usdcTransferOutQuantity, redeemQuantityNetFees); + const feeAdjustedTransferOutUSDC = preciseMul(redeemQuantityNetFees, externalPositionUnit); // 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); + const positionUnitInfo = await perpLeverageModule.getPositionUnitInfo(setToken); for (const info of positionUnitInfo) { - const baseTradeQuantityNotional = preciseMul(info.baseUnit, redeemQuantity); + const baseTradeQuantityNotional = preciseMul(info.baseUnit, redeemQuantityNetFees); const { deltaQuote } = await perpSetup.getSwapQuote( info.baseToken, @@ -222,14 +217,27 @@ describe("PerpV2LeverageSlippageIssuance", () => { return { feeAdjustedTransferOutUSDC, - realizedPnlUSDC, - redeemQuantityWithFees + realizedPnlUSDC }; } + function calculateQuantityNetFees( + setQuantity: BigNumber, + issueFee: BigNumber, + redeemFee: BigNumber, + isIssue: boolean, + ): BigNumber { + if (isIssue) { + return preciseMul(setQuantity, PRECISE_UNIT.add(issueFee)); + } else { + return preciseMul(setQuantity, PRECISE_UNIT.sub(redeemFee)); + } + } + describe("#issuance", async () => { let setToken: SetToken; let issueFee: BigNumber; + let redeemFee: BigNumber; let usdcDefaultPositionUnit: BigNumber; let subjectSetToken: Address; @@ -248,6 +256,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { [perpLeverageModule.address, slippageIssuanceModule.address] ); issueFee = ether(0.005); + redeemFee = ether(0.005); await slippageIssuanceModule.initialize( setToken.address, ether(0.02), @@ -333,13 +342,14 @@ describe("PerpV2LeverageSlippageIssuance", () => { let baseToken: Address; let depositQuantityUnit: BigNumber; let usdcTransferInQuantity: BigNumber; + let quantityNetFees: BigNumber; cacheBeforeEach(initializeContracts); beforeEach(async () => { // Issue 1 SetToken issueQuantity = ether(1); - await slippageIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + await slippageIssuanceModule.issueWithSlippage(setToken.address, issueQuantity, [], [], owner.address); depositQuantityUnit = usdcDefaultPositionUnit; await perpLeverageModule.deposit(setToken.address, depositQuantityUnit); @@ -381,15 +391,16 @@ describe("PerpV2LeverageSlippageIssuance", () => { beforeEach(async () => { subjectQuantity = ether(1); + quantityNetFees = calculateQuantityNetFees(subjectQuantity, issueFee, redeemFee, true); usdcTransferInQuantity = await calculateUSDCTransferIn( setToken, - subjectQuantity, + quantityNetFees, perpLeverageModule, perpSetup ); }); - it("should not update the USDC defaultPositionUnit", async () => { + it("should NOT update the USDC defaultPositionUnit", async () => { const initialDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address); await subject(); const finalDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address);; @@ -397,8 +408,22 @@ describe("PerpV2LeverageSlippageIssuance", () => { expect(finalDefaultPositionUnit).to.eq(initialDefaultPositionUnit); }); + it("should NOT update the virtual quote token position unit", async () => { + const totalSupply = await setToken.totalSupply(); + const initialBaseBalance = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; + const initialBasePositionUnit = preciseDiv(initialBaseBalance, totalSupply); + + await subject(); + + const newTotalSupply = await setToken.totalSupply(); + const finalBaseBalance = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; + const finalBasePositionUnit = preciseDiv(finalBaseBalance, newTotalSupply); + + expect(initialBasePositionUnit).to.eq(finalBasePositionUnit); + }); + it("should have set the expected USDC externalPositionUnit", async () => { - const expectedExternalPositionUnit = preciseDiv(usdcTransferInQuantity, subjectQuantity); + const expectedExternalPositionUnit = preciseDiv(usdcTransferInQuantity, quantityNetFees); await subject(); @@ -419,7 +444,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { const finalBaseBalance = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; const basePositionUnit = preciseDiv(initialBaseBalance, totalSupply); - const baseTokenBoughtNotional = preciseMul(basePositionUnit, subjectQuantity); + const baseTokenBoughtNotional = preciseMul(basePositionUnit, quantityNetFees); const expectedBaseBalance = initialBaseBalance.add(baseTokenBoughtNotional); expect(finalBaseBalance).eq(expectedBaseBalance); @@ -429,30 +454,13 @@ describe("PerpV2LeverageSlippageIssuance", () => { const initialCollateralBalance = (await perpLeverageModule.getAccountInfo(subjectSetToken)).collateralBalance; await subject(); const finalCollateralBalance = (await perpLeverageModule.getAccountInfo(subjectSetToken)).collateralBalance; - - const issueQuantityWithFees = (await slippageIssuanceModule.calculateTotalFees( - subjectSetToken, - subjectQuantity, - true - ))[0]; - - const feeAdjustedTransferIn = preciseMul(issueQuantityWithFees, usdcTransferInQuantity); - - // usdcTransferIn = 10_008_105 - // feeAdjustedTransferIn = 10_058_145 - const expectedCollateralBalance = toUSDCDecimals(initialCollateralBalance).add(feeAdjustedTransferIn); + const expectedCollateralBalance = toUSDCDecimals(initialCollateralBalance).add(usdcTransferInQuantity); expect(toUSDCDecimals(finalCollateralBalance)).to.be.closeTo(expectedCollateralBalance, 2); }); it("should get required component issuance units correctly", async () => { - const issueQuantityWithFees = (await slippageIssuanceModule.calculateTotalFees( - subjectSetToken, - subjectQuantity, - true - ))[0]; - - const externalPositionUnit = preciseDiv(usdcTransferInQuantity, subjectQuantity); - const feeAdjustedTransferIn = preciseMul(issueQuantityWithFees, externalPositionUnit); + const externalPositionUnit = preciseDiv(usdcTransferInQuantity, quantityNetFees); + const feeAdjustedTransferIn = preciseMul(quantityNetFees, externalPositionUnit); const [components, equityFlows, debtFlows] = await slippageIssuanceModule.callStatic.getRequiredComponentIssuanceUnitsOffChain( subjectSetToken, @@ -473,16 +481,39 @@ describe("PerpV2LeverageSlippageIssuance", () => { beforeEach(async () => { subjectQuantity = ether(2); + quantityNetFees = calculateQuantityNetFees(subjectQuantity, issueFee, redeemFee, true); usdcTransferInQuantity = await calculateUSDCTransferIn( setToken, - subjectQuantity, + quantityNetFees, perpLeverageModule, perpSetup ); }); + it("should NOT update the USDC defaultPositionUnit", async () => { + const initialDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address); + await subject(); + const finalDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address);; + + expect(finalDefaultPositionUnit).to.eq(initialDefaultPositionUnit); + }); + + it("should NOT update the virtual quote token position unit", async () => { + const totalSupply = await setToken.totalSupply(); + const initialBaseBalance = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; + const initialBasePositionUnit = preciseDiv(initialBaseBalance, totalSupply); + + await subject(); + + const newTotalSupply = await setToken.totalSupply(); + const finalBaseBalance = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; + const finalBasePositionUnit = preciseDiv(finalBaseBalance, newTotalSupply); + + expect(initialBasePositionUnit).to.eq(finalBasePositionUnit); + }); + it("should have set the expected USDC externalPositionUnit", async () => { - const expectedExternalPositionUnit = preciseDiv(usdcTransferInQuantity, subjectQuantity); + const expectedExternalPositionUnit = preciseDiv(usdcTransferInQuantity, quantityNetFees); await subject(); @@ -503,7 +534,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { const finalBaseBalance = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; const basePositionUnit = preciseDiv(initialBaseBalance, totalSupply); - const baseTokenBoughtNotional = preciseMul(basePositionUnit, subjectQuantity); + const baseTokenBoughtNotional = preciseMul(basePositionUnit, quantityNetFees); const expectedBaseBalance = initialBaseBalance.add(baseTokenBoughtNotional); expect(finalBaseBalance).eq(expectedBaseBalance); @@ -514,14 +545,8 @@ describe("PerpV2LeverageSlippageIssuance", () => { await subject(); 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 externalPositionUnit = preciseDiv(usdcTransferInQuantity, quantityNetFees); + const feeAdjustedTransferIn = preciseMul(quantityNetFees, externalPositionUnit); // usdcTransferIn = 20_024_302 // feeAdjustedTransferIn = 20_124_423 @@ -530,14 +555,8 @@ describe("PerpV2LeverageSlippageIssuance", () => { }); it("should deposit the expected amount into the Perp vault", async () => { - const issueQuantityWithFees = (await slippageIssuanceModule.calculateTotalFees( - subjectSetToken, - subjectQuantity, - true - ))[0]; - - const externalPositionUnit = preciseDiv(usdcTransferInQuantity, subjectQuantity); - const feeAdjustedTransferIn = preciseMul(issueQuantityWithFees, externalPositionUnit); + const externalPositionUnit = preciseDiv(usdcTransferInQuantity, quantityNetFees); + const feeAdjustedTransferIn = preciseMul(quantityNetFees, externalPositionUnit); const initialCollateralBalance = (await perpLeverageModule.getAccountInfo(subjectSetToken)).collateralBalance; await subject(); @@ -755,10 +774,12 @@ describe("PerpV2LeverageSlippageIssuance", () => { describe("#redemption", async () => { let setToken: SetToken; let baseToken: Address; + let issueFee: BigNumber; let redeemFee: BigNumber; let depositQuantityUnit: BigNumber; let usdcDefaultPositionUnit: BigNumber; let usdcTransferOutQuantity: BigNumber; + let quantityNetFees: BigNumber; let subjectSetToken: Address; let subjectQuantity: BigNumber; @@ -775,6 +796,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { [usdcDefaultPositionUnit], [perpLeverageModule.address, slippageIssuanceModule.address] ); + issueFee = ether(0.005); redeemFee = ether(0.005); await slippageIssuanceModule.initialize( setToken.address, @@ -808,7 +830,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { beforeEach(async () => { // Issue 1 SetToken issueQuantity = ether(1); - await slippageIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + await slippageIssuanceModule.issueWithSlippage(setToken.address, issueQuantity, [], [], owner.address); depositQuantityUnit = usdcUnits(10); await perpLeverageModule.deposit(setToken.address, depositQuantityUnit); @@ -833,15 +855,16 @@ describe("PerpV2LeverageSlippageIssuance", () => { subjectTo = owner.address; subjectCaller = owner; + quantityNetFees = calculateQuantityNetFees(subjectQuantity, issueFee, redeemFee, false); usdcTransferOutQuantity = await calculateUSDCTransferOut( setToken, - subjectQuantity, + quantityNetFees, perpLeverageModule, perpSetup ); }); - it("should not update the USDC defaultPositionUnit", async () => { + it("should NOT update the USDC defaultPositionUnit", async () => { const initialDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address); await subject(); const finalDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address); @@ -849,15 +872,29 @@ describe("PerpV2LeverageSlippageIssuance", () => { expect(initialDefaultPositionUnit).eq(finalDefaultPositionUnit); }); + it("should NOT update the virtual quote token position unit", async () => { + const totalSupply = await setToken.totalSupply(); + const initialBaseBalance = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; + const initialBasePositionUnit = preciseDiv(initialBaseBalance, totalSupply); + + await subject(); + + const newTotalSupply = await setToken.totalSupply(); + const finalBaseBalance = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; + const finalBasePositionUnit = preciseDiv(finalBaseBalance, newTotalSupply); + + expect(initialBasePositionUnit).to.eq(finalBasePositionUnit); + }); + it("should have updated the USDC externalPositionUnit", async () => { const initialExternalPositionUnit = await setToken.getExternalPositionRealUnit(usdc.address, perpLeverageModule.address); await subject(); const finalExternalPositionUnit = await setToken.getExternalPositionRealUnit(usdc.address, perpLeverageModule.address); - const expectedExternalPositionUnit = usdcTransferOutQuantity; + const expectedExternalPositionUnit = preciseDiv(usdcTransferOutQuantity, quantityNetFees); expect(initialExternalPositionUnit).not.eq(finalExternalPositionUnit); - expect(finalExternalPositionUnit).eq(expectedExternalPositionUnit); + expect(finalExternalPositionUnit).closeTo(expectedExternalPositionUnit, 1); }); it("should have the expected virtual token balance", async () => { @@ -868,7 +905,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { const finalBaseBalance = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; const basePositionUnit = preciseDiv(initialBaseBalance, totalSupply); - const baseTokenBoughtNotional = preciseMul(basePositionUnit, subjectQuantity); + const baseTokenBoughtNotional = preciseMul(basePositionUnit, quantityNetFees); const expectedBaseBalance = initialBaseBalance.sub(baseTokenBoughtNotional); expect(finalBaseBalance).eq(expectedBaseBalance); @@ -892,7 +929,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { realizedPnlUSDC } = await calculateRedemptionData( subjectSetToken, - subjectQuantity, + quantityNetFees, usdcTransferOutQuantity )); }); @@ -927,7 +964,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { const finalOwnerUSDCBalance = await usdc.balanceOf(subjectCaller.address); const expectedUSDCBalance = initialOwnerUSDCBalance.add(feeAdjustedTransferOutUSDC); - expect(finalOwnerUSDCBalance).eq(expectedUSDCBalance); + expect(finalOwnerUSDCBalance).closeTo(expectedUSDCBalance, 1); }); }); }); @@ -941,7 +978,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { beforeEach(async () => { // Issue 2 SetTokens issueQuantity = ether(2); - await slippageIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + await slippageIssuanceModule.issueWithSlippage(setToken.address, issueQuantity, [], [], owner.address); // Deposit entire default position depositQuantityUnit = usdcDefaultPositionUnit; @@ -967,15 +1004,16 @@ describe("PerpV2LeverageSlippageIssuance", () => { subjectTo = owner.address; subjectCaller = owner; + quantityNetFees = calculateQuantityNetFees(subjectQuantity, issueFee, redeemFee, false); usdcTransferOutQuantity = await calculateUSDCTransferOut( setToken, - subjectQuantity, + quantityNetFees, perpLeverageModule, perpSetup ); }); - it("should not update the USDC defaultPositionUnit", async () => { + it("should NOT update the USDC defaultPositionUnit", async () => { const initialDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address); await subject(); const finalDefaultPositionUnit = await setToken.getDefaultPositionRealUnit(usdc.address); @@ -983,6 +1021,20 @@ describe("PerpV2LeverageSlippageIssuance", () => { expect(initialDefaultPositionUnit).eq(finalDefaultPositionUnit); }); + it("should NOT update the virtual quote token position unit", async () => { + const totalSupply = await setToken.totalSupply(); + const initialBaseBalance = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; + const initialBasePositionUnit = preciseDiv(initialBaseBalance, totalSupply); + + await subject(); + + const newTotalSupply = await setToken.totalSupply(); + const finalBaseBalance = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; + const finalBasePositionUnit = preciseDiv(finalBaseBalance, newTotalSupply); + + expect(initialBasePositionUnit).to.eq(finalBasePositionUnit); + }); + it("should update the USDC externalPositionUnit", async () => { const initialExternalPositionUnit = await setToken.getExternalPositionRealUnit(usdc.address, perpLeverageModule.address); await subject(); @@ -991,7 +1043,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { // initialExternalPositionUnit = 10_000_000 // finalExternalPositionUnit = 9_597_857 - const expectedExternalPositionUnit = preciseDiv(usdcTransferOutQuantity, subjectQuantity);; + const expectedExternalPositionUnit = preciseDiv(usdcTransferOutQuantity, quantityNetFees);; expect(initialExternalPositionUnit).eq(usdcDefaultPositionUnit); expect(finalExternalPositionUnit).to.be.closeTo(expectedExternalPositionUnit, 1); }); @@ -1004,21 +1056,15 @@ describe("PerpV2LeverageSlippageIssuance", () => { const finalBaseBalance = (await perpLeverageModule.getPositionNotionalInfo(subjectSetToken))[0].baseBalance; const basePositionUnit = preciseDiv(initialBaseBalance, totalSupply); - const baseTokenSoldNotional = preciseMul(basePositionUnit, subjectQuantity); + const baseTokenSoldNotional = preciseMul(basePositionUnit, quantityNetFees); const expectedBaseBalance = initialBaseBalance.sub(baseTokenSoldNotional); expect(finalBaseBalance).eq(expectedBaseBalance); }); it("should get required component redemption units correctly", async () => { - const issueQuantityWithFees = (await slippageIssuanceModule.calculateTotalFees( - subjectSetToken, - subjectQuantity, - false - ))[0]; - - const externalPositionUnit = preciseDiv(usdcTransferOutQuantity, subjectQuantity); - const feeAdjustedTransferOut = preciseMul(issueQuantityWithFees, externalPositionUnit); + const externalPositionUnit = preciseDiv(usdcTransferOutQuantity, quantityNetFees); + const feeAdjustedTransferOut = preciseMul(quantityNetFees, externalPositionUnit); const [components, equityFlows, debtFlows] = await slippageIssuanceModule .callStatic @@ -1089,7 +1135,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { realizedPnlUSDC } = await calculateRedemptionData( subjectSetToken, - subjectQuantity, + quantityNetFees, usdcTransferOutQuantity) ); }); @@ -1185,6 +1231,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { describe("when redeeming after a liquidation", async () => { beforeEach(async () => { subjectQuantity = ether(1); + quantityNetFees = calculateQuantityNetFees(subjectQuantity, issueFee, redeemFee, false); // Calculated leverage = ~8.5X = 8_654_438_822_995_683_587 await leverUp( @@ -1222,21 +1269,20 @@ describe("PerpV2LeverageSlippageIssuance", () => { const usdcTransferOutQuantity = await calculateUSDCTransferOut( setToken, - subjectQuantity, + quantityNetFees, perpLeverageModule, perpSetup ); const { feeAdjustedTransferOutUSDC, - redeemQuantityWithFees } = await calculateRedemptionData( subjectSetToken, - subjectQuantity, + quantityNetFees, usdcTransferOutQuantity ); - const expectedTotalSupply = initialTotalSupply.sub(redeemQuantityWithFees); + const expectedTotalSupply = initialTotalSupply.sub(quantityNetFees); const expectedCollateralBalance = toUSDCDecimals(initialCollateralBalance) .sub(feeAdjustedTransferOutUSDC) .add(owedRealizedPnlUSDC); @@ -1287,12 +1333,6 @@ describe("PerpV2LeverageSlippageIssuance", () => { // collateralBalance = 10050000000000000000 // owedRealizedPnl = -31795534271984084912 it("should redeem without transferring any usdc (because account worth 0)", async () => { - const redeemQuantityWithFees = (await slippageIssuanceModule.calculateTotalFees( - subjectSetToken, - subjectQuantity, - false - ))[0]; - const initialRedeemerUSDCBalance = await usdc.balanceOf(subjectCaller.address); const initialTotalSupply = await setToken.totalSupply(); @@ -1301,7 +1341,7 @@ describe("PerpV2LeverageSlippageIssuance", () => { const finalRedeemerUSDCBalance = await usdc.balanceOf(subjectCaller.address); const finalTotalSupply = await setToken.totalSupply(); - const expectedTotalSupply = initialTotalSupply.sub(redeemQuantityWithFees); + const expectedTotalSupply = initialTotalSupply.sub(quantityNetFees); expect(finalTotalSupply).eq(expectedTotalSupply); expect(finalRedeemerUSDCBalance).eq(initialRedeemerUSDCBalance); diff --git a/test/protocol/modules/v1/slippageIssuanceModule.spec.ts b/test/protocol/modules/v1/slippageIssuanceModule.spec.ts index acef37f95..7c8a0e81a 100644 --- a/test/protocol/modules/v1/slippageIssuanceModule.spec.ts +++ b/test/protocol/modules/v1/slippageIssuanceModule.spec.ts @@ -22,6 +22,7 @@ import { } from "@utils/test/index"; import { SystemFixture } from "@utils/fixtures"; import { ContractTransaction } from "ethers"; +import { getRandomAddress } from "@utils/common"; const expect = getWaffleExpect(); @@ -825,7 +826,7 @@ describe("SlippageIssuanceModule", () => { await setup.weth.approve(slippageIssuance.address, equityFlows[0].mul(ether(1.005))); - await slippageIssuance.issue(setToken.address, ether(1), owner.address); + await slippageIssuance.issueWithSlippage(setToken.address, ether(1), [], [], owner.address); await setup.dai.approve(slippageIssuance.address, ether(100.5)); @@ -1144,5 +1145,45 @@ describe("SlippageIssuanceModule", () => { }); }); }); + + describe("#issue", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + + beforeEach(async () => { + subjectSetToken = await getRandomAddress(); + subjectQuantity = ether(1); + subjectTo = await getRandomAddress(); + }); + + async function subject(): Promise { + return await slippageIssuance.issue(subjectSetToken, subjectQuantity, subjectTo); + } + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Call issueWithSlippage instead"); + }); + }); + + describe("#redeem", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + + beforeEach(async () => { + subjectSetToken = await getRandomAddress(); + subjectQuantity = ether(1); + subjectTo = await getRandomAddress(); + }); + + async function subject(): Promise { + return await slippageIssuance.redeem(subjectSetToken, subjectQuantity, subjectTo); + } + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Call redeemWithSlippage instead"); + }); + }); }); });