import "module-alias/register" ; import { ContractTransaction } from "ethers" ; import { Address } from "@utils/types" ; import { Account } from "@utils/test/types" ; import { PerpV2 , PerpV2LeverageModule , SlippageIssuanceModule , SetToken , StandardTokenMock , } from "@utils/contracts" ; import { PerpV2BaseToken } from "@utils/contracts/perpV2" ; import { toUSDCDecimals , calculateUSDCTransferIn , calculateUSDCTransferOut , leverUp } from "@utils/common" ; import DeployHelper from "@utils/deploys" ; import { ether , usdc as usdcUnits , preciseDiv , preciseMul } from "@utils/index" ; import { cacheBeforeEach , getAccounts , getWaffleExpect , getSystemFixture , getPerpV2Fixture , } 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" ; const expect = getWaffleExpect ( ) ; describe ( "PerpV2LeverageSlippageIssuance" , ( ) => { let owner ; let maker ; let otherTrader ; let feeRecipient ; let deployer ; let perpLib ; let perpLeverageModule ; let slippageIssuanceModule ; let setup ; let perpSetup ; let vETH ; let vBTC ; let usdc ; cacheBeforeEach ( async ( ) => { [ owner , maker , otherTrader , feeRecipient ] = await getAccounts ( ) ; deployer = new DeployHelper ( owner . wallet ) ; setup = getSystemFixture ( owner . address ) ; await setup . initialize ( ) ; perpSetup = getPerpV2Fixture ( owner . address ) ; await perpSetup . initialize ( maker , otherTrader ) ; vETH = perpSetup . vETH ; vBTC = perpSetup . vBTC ; usdc = perpSetup . usdc ; await perpSetup . setBaseTokenOraclePrice ( vETH , usdcUnits ( 10 ) ) ; await perpSetup . initializePoolWithLiquidityWide ( vETH , ether ( 10000 ) , ether ( 100000 ) ) ; await perpSetup . setBaseTokenOraclePrice ( vBTC , usdcUnits ( 20 ) ) ; await perpSetup . initializePoolWithLiquidityWide ( vBTC , ether ( 10000 ) , ether ( 200000 ) ) ; slippageIssuanceModule = await deployer . modules . deploySlippageIssuanceModule ( setup . controller . address ) ; await setup . controller . addModule ( slippageIssuanceModule . address ) ; perpLib = await deployer . libraries . deployPerpV2 ( ) ; perpLeverageModule = await deployer . modules . deployPerpV2LeverageModule ( setup . controller . address , perpSetup . vault . address , perpSetup . quoter . address , perpSetup . marketRegistry . address , "contracts/protocol/integration/lib/PerpV2.sol:PerpV2" , perpLib . address ) ; await setup . controller . addModule ( perpLeverageModule . address ) ; await setup . integrationRegistry . addIntegration ( perpLeverageModule . address , "DefaultIssuanceModule" , slippageIssuanceModule . address ) ; } ) ; async function calculateFlashLeverage ( setToken , setQuantity ) { const spotPrice = await perpLeverageModule . getAMMSpotPrice ( vETH . address ) ; const { collateralBalance } = await perpLeverageModule . getAccountInfo ( setToken ) ; const positionNotionalInfo = ( await perpLeverageModule . getPositionNotionalInfo ( setToken ) ) [ 0 ] ; const positionUnitInfo = ( await perpLeverageModule . getPositionUnitInfo ( setToken ) ) [ 0 ] ; const currentAssetValue = preciseMul ( positionNotionalInfo . baseBalance , spotPrice ) ; const currentDebtValue = positionNotionalInfo . quoteBalance ; const flashAssetQuantityNotional = preciseMul ( positionUnitInfo . baseUnit , setQuantity ) ; const flashAssetValue = preciseMul ( flashAssetQuantityNotional , spotPrice ) ; const flashDebtValue = ( await perpSetup . getSwapQuote ( vETH . address , flashAssetQuantityNotional , true ) ) . deltaQuote ; const totalAssetValueBeforeRepayment = currentAssetValue . add ( flashAssetValue ) ; const totalDebtValueBeforeRepayment = currentDebtValue . sub ( flashDebtValue ) ; return preciseDiv ( totalAssetValueBeforeRepayment , totalAssetValueBeforeRepayment . add ( totalDebtValueBeforeRepayment ) . add ( collateralBalance ) ) ; } async function calculateTotalSlippage ( setToken , setQuantity ) { let totalExpectedSlippage = BigNumber . from ( 0 ) ; const allPositionInfo = await perpLeverageModule . getPositionNotionalInfo ( setToken . address ) ; for ( const positionInfo of allPositionInfo ) { const basePositionUnit = preciseDiv ( positionInfo . baseBalance , await setToken . totalSupply ( ) ) ; const baseTradeQuantityNotional = preciseMul ( basePositionUnit , setQuantity ) ; const isLong = ( basePositionUnit . gte ( ZERO ) ) ; const { deltaBase , deltaQuote } = await perpSetup . getSwapQuote ( positionInfo . baseToken , baseTradeQuantityNotional . abs ( ) , isLong ) ; const idealQuote = preciseMul ( deltaBase , await perpSetup . getSpotPrice ( positionInfo . baseToken ) ) ; const expectedSlippage = ( isLong ) ? idealQuote . sub ( deltaQuote ) . mul ( - 1 ) : idealQuote . sub ( deltaQuote ) ; totalExpectedSlippage = totalExpectedSlippage . add ( expectedSlippage ) ; } return totalExpectedSlippage ; } async function calculateRedemptionData ( setToken , redeemQuantity , usdcTransferOutQuantity ) { const redeemQuantityWithFees = ( await slippageIssuanceModule . calculateTotalFees ( setToken , redeemQuantity , false ) ) [ 0 ] ; const feeAdjustedTransferOutUSDC = preciseMul ( redeemQuantityWithFees , usdcTransferOutQuantity ) ; 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 ; let issueFee ; let usdcDefaultPositionUnit ; let subjectSetToken ; let subjectQuantity ; let subjectCheckedComponents ; let subjectMaxTokenAmountsIn ; let subjectTo ; let subjectCaller ; let issueQuantity ; const initializeContracts = async function ( ) { usdcDefaultPositionUnit = usdcUnits ( 10 ) ; setToken = await setup . createSetToken ( [ usdc . address ] , [ usdcDefaultPositionUnit ] , [ perpLeverageModule . address , slippageIssuanceModule . address ] ) ; issueFee = ether ( 0.005 ) ; await slippageIssuanceModule . initialize ( setToken . address , ether ( 0.02 ) , issueFee , ether ( 0.005 ) , feeRecipient . address , ADDRESS_ZERO ) ; await perpLeverageModule . updateAllowedSetToken ( setToken . address , true ) ; await perpLeverageModule . initialize ( setToken . address ) ; await usdc . approve ( slippageIssuanceModule . address , usdcUnits ( 1000 ) ) ; } ; async function subject ( ) { return slippageIssuanceModule . connect ( subjectCaller . wallet ) . issueWithSlippage ( subjectSetToken , subjectQuantity , subjectCheckedComponents , subjectMaxTokenAmountsIn , subjectTo ) ; } context ( "when there is a default usdc position with 0 supply" , async ( ) => { cacheBeforeEach ( initializeContracts ) ; beforeEach ( ( ) => { subjectSetToken = setToken . address ; subjectQuantity = ether ( 1 ) ; subjectCheckedComponents = [ ] ; subjectMaxTokenAmountsIn = [ ] ; subjectTo = owner . address ; subjectCaller = owner ; } ) ; it ( "should not update the collateral position on the SetToken" , async ( ) => { const initialPositions = await setToken . getPositions ( ) ; await subject ( ) ; const currentPositions = await setToken . getPositions ( ) ; const newFirstPosition = ( await setToken . getPositions ( ) ) [ 0 ] ; expect ( initialPositions . length ) . to . eq ( 1 ) ; expect ( currentPositions . length ) . to . eq ( 1 ) ; expect ( newFirstPosition . component ) . to . eq ( usdc . address ) ; expect ( newFirstPosition . positionState ) . to . eq ( 0 ) ; expect ( newFirstPosition . unit ) . to . eq ( usdcDefaultPositionUnit ) ; expect ( newFirstPosition . module ) . to . eq ( ADDRESS_ZERO ) ; } ) ; it ( "should not have an external usdc position" , async ( ) => { const initialPositions = await setToken . getPositions ( ) ; await subject ( ) ; const currentPositions = await setToken . getPositions ( ) ; expect ( initialPositions . length ) . to . eq ( 1 ) ; expect ( currentPositions . length ) . to . eq ( 1 ) ; } ) ; it ( "should have the correct token balances" , async ( ) => { const preMinterUSDCBalance = await usdc . balanceOf ( subjectCaller . address ) ; const preSetUSDCBalance = await usdc . balanceOf ( subjectSetToken ) ; await subject ( ) ; const mintQuantity = preciseMul ( subjectQuantity , ether ( 1 ) . add ( issueFee ) ) ; const usdcFlows = preciseMul ( mintQuantity , usdcDefaultPositionUnit ) ; const postMinterUSDCBalance = await usdc . balanceOf ( subjectCaller . address ) ; const postSetUSDCBalance = await usdc . balanceOf ( subjectSetToken ) ; expect ( postMinterUSDCBalance ) . to . eq ( preMinterUSDCBalance . sub ( usdcFlows ) ) ; expect ( postSetUSDCBalance ) . to . eq ( preSetUSDCBalance . add ( usdcFlows ) ) ; } ) ; } ) ; context ( "when there is only an external USDC position and totalSupply is 1" , async ( ) => { let baseToken ; let depositQuantityUnit ; let usdcTransferInQuantity ; cacheBeforeEach ( initializeContracts ) ; beforeEach ( async ( ) => { issueQuantity = ether ( 1 ) ; await slippageIssuanceModule . issue ( setToken . address , issueQuantity , owner . address ) ; depositQuantityUnit = usdcDefaultPositionUnit ; await perpLeverageModule . deposit ( setToken . address , depositQuantityUnit ) ; baseToken = vETH . address ; await leverUp ( setToken , perpLeverageModule , perpSetup , owner , baseToken , 2 , ether ( .02 ) , true ) ; subjectSetToken = setToken . address ; subjectCheckedComponents = [ ] ; subjectMaxTokenAmountsIn = [ ] ; subjectTo = owner . address ; subjectCaller = owner ; } ) ; describe ( "starting test assumptions" , async ( ) => { it ( "should be correct" , async ( ) => { const defaultPositionUnit = await setToken . getDefaultPositionRealUnit ( usdc . address ) ; const externalPositionUnit = await setToken . getExternalPositionRealUnit ( usdc . address , perpLeverageModule . address ) ; expect ( defaultPositionUnit ) . eq ( ZERO ) ; expect ( externalPositionUnit ) . eq ( depositQuantityUnit ) ; } ) ; } ) ; describe ( "when minting a single set" , ( ) => { beforeEach ( async ( ) => { subjectQuantity = ether ( 1 ) ; usdcTransferInQuantity = await calculateUSDCTransferIn ( setToken , subjectQuantity , 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 have set the expected USDC externalPositionUnit" , async ( ) => { const expectedExternalPositionUnit = preciseDiv ( usdcTransferInQuantity , subjectQuantity ) ; await subject ( ) ; const externalPositionUnit = await setToken . getExternalPositionRealUnit ( usdc . address , perpLeverageModule . address ) ; expect ( externalPositionUnit ) . to . be . closeTo ( expectedExternalPositionUnit , 1 ) ; } ) ; it ( "have the expected virtual token balance" , async ( ) => { const totalSupply = await setToken . totalSupply ( ) ; const initialBaseBalance = ( await perpLeverageModule . getPositionNotionalInfo ( subjectSetToken ) ) [ 0 ] . baseBalance ; await subject ( ) ; const finalBaseBalance = ( await perpLeverageModule . getPositionNotionalInfo ( subjectSetToken ) ) [ 0 ] . baseBalance ; const basePositionUnit = preciseDiv ( initialBaseBalance , totalSupply ) ; const baseTokenBoughtNotional = preciseMul ( basePositionUnit , subjectQuantity ) ; const expectedBaseBalance = initialBaseBalance . add ( baseTokenBoughtNotional ) ; expect ( finalBaseBalance ) . eq ( expectedBaseBalance ) ; } ) ; it ( "should deposit the expected amount into the Perp vault" , async ( ) => { 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 ) ; const expectedCollateralBalance = toUSDCDecimals ( initialCollateralBalance ) . add ( feeAdjustedTransferIn ) ; 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 [ components , equityFlows , debtFlows ] = await slippageIssuanceModule . callStatic . getRequiredComponentIssuanceUnitsOffChain ( subjectSetToken , subjectQuantity ) ; const expectedComponents = await setToken . getComponents ( ) ; const expectedEquityFlows = [ feeAdjustedTransferIn ] ; const expectedDebtFlows = [ ZERO ] ; expect ( expectedComponents [ 0 ] ) . to . eq ( components [ 0 ] ) ; expect ( expectedEquityFlows [ 0 ] ) . to . be . closeTo ( equityFlows [ 0 ] , 50 ) ; expect ( expectedDebtFlows [ 0 ] ) . to . eq ( debtFlows [ 0 ] ) ; } ) ; } ) ; describe ( "when minting multiple sets" , ( ) => { beforeEach ( async ( ) => { subjectQuantity = ether ( 2 ) ; usdcTransferInQuantity = await calculateUSDCTransferIn ( setToken , subjectQuantity , perpLeverageModule , perpSetup ) ; } ) ; it ( "should have set the expected USDC externalPositionUnit" , async ( ) => { const expectedExternalPositionUnit = preciseDiv ( usdcTransferInQuantity , subjectQuantity ) ; await subject ( ) ; const externalPositionUnit = await setToken . getExternalPositionRealUnit ( usdc . address , perpLeverageModule . address ) ; expect ( externalPositionUnit ) . to . be . closeTo ( expectedExternalPositionUnit , 1 ) ; } ) ; it ( "have the expected virtual token balance" , async ( ) => { const totalSupply = await setToken . totalSupply ( ) ; const initialBaseBalance = ( await perpLeverageModule . getPositionNotionalInfo ( subjectSetToken ) ) [ 0 ] . baseBalance ; await subject ( ) ; const finalBaseBalance = ( await perpLeverageModule . getPositionNotionalInfo ( subjectSetToken ) ) [ 0 ] . baseBalance ; const basePositionUnit = preciseDiv ( initialBaseBalance , totalSupply ) ; const baseTokenBoughtNotional = preciseMul ( basePositionUnit , subjectQuantity ) ; const expectedBaseBalance = initialBaseBalance . add ( baseTokenBoughtNotional ) ; expect ( finalBaseBalance ) . eq ( expectedBaseBalance ) ; } ) ; it ( "should deposit the expected amount into the Perp vault" , async ( ) => { 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 externalPositionUnit = preciseDiv ( usdcTransferInQuantity , subjectQuantity ) ; const feeAdjustedTransferIn = preciseMul ( issueQuantityWithFees , externalPositionUnit ) ; const expectedCollateralBalance = toUSDCDecimals ( initialCollateralBalance ) . add ( feeAdjustedTransferIn ) ; expect ( toUSDCDecimals ( finalCollateralBalance ) ) . to . be . closeTo ( expectedCollateralBalance , 2 ) ; } ) ; 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 initialCollateralBalance = ( await perpLeverageModule . getAccountInfo ( subjectSetToken ) ) . collateralBalance ; await subject ( ) ; const finalCollateralBalance = ( await perpLeverageModule . getAccountInfo ( subjectSetToken ) ) . collateralBalance ; const expectedCollateralBalance = toUSDCDecimals ( initialCollateralBalance ) . add ( feeAdjustedTransferIn ) ; expect ( toUSDCDecimals ( finalCollateralBalance ) ) . to . be . closeTo ( expectedCollateralBalance , 2 ) ; } ) ; it . skip ( "should not incur a premium" , async ( ) => { const issueQuantityWithFees = ( await slippageIssuanceModule . calculateTotalFees ( subjectSetToken , subjectQuantity , true ) ) [ 0 ] ; const externalPositionUnit = preciseDiv ( usdcTransferInQuantity , subjectQuantity ) ; const feeAdjustedTransferIn = preciseMul ( issueQuantityWithFees , externalPositionUnit ) ; const feeAdjustedExternalPositionUnit = preciseDiv ( feeAdjustedTransferIn , issueQuantityWithFees ) ; const totalSlippageAndFees = await calculateTotalSlippage ( setToken , subjectQuantity ) ; const totalSlippagePositionUnit = preciseDiv ( totalSlippageAndFees , subjectQuantity ) ; const feeAndSlippageAdjustedExternalPositionUnit = feeAdjustedExternalPositionUnit . sub ( toUSDCDecimals ( totalSlippagePositionUnit ) ) ; await subject ( ) ; const accountInfo = await perpLeverageModule . getAccountInfo ( subjectSetToken ) ; const spotPrice = await perpSetup . getSpotPrice ( baseToken ) ; const baseBalance = ( await perpLeverageModule . getPositionNotionalInfo ( subjectSetToken ) ) [ 0 ] . baseBalance ; const notionalBaseValue = preciseMul ( baseBalance , spotPrice ) ; const totalSetValue = notionalBaseValue . add ( accountInfo . netQuoteBalance ) . add ( accountInfo . collateralBalance ) . add ( accountInfo . owedRealizedPnl ) ; const valuePerSet = preciseDiv ( totalSetValue , await setToken . totalSupply ( ) ) ; const valuePerSetUSDC = toUSDCDecimals ( valuePerSet ) ; expect ( valuePerSetUSDC ) . eq ( feeAndSlippageAdjustedExternalPositionUnit ) ; } ) ; } ) ; describe ( "when flash-issuing at high margin (success)" , async ( ) => { beforeEach ( async ( ) => { subjectQuantity = ether ( 3 ) ; } ) ; it ( "~8.7X succeeds" , async ( ) => { const flashLeverage = await calculateFlashLeverage ( subjectSetToken , subjectQuantity ) ; await subject ( ) ; expect ( flashLeverage ) . to . be . gt ( ether ( 8 ) ) ; expect ( flashLeverage ) . to . be . lt ( ether ( 9 ) ) ; } ) ; } ) ; describe ( "when flash-issuing at high margin (failure)" , async ( ) => { beforeEach ( async ( ) => { subjectQuantity = ether ( 3.5 ) ; } ) ; it ( "~9.9X fails" , async ( ) => { const flashLeverage = await calculateFlashLeverage ( subjectSetToken , subjectQuantity ) ; await expect ( subject ( ) ) . to . be . revertedWith ( "CH_NEFCI" ) ; expect ( flashLeverage ) . to . be . gt ( ether ( 9 ) ) ; expect ( flashLeverage ) . to . be . lt ( ether ( 10 ) ) ; } ) ; } ) ; describe ( "when issuing after a liquidation" , async ( ) => { beforeEach ( async ( ) => { subjectQuantity = ether ( 1 ) ; 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 ( ) ; 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 ) ; } ) ; } ) ; } ) ; } ) ; describe ( "#redemption" , async ( ) => { let setToken ; let baseToken ; let redeemFee ; let depositQuantityUnit ; let usdcDefaultPositionUnit ; let usdcTransferOutQuantity ; let subjectSetToken ; let subjectQuantity ; let subjectCheckedComponents ; let subjectMaxTokenAmountsIn ; let subjectTo ; let subjectCaller ; let issueQuantity ; const initializeContracts = async function ( ) { usdcDefaultPositionUnit = usdcUnits ( 10 ) ; setToken = await setup . createSetToken ( [ usdc . address ] , [ usdcDefaultPositionUnit ] , [ perpLeverageModule . address , slippageIssuanceModule . address ] ) ; redeemFee = ether ( 0.005 ) ; await slippageIssuanceModule . initialize ( setToken . address , ether ( 0.02 ) , ether ( 0.005 ) , redeemFee , feeRecipient . address , ADDRESS_ZERO ) ; await perpLeverageModule . updateAllowedSetToken ( setToken . address , true ) ; await perpLeverageModule . initialize ( setToken . address ) ; await usdc . approve ( slippageIssuanceModule . address , usdcUnits ( 1000 ) ) ; } ; async function subject ( ) { return slippageIssuanceModule . connect ( subjectCaller . wallet ) . redeemWithSlippage ( subjectSetToken , subjectQuantity , subjectCheckedComponents , subjectMaxTokenAmountsIn , subjectTo ) ; } context ( "when there is only an external USDC position and redeem will take supply to 0" , async ( ) => { cacheBeforeEach ( initializeContracts ) ; beforeEach ( async ( ) => { issueQuantity = ether ( 1 ) ; await slippageIssuanceModule . issue ( setToken . address , issueQuantity , owner . address ) ; depositQuantityUnit = usdcUnits ( 10 ) ; await perpLeverageModule . deposit ( setToken . address , depositQuantityUnit ) ; baseToken = vETH . address ; await leverUp ( setToken , perpLeverageModule , perpSetup , owner , baseToken , 2 , ether ( .02 ) , true ) ; subjectSetToken = setToken . address ; subjectQuantity = issueQuantity ; subjectCheckedComponents = [ ] ; subjectMaxTokenAmountsIn = [ ] ; subjectTo = owner . address ; subjectCaller = owner ; usdcTransferOutQuantity = await calculateUSDCTransferOut ( setToken , subjectQuantity , 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 ( initialDefaultPositionUnit ) . eq ( finalDefaultPositionUnit ) ; } ) ; 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 ; expect ( initialExternalPositionUnit ) . not . eq ( finalExternalPositionUnit ) ; expect ( finalExternalPositionUnit ) . eq ( expectedExternalPositionUnit ) ; } ) ; it ( "should have the expected virtual token balance" , async ( ) => { const totalSupply = await setToken . totalSupply ( ) ; const initialBaseBalance = ( await perpLeverageModule . getPositionNotionalInfo ( subjectSetToken ) ) [ 0 ] . baseBalance ; await subject ( ) ; const finalBaseBalance = ( await perpLeverageModule . getPositionNotionalInfo ( subjectSetToken ) ) [ 0 ] . baseBalance ; const basePositionUnit = preciseDiv ( initialBaseBalance , totalSupply ) ; const baseTokenBoughtNotional = preciseMul ( basePositionUnit , subjectQuantity ) ; const expectedBaseBalance = initialBaseBalance . sub ( baseTokenBoughtNotional ) ; expect ( finalBaseBalance ) . eq ( expectedBaseBalance ) ; } ) ; it ( "should not have updated the setToken USDC token balance" , async ( ) => { const initialUSDCBalance = await usdc . balanceOf ( subjectSetToken ) ; await subject ( ) ; const finalUSDCBalance = await usdc . balanceOf ( subjectSetToken ) ; expect ( initialUSDCBalance ) . eq ( finalUSDCBalance ) ; } ) ; describe ( "withdrawal" , ( ) => { let feeAdjustedTransferOutUSDC ; let realizedPnlUSDC ; beforeEach ( async ( ) => { ( { feeAdjustedTransferOutUSDC , realizedPnlUSDC } = await calculateRedemptionData ( subjectSetToken , subjectQuantity , usdcTransferOutQuantity ) ) ; } ) ; it ( "should withdraw the expected amount from the Perp vault" , async ( ) => { const initialCollateralBalance = ( await perpLeverageModule . getAccountInfo ( subjectSetToken ) ) . collateralBalance ; await subject ( ) ; const finalCollateralBalance = ( await perpLeverageModule . getAccountInfo ( subjectSetToken ) ) . collateralBalance ; const initialCollateralBalanceUSDC = toUSDCDecimals ( initialCollateralBalance ) ; const finalCollateralBalanceUSDC = toUSDCDecimals ( finalCollateralBalance ) ; const expectedCollateralBalanceUSDC = initialCollateralBalanceUSDC . sub ( feeAdjustedTransferOutUSDC ) . add ( realizedPnlUSDC ) ; expect ( finalCollateralBalanceUSDC ) . to . be . closeTo ( expectedCollateralBalanceUSDC , 1 ) ; } ) ; it ( "should not update the setToken USDC token balance" , async ( ) => { const initialUSDCBalance = await usdc . balanceOf ( subjectSetToken ) ; await subject ( ) ; const finalUSDCBalance = await usdc . balanceOf ( subjectSetToken ) ; expect ( initialUSDCBalance ) . eq ( 0 ) ; expect ( finalUSDCBalance ) . eq ( initialUSDCBalance ) ; } ) ; it ( "should have transferred expected USDC to set token holder" , async ( ) => { const initialOwnerUSDCBalance = await usdc . balanceOf ( subjectCaller . address ) ; await subject ( ) ; const finalOwnerUSDCBalance = await usdc . balanceOf ( subjectCaller . address ) ; const expectedUSDCBalance = initialOwnerUSDCBalance . add ( feeAdjustedTransferOutUSDC ) ; expect ( finalOwnerUSDCBalance ) . eq ( expectedUSDCBalance ) ; } ) ; } ) ; } ) ; context ( "when there is only an external USDC position and redeem will take supply to 1" , async ( ) => { let depositQuantityUnit ; let usdcTransferOutQuantity ; cacheBeforeEach ( initializeContracts ) ; beforeEach ( async ( ) => { issueQuantity = ether ( 2 ) ; await slippageIssuanceModule . issue ( setToken . address , issueQuantity , owner . address ) ; depositQuantityUnit = usdcDefaultPositionUnit ; await perpLeverageModule . deposit ( setToken . address , depositQuantityUnit ) ; baseToken = vETH . address ; await leverUp ( setToken , perpLeverageModule , perpSetup , owner , baseToken , 2 , ether ( .02 ) , true ) ; subjectSetToken = setToken . address ; subjectQuantity = ether ( 1 ) ; subjectCheckedComponents = [ ] ; subjectMaxTokenAmountsIn = [ ] ; subjectTo = owner . address ; subjectCaller = owner ; usdcTransferOutQuantity = await calculateUSDCTransferOut ( setToken , subjectQuantity , 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 ( initialDefaultPositionUnit ) . eq ( finalDefaultPositionUnit ) ; } ) ; it ( "should update 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 = preciseDiv ( usdcTransferOutQuantity , subjectQuantity ) ; expect ( initialExternalPositionUnit ) . eq ( usdcDefaultPositionUnit ) ; expect ( finalExternalPositionUnit ) . to . be . closeTo ( expectedExternalPositionUnit , 1 ) ; } ) ; it ( "should have the expected virtual token balance" , async ( ) => { const totalSupply = await setToken . totalSupply ( ) ; const initialBaseBalance = ( await perpLeverageModule . getPositionNotionalInfo ( subjectSetToken ) ) [ 0 ] . baseBalance ; await subject ( ) ; const finalBaseBalance = ( await perpLeverageModule . getPositionNotionalInfo ( subjectSetToken ) ) [ 0 ] . baseBalance ; const basePositionUnit = preciseDiv ( initialBaseBalance , totalSupply ) ; const baseTokenSoldNotional = preciseMul ( basePositionUnit , subjectQuantity ) ; 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 [ components , equityFlows , debtFlows ] = await slippageIssuanceModule . callStatic . getRequiredComponentRedemptionUnitsOffChain ( subjectSetToken , subjectQuantity ) ; const expectedComponents = await setToken . getComponents ( ) ; const expectedEquityFlows = [ feeAdjustedTransferOut ] ; const expectedDebtFlows = [ ZERO ] ; expect ( expectedComponents [ 0 ] ) . to . eq ( components [ 0 ] ) ; expect ( expectedEquityFlows [ 0 ] ) . to . be . closeTo ( equityFlows [ 0 ] , 50 ) ; expect ( expectedDebtFlows [ 0 ] ) . to . eq ( debtFlows [ 0 ] ) ; } ) ; it . skip ( "should not incur a premium" , async ( ) => { const redeemQuantityWithFees = ( await slippageIssuanceModule . calculateTotalFees ( subjectSetToken , subjectQuantity , false ) ) [ 0 ] ; const externalPositionUnit = preciseDiv ( usdcTransferOutQuantity , subjectQuantity ) ; const feeAdjustedTransferOut = preciseMul ( redeemQuantityWithFees , externalPositionUnit ) ; const feeAdjustedExternalPositionUnit = preciseDiv ( feeAdjustedTransferOut , redeemQuantityWithFees ) ; const totalSlippageAndFees = await calculateTotalSlippage ( setToken , subjectQuantity ) ; const totalSlippagePositionUnit = preciseDiv ( totalSlippageAndFees , subjectQuantity ) ; const feeAndSlippageAdjustedExternalPositionUnit = feeAdjustedExternalPositionUnit . add ( toUSDCDecimals ( totalSlippagePositionUnit ) ) ; await subject ( ) ; const accountInfo = await perpLeverageModule . getAccountInfo ( subjectSetToken ) ; const spotPrice = await perpSetup . getSpotPrice ( baseToken ) ; const baseBalance = ( await perpLeverageModule . getPositionNotionalInfo ( subjectSetToken ) ) [ 0 ] . baseBalance ; const notionalBaseValue = preciseMul ( baseBalance , spotPrice ) ; const totalSetValue = notionalBaseValue . add ( accountInfo . netQuoteBalance ) . add ( accountInfo . collateralBalance ) . add ( accountInfo . owedRealizedPnl ) ; const valuePerSet = preciseDiv ( totalSetValue , await setToken . totalSupply ( ) ) ; const valuePerSetUSDC = toUSDCDecimals ( valuePerSet ) ; expect ( valuePerSetUSDC ) . eq ( feeAndSlippageAdjustedExternalPositionUnit ) ; } ) ; describe ( "withdrawal" , ( ) => { let feeAdjustedTransferOutUSDC ; let realizedPnlUSDC ; beforeEach ( async ( ) => { ( { feeAdjustedTransferOutUSDC , realizedPnlUSDC } = await calculateRedemptionData ( subjectSetToken , subjectQuantity , usdcTransferOutQuantity ) ) ; } ) ; it ( "should withdraw the expected amount from the Perp vault" , async ( ) => { const initialCollateralBalance = ( await perpLeverageModule . getAccountInfo ( subjectSetToken ) ) . collateralBalance ; await subject ( ) ; const finalCollateralBalance = ( await perpLeverageModule . getAccountInfo ( subjectSetToken ) ) . collateralBalance ; const initialCollateralBalanceUSDC = toUSDCDecimals ( initialCollateralBalance ) ; const finalCollateralBalanceUSDC = toUSDCDecimals ( finalCollateralBalance ) ; const expectedCollateralBalanceUSDC = initialCollateralBalanceUSDC . sub ( feeAdjustedTransferOutUSDC ) . add ( realizedPnlUSDC ) ; expect ( finalCollateralBalanceUSDC ) . to . be . closeTo ( expectedCollateralBalanceUSDC , 1 ) ; } ) ; it ( "should not update the setToken USDC token balance" , async ( ) => { const initialUSDCBalance = await usdc . balanceOf ( subjectSetToken ) ; await subject ( ) ; const finalUSDCBalance = await usdc . balanceOf ( subjectSetToken ) ; expect ( initialUSDCBalance ) . eq ( 0 ) ; expect ( finalUSDCBalance ) . eq ( initialUSDCBalance ) ; } ) ; it ( "should have transferred expected USDC to set token holder" , async ( ) => { const initialOwnerUSDCBalance = await usdc . balanceOf ( subjectCaller . address ) ; await subject ( ) ; const finalOwnerUSDCBalance = await usdc . balanceOf ( subjectCaller . address ) ; const expectedUSDCBalance = initialOwnerUSDCBalance . add ( feeAdjustedTransferOutUSDC ) ; expect ( finalOwnerUSDCBalance ) . to . be . closeTo ( expectedUSDCBalance , 1 ) ; } ) ; it ( "should remove the module when dust is in the account and be able to add module back" , async ( ) => { await subject ( ) ; const initialModules = await setToken . getModules ( ) ; expect ( initialModules . includes ( perpLeverageModule . address ) ) . eq ( true ) ; const { baseUnit : initialBaseUnit } = ( await perpLeverageModule . getPositionUnitInfo ( subjectSetToken ) ) [ 0 ] ; await perpLeverageModule . connect ( owner . wallet ) . trade ( subjectSetToken , baseToken , initialBaseUnit . mul ( - 1 ) , ZERO ) ; const positionInfo = await perpLeverageModule . getPositionUnitInfo ( subjectSetToken ) ; const freeCollateral = await perpSetup . vault . getFreeCollateral ( subjectSetToken ) ; const freeCollateralPositionUnit = preciseDiv ( freeCollateral , await setToken . totalSupply ( ) ) ; await perpLeverageModule . connect ( owner . wallet ) . withdraw ( subjectSetToken , freeCollateralPositionUnit ) ; const { collateralBalance : finalCollateralBalance } = await perpLeverageModule . getAccountInfo ( subjectSetToken ) ; await setToken . removeModule ( perpLeverageModule . address ) ; const finalModules = await setToken . getModules ( ) ; expect ( finalModules . includes ( perpLeverageModule . address ) ) . eq ( false ) ; expect ( positionInfo . length ) . eq ( 0 ) ; expect ( toUSDCDecimals ( finalCollateralBalance ) ) . eq ( 1 ) ; await setToken . connect ( owner . wallet ) . addModule ( perpLeverageModule . address ) ; await perpLeverageModule . updateAllowedSetToken ( setToken . address , true ) ; await perpLeverageModule . initialize ( setToken . address ) ; const restoredModules = await setToken . getModules ( ) ; expect ( restoredModules . includes ( perpLeverageModule . address ) ) . eq ( true ) ; await perpLeverageModule . deposit ( setToken . address , usdcUnits ( 5 ) ) ; } ) ; } ) ; describe ( "when redeeming after a liquidation" , async ( ) => { beforeEach ( async ( ) => { subjectQuantity = ether ( 1 ) ; 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 ; 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 ( ) => { await leverUp ( setToken , perpLeverageModule , perpSetup , owner , baseToken , 6 , ether ( .02 ) , true ) ; await perpSetup . setBaseTokenOraclePrice ( vETH , usdcUnits ( 5.0 ) ) ; await perpSetup . clearingHouse . connect ( maker . wallet ) . openPosition ( { baseToken : vETH . address , isBaseToQuote : true , isExactInput : false , 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 ) ; expect ( collateralBalance ) . gt ( 1 ) ; expect ( freeCollateral ) . eq ( 0 ) ; expect ( accountValue ) . lt ( - 1 ) ; await setToken . removeModule ( perpLeverageModule . address ) ; const finalModules = await setToken . getModules ( ) ; expect ( finalModules . includes ( perpLeverageModule . address ) ) . eq ( false ) ; } ) ; } ) ; } ) ; } ) ; } ) ;