@@ -190,6 +190,73 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest {
190190 vm.stopPrank ();
191191 }
192192
193+ /// @notice Helper to set up a leveraged position for any user
194+ /// @dev More flexible version that accepts owner, account, and vault parameters
195+ function _setupLeveragedPositionFor (
196+ address owner ,
197+ address account ,
198+ address collateralAsset ,
199+ address collateralVault ,
200+ address borrowVault ,
201+ uint256 collateralAmount ,
202+ uint256 borrowAmount
203+ ) internal {
204+ vm.startPrank (owner);
205+ IERC20 (collateralAsset).approve (collateralVault, type (uint256 ).max);
206+ EVC.enableCollateral (account, collateralVault);
207+ EVC.enableController (account, borrowVault);
208+ IERC4626 (collateralVault).deposit (collateralAmount, account);
209+ vm.stopPrank ();
210+
211+ vm.prank (account);
212+ IBorrowing (borrowVault).borrow (borrowAmount, owner);
213+ }
214+
215+ /// @notice Setup approvals for a specific user to close their position
216+ function _setupClosePositionApprovalsFor (
217+ address owner ,
218+ address account ,
219+ address collateralVault ,
220+ address repaymentAsset
221+ ) internal {
222+ vm.startPrank (owner);
223+
224+ // Approve vault shares from subaccount
225+ IEVC.BatchItem[] memory items = new IEVC.BatchItem [](1 );
226+ items[0 ] = IEVC.BatchItem ({
227+ onBehalfOfAccount: account,
228+ targetContract: collateralVault,
229+ value: 0 ,
230+ data: abi.encodeCall (IERC20 .approve, (address (closePositionWrapper), type (uint256 ).max))
231+ });
232+ EVC.batch (items);
233+
234+ // Approve vault shares for settlement
235+ IEVault (collateralVault).approve (COW_SETTLEMENT.vaultRelayer (), type (uint256 ).max);
236+
237+ // Approve wrapper to spend repayment asset
238+ IERC20 (repaymentAsset).approve (address (closePositionWrapper), type (uint256 ).max);
239+
240+ vm.stopPrank ();
241+ }
242+
243+ /// @notice Create permit signature for any user
244+ function _createPermitSignatureFor (
245+ CowEvcClosePositionWrapper.ClosePositionParams memory params ,
246+ uint256 userPrivateKey
247+ ) internal returns (bytes memory ) {
248+ ecdsa.setPrivateKey (userPrivateKey);
249+ return ecdsa.signPermit (
250+ params.owner,
251+ address (closePositionWrapper),
252+ uint256 (uint160 (address (closePositionWrapper))),
253+ 0 ,
254+ params.deadline,
255+ 0 ,
256+ closePositionWrapper.getSignedCalldata (params)
257+ );
258+ }
259+
193260 /// @notice Create settlement data for closing a leveraged position
194261 /// @dev Sells vault shares to buy repayment token (WETH)
195262 function getClosePositionSettlement (
@@ -478,4 +545,151 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest {
478545 assertEq (IEVault (EWETH).debtOf (account), 0 , "User should have no debt after closing " );
479546 assertEq (debtBefore, borrowAmount, "User should have started with debt " );
480547 }
548+
549+ /// @notice Test that the wrapper can handle being called three times in the same chain
550+ /// @dev Two users close positions in the same direction (long SUSDS), one user closes opposite (long WETH)
551+ function test_ClosePositionWrapper_ThreeUsers_TwoSameOneOpposite () external {
552+ vm.skip (bytes (forkRpcUrl).length == 0 );
553+
554+ // Configure vault LTVs for both directions
555+ vm.startPrank (IEVault (ESUSDS).governorAdmin ());
556+ IEVault (ESUSDS).setLTV (EWETH, 0.9e4 , 0.9e4 , 0 );
557+ vm.stopPrank ();
558+
559+ // Setup accounts
560+ address account1 = address (uint160 (user) ^ 1 );
561+ address account2 = address (uint160 (user2) ^ 1 );
562+ address account3 = address (uint160 (user3) ^ 1 );
563+
564+ // Setup User1: Long SUSDS (SUSDS collateral, WETH debt). ~1 ETH debt
565+ deal (SUSDS, user, 10000 ether);
566+ _setupLeveragedPositionFor (user, account1, SUSDS, ESUSDS, EWETH, 3500 ether, 1 ether);
567+
568+ // Setup User2: Long SUSDS (SUSDS collateral, WETH debt). ~3 ETH debt
569+ deal (SUSDS, user2, 10000 ether);
570+ _setupLeveragedPositionFor (user2, account2, SUSDS, ESUSDS, EWETH, 10000 ether, 3 ether);
571+
572+ // Setup User3: Long WETH (WETH collateral, SUSDS debt). ~5000 SUSDS debt
573+ deal (WETH, user3, 3 ether);
574+ _setupLeveragedPositionFor (user3, account3, WETH, EWETH, ESUSDS, 3 ether, 5000 ether);
575+
576+ // Verify positions exist
577+ assertEq (IEVault (EWETH).debtOf (account1), 1 ether, "User1 should have WETH debt " );
578+ assertEq (IEVault (EWETH).debtOf (account2), 3 ether, "User2 should have WETH debt " );
579+ assertEq (IEVault (ESUSDS).debtOf (account3), 5000 ether, "User3 should have SUSDS debt " );
580+
581+ // Setup approvals for all users
582+ _setupClosePositionApprovalsFor (user, account1, ESUSDS, WETH);
583+ _setupClosePositionApprovalsFor (user2, account2, ESUSDS, WETH);
584+ _setupClosePositionApprovalsFor (user3, account3, EWETH, SUSDS);
585+
586+ // Create params for all users
587+ CowEvcClosePositionWrapper.ClosePositionParams memory params1 = CowEvcClosePositionWrapper.ClosePositionParams ({
588+ owner: user,
589+ account: account1,
590+ deadline: block .timestamp + 1 hours,
591+ borrowVault: EWETH,
592+ collateralVault: ESUSDS,
593+ collateralAmount: 2550 ether,
594+ repayAmount: 1.001 ether,
595+ kind: GPv2Order.KIND_BUY
596+ });
597+
598+ CowEvcClosePositionWrapper.ClosePositionParams memory params2 = CowEvcClosePositionWrapper.ClosePositionParams ({
599+ owner: user2,
600+ account: account2,
601+ deadline: block .timestamp + 1 hours,
602+ borrowVault: EWETH,
603+ collateralVault: ESUSDS,
604+ collateralAmount: 7600 ether,
605+ repayAmount: 3.003 ether,
606+ kind: GPv2Order.KIND_BUY
607+ });
608+
609+ CowEvcClosePositionWrapper.ClosePositionParams memory params3 = CowEvcClosePositionWrapper.ClosePositionParams ({
610+ owner: user3,
611+ account: account3,
612+ deadline: block .timestamp + 1 hours,
613+ borrowVault: ESUSDS,
614+ collateralVault: EWETH,
615+ collateralAmount: 2.1 ether,
616+ repayAmount: 5005 ether,
617+ kind: GPv2Order.KIND_BUY
618+ });
619+
620+ // Create permit signatures for all users
621+ bytes memory permitSignature1 = _createPermitSignatureFor (params1, privateKey);
622+ bytes memory permitSignature2 = _createPermitSignatureFor (params2, privateKey2);
623+ bytes memory permitSignature3 = _createPermitSignatureFor (params3, privateKey3);
624+
625+ // Create settlement with all three trades
626+ uint32 validTo = uint32 (block .timestamp + 1 hours);
627+
628+ address [] memory tokens = new address [](4 );
629+ tokens[0 ] = SUSDS;
630+ tokens[1 ] = WETH;
631+ tokens[2 ] = ESUSDS;
632+ tokens[3 ] = EWETH;
633+
634+ uint256 [] memory clearingPrices = new uint256 [](4 );
635+ clearingPrices[0 ] = 1 ether ; // SUSDS price
636+ clearingPrices[1 ] = 2500 ether ; // WETH price
637+ clearingPrices[2 ] = 0.99 ether ; // eSUSDS price
638+ clearingPrices[3 ] = 2495 ether ; // eWETH price
639+
640+ ICowSettlement.Trade[] memory trades = new ICowSettlement.Trade [](3 );
641+ (trades[0 ],,) =
642+ setupCowOrder (tokens, 2 , 1 , params1.collateralAmount, params1.repayAmount, validTo, user, user, true );
643+ (trades[1 ],,) =
644+ setupCowOrder (tokens, 2 , 1 , params2.collateralAmount, params2.repayAmount, validTo, user2, user2, true );
645+ (trades[2 ],,) =
646+ setupCowOrder (tokens, 3 , 0 , params3.collateralAmount, params3.repayAmount, validTo, user3, user3, true );
647+
648+ // Setup interactions
649+ ICowSettlement.Interaction[][3 ] memory interactions;
650+ interactions[0 ] = new ICowSettlement.Interaction [](0 );
651+ interactions[1 ] = new ICowSettlement.Interaction [](3 );
652+ interactions[2 ] = new ICowSettlement.Interaction [](0 );
653+
654+ // We pull the money out of the euler vaults
655+ interactions[1 ][0 ] = getWithdrawInteraction (
656+ ESUSDS, (params1.repayAmount + params2.repayAmount) * clearingPrices[1 ] / clearingPrices[0 ]
657+ );
658+ interactions[1 ][1 ] = getWithdrawInteraction (EWETH, params3.repayAmount * clearingPrices[0 ] / clearingPrices[1 ]);
659+
660+ // We swap. We only need to swap the difference of the 3 closes (since coincidence of wants)
661+ // It comes out to 5000 SUSDS needs to become WETH
662+ interactions[1 ][2 ] = getSwapInteraction (SUSDS, WETH, 5000 ether);
663+
664+ // Encode settlement data
665+ bytes memory settleData = abi.encodeCall (ICowSettlement.settle, (tokens, clearingPrices, trades, interactions));
666+
667+ // Chain wrapper data
668+ bytes memory wrapper1Data = abi.encode (params1, permitSignature1);
669+ bytes memory wrapper2Data = abi.encode (params2, permitSignature2);
670+ bytes memory wrapper3Data = abi.encode (params3, permitSignature3);
671+
672+ bytes memory wrapperData = abi.encodePacked (
673+ uint16 (wrapper1Data.length ),
674+ wrapper1Data,
675+ address (closePositionWrapper),
676+ uint16 (wrapper2Data.length ),
677+ wrapper2Data,
678+ address (closePositionWrapper),
679+ uint16 (wrapper3Data.length ),
680+ wrapper3Data
681+ );
682+
683+ // Execute wrapped settlement
684+ address [] memory targets = new address [](1 );
685+ bytes [] memory datas = new bytes [](1 );
686+ targets[0 ] = address (closePositionWrapper);
687+ datas[0 ] = abi.encodeCall (closePositionWrapper.wrappedSettle, (settleData, wrapperData));
688+ solver.runBatch (targets, datas);
689+
690+ // Verify all positions closed successfully
691+ assertEq (IEVault (EWETH).debtOf (account1), 0 , "User1 should have no WETH debt after closing " );
692+ assertEq (IEVault (EWETH).debtOf (account2), 0 , "User2 should have no WETH debt after closing " );
693+ assertEq (IEVault (ESUSDS).debtOf (account3), 0 , "User3 should have no SUSDS debt after closing " );
694+ }
481695}
0 commit comments