@@ -24,6 +24,7 @@ contract TestProtocolVault is Base {
2424 error EnforcedPause ();
2525 error NotEnoughUnclaimedAssets (uint256 amount );
2626 error VaultClosed ();
27+ error NotEnoughCrossChainFee ();
2728
2829 function setUp () public override {
2930 super .setUp ();
@@ -624,4 +625,230 @@ contract TestProtocolVault is Base {
624625 protocolVault.deposit {value: nativeFee}(depositParams);
625626 vm.stopPrank ();
626627 }
628+
629+ function testWithdrawCrossChainFee () public {
630+ // Setup: Create some cross chain fees
631+ uint256 periodId;
632+ bytes32 vaultId;
633+ uint256 amount = 100e6 ;
634+ uint256 ccFee = 20 ; // Total fee
635+
636+ bytes32 [] memory requestIds = new bytes32 [](2 );
637+ requestIds[0 ] = keccak256 (abi.encode (0 ));
638+ requestIds[1 ] = keccak256 (abi.encode (1 ));
639+
640+ bytes memory signature = _getUpdateUnclaimedSignature (evmChainId, periodId, vaultId, requestIds);
641+ vm.deal (address (bVaultCrossChainManager), 10 ether);
642+
643+ // Add claim info on ledger
644+ svLedger.setLpClaimInfo (requestIds[0 ], userA_id, amount);
645+ svLedger.setLpClaimInfo (requestIds[1 ], userB_id, amount);
646+
647+ // Process unclaimed update with fees
648+ vm.prank (operator);
649+ svLedger.updateUnclaimed (evmChainId, periodId, ccFee, vaultId, requestIds, signature);
650+ verifyPackets (srcEid, address (aVaultCrossChainManager));
651+
652+ // Calculate expected accumulated fee: feePerUser = 20/2 = 10, actualTotalFee = 10*2 = 20
653+ uint256 expectedAccumulatedFee = (ccFee / requestIds.length ) * requestIds.length ;
654+ assertEq (protocolVault.claimCrossChainFee (), expectedAccumulatedFee, "Cross chain fee should be accumulated correctly " );
655+
656+ // Mint tokens to vault to represent collected fees
657+ mockToken.mint (address (protocolVault), expectedAccumulatedFee);
658+
659+ // Owner withdraws part of the fees
660+ uint256 withdrawAmount = 15 ;
661+ uint256 ownerBalanceBefore = mockToken.balanceOf (owner);
662+
663+ vm.prank (owner);
664+ protocolVault.withdrawToken (address (mockToken), owner, withdrawAmount);
665+
666+ // Verify withdrawal
667+ uint256 ownerBalanceAfter = mockToken.balanceOf (owner);
668+ assertEq (ownerBalanceAfter - ownerBalanceBefore, withdrawAmount, "Owner should receive withdrawn amount " );
669+ assertEq (protocolVault.claimCrossChainFee (), expectedAccumulatedFee - withdrawAmount, "Cross chain fee should be reduced after withdrawal " );
670+ }
671+
672+ function testWithdrawCrossChainFeeExceedsAvailable () public {
673+ // Setup: Create some cross chain fees
674+ uint256 periodId;
675+ bytes32 vaultId;
676+ uint256 amount = 100e6 ;
677+ uint256 ccFee = 10 ;
678+
679+ bytes32 [] memory requestIds = new bytes32 [](1 );
680+ requestIds[0 ] = keccak256 (abi.encode (0 ));
681+
682+ bytes memory signature = _getUpdateUnclaimedSignature (evmChainId, periodId, vaultId, requestIds);
683+ vm.deal (address (bVaultCrossChainManager), 10 ether);
684+
685+ svLedger.setLpClaimInfo (requestIds[0 ], userA_id, amount);
686+
687+ vm.prank (operator);
688+ svLedger.updateUnclaimed (evmChainId, periodId, ccFee, vaultId, requestIds, signature);
689+ verifyPackets (srcEid, address (aVaultCrossChainManager));
690+
691+ uint256 expectedAccumulatedFee = ccFee; // Only 1 user, so actualTotalFee = ccFee
692+ assertEq (protocolVault.claimCrossChainFee (), expectedAccumulatedFee, "Cross chain fee should be accumulated " );
693+
694+ // Try to withdraw more than available
695+ uint256 excessiveWithdrawAmount = expectedAccumulatedFee + 1 ;
696+
697+ vm.prank (owner);
698+ vm.expectRevert (abi.encodeWithSelector (NotEnoughCrossChainFee.selector ));
699+ protocolVault.withdrawToken (address (mockToken), owner, excessiveWithdrawAmount);
700+ }
701+
702+ function testMultipleFeeAccumulationAndWithdrawal () public {
703+ uint256 periodId;
704+ bytes32 vaultId;
705+ uint256 amount = 100e6 ;
706+
707+ // First batch of fees
708+ uint256 ccFee1 = 12 ;
709+ bytes32 [] memory requestIds1 = new bytes32 [](3 );
710+ requestIds1[0 ] = keccak256 (abi.encode (0 ));
711+ requestIds1[1 ] = keccak256 (abi.encode (1 ));
712+ requestIds1[2 ] = keccak256 (abi.encode (2 ));
713+
714+ bytes memory signature1 = _getUpdateUnclaimedSignature (evmChainId, periodId, vaultId, requestIds1);
715+ vm.deal (address (bVaultCrossChainManager), 10 ether);
716+
717+ for (uint256 i = 0 ; i < requestIds1.length ; i++ ) {
718+ svLedger.setLpClaimInfo (requestIds1[i], userA_id, amount);
719+ }
720+
721+ vm.prank (operator);
722+ svLedger.updateUnclaimed (evmChainId, periodId, ccFee1, vaultId, requestIds1, signature1);
723+ verifyPackets (srcEid, address (aVaultCrossChainManager));
724+
725+ uint256 expectedFee1 = (ccFee1 / requestIds1.length ) * requestIds1.length ; // 4 * 3 = 12
726+ assertEq (protocolVault.claimCrossChainFee (), expectedFee1, "First fee accumulation should be correct " );
727+
728+ // Second batch of fees
729+ uint256 ccFee2 = 21 ;
730+ bytes32 [] memory requestIds2 = new bytes32 [](2 );
731+ requestIds2[0 ] = keccak256 (abi.encode (3 ));
732+ requestIds2[1 ] = keccak256 (abi.encode (4 ));
733+
734+ bytes memory signature2 = _getUpdateUnclaimedSignature (evmChainId, periodId, vaultId, requestIds2);
735+
736+ for (uint256 i = 0 ; i < requestIds2.length ; i++ ) {
737+ svLedger.setLpClaimInfo (requestIds2[i], userB_id, amount);
738+ }
739+
740+ vm.prank (operator);
741+ svLedger.updateUnclaimed (evmChainId, periodId, ccFee2, vaultId, requestIds2, signature2);
742+ verifyPackets (srcEid, address (aVaultCrossChainManager));
743+
744+ uint256 expectedFee2 = (ccFee2 / requestIds2.length ) * requestIds2.length ; // 10 * 2 = 20
745+ uint256 totalExpectedFee = expectedFee1 + expectedFee2; // 12 + 20 = 32
746+ assertEq (protocolVault.claimCrossChainFee (), totalExpectedFee, "Total fee accumulation should be correct " );
747+
748+ // Mint tokens to represent collected fees
749+ mockToken.mint (address (protocolVault), totalExpectedFee);
750+
751+ // Owner withdraws all fees
752+ uint256 ownerBalanceBefore = mockToken.balanceOf (owner);
753+
754+ vm.prank (owner);
755+ protocolVault.withdrawToken (address (mockToken), owner, totalExpectedFee);
756+
757+ // Verify complete withdrawal
758+ uint256 ownerBalanceAfter = mockToken.balanceOf (owner);
759+ assertEq (ownerBalanceAfter - ownerBalanceBefore, totalExpectedFee, "Owner should receive all fees " );
760+ assertEq (protocolVault.claimCrossChainFee (), 0 , "Cross chain fee should be zero after full withdrawal " );
761+ }
762+
763+ function testWithdrawNativeToken () public {
764+ // Send some native tokens to the vault
765+ uint256 nativeAmount = 1 ether ;
766+ vm.deal (address (protocolVault), nativeAmount);
767+
768+ uint256 ownerBalanceBefore = owner.balance;
769+
770+ // Withdraw native tokens
771+ vm.prank (owner);
772+ protocolVault.withdrawToken (address (0 ), owner, nativeAmount);
773+
774+ uint256 ownerBalanceAfter = owner.balance;
775+ assertEq (ownerBalanceAfter - ownerBalanceBefore, nativeAmount, "Owner should receive native tokens " );
776+ assertEq (address (protocolVault).balance, 0 , "Vault should have no native tokens left " );
777+ }
778+
779+ function testSingleUserCcFeeCollectionAndWithdrawal () public {
780+ // Setup: Create cross chain fee scenario with single user
781+ uint256 periodId;
782+ bytes32 vaultId;
783+ uint256 userAsset = 1000e6 ; // User has 1000 USDC to claim
784+ uint256 ccFee = 50 ; // 50 USDC cross chain fee
785+
786+ bytes32 [] memory requestIds = new bytes32 [](1 );
787+ requestIds[0 ] = keccak256 (abi.encode ("singleUser " ));
788+
789+ bytes memory signature = _getUpdateUnclaimedSignature (evmChainId, periodId, vaultId, requestIds);
790+ vm.deal (address (bVaultCrossChainManager), 10 ether);
791+
792+ // Add claim info for single user
793+ svLedger.setLpClaimInfo (requestIds[0 ], userA_id, userAsset);
794+
795+ // Process unclaimed update with fees
796+ vm.prank (operator);
797+ svLedger.updateUnclaimed (evmChainId, periodId, ccFee, vaultId, requestIds, signature);
798+ verifyPackets (srcEid, address (aVaultCrossChainManager));
799+
800+ // For single user: feePerUser = ccFee / 1 = 50, actualTotalFee = 50 * 1 = 50
801+ uint256 expectedFeePerUser = ccFee; // 50
802+ uint256 expectedActualTotalFee = ccFee; // 50 (same as ccFee for single user)
803+ uint256 expectedUserAssets = userAsset - expectedFeePerUser; // 1000 - 50 = 950
804+
805+ // Verify user claim info (asset should be reduced by fee)
806+ UserClaimedInfo memory userClaimedInfo = protocolVault.getUserClaimedInfo (userA_id);
807+ assertEq (userClaimedInfo.unClaimedAssets, expectedUserAssets, "User assets should be reduced by ccFee " );
808+ assertEq (userClaimedInfo.requestIds.length , 1 , "User should have 1 request ID " );
809+ assertEq (userClaimedInfo.requestIds[0 ], requestIds[0 ], "Request ID should match " );
810+
811+ // Verify protocol vault accumulated the correct fee
812+ assertEq (protocolVault.claimCrossChainFee (), expectedActualTotalFee, "Protocol should collect exactly the ccFee amount " );
813+
814+ // Mint tokens to vault to represent the collected fees
815+ mockToken.mint (address (protocolVault), expectedActualTotalFee);
816+
817+ // Owner withdraws the collected fees
818+ uint256 ownerBalanceBefore = mockToken.balanceOf (owner);
819+
820+ vm.prank (owner);
821+ protocolVault.withdrawToken (address (mockToken), owner, expectedActualTotalFee);
822+
823+ // Verify owner received the fees
824+ uint256 ownerBalanceAfter = mockToken.balanceOf (owner);
825+ assertEq (ownerBalanceAfter - ownerBalanceBefore, expectedActualTotalFee, "Owner should receive all collected fees " );
826+
827+ // Verify protocol vault fee counter is reset
828+ assertEq (protocolVault.claimCrossChainFee (), 0 , "Cross chain fee should be zero after withdrawal " );
829+
830+ // Verify the fee amount calculation
831+ assertEq (expectedActualTotalFee, ccFee, "For single user, actualTotalFee should equal original ccFee " );
832+
833+ // Additional verification: User can still claim their remaining assets
834+ mockToken.mint (address (protocolVault), expectedUserAssets);
835+
836+ uint256 userBalanceBefore = mockToken.balanceOf (userA);
837+ ClaimParams memory claimParams = ClaimParams ({
838+ roleType: RoleType.LP,
839+ token: address (mockToken),
840+ brokerHash: ORDERLY_BROKER
841+ });
842+
843+ vm.prank (userA);
844+ protocolVault.claim (claimParams);
845+
846+ uint256 userBalanceAfter = mockToken.balanceOf (userA);
847+ assertEq (userBalanceAfter - userBalanceBefore, expectedUserAssets, "User should receive assets minus fee " );
848+
849+ // Final verification: User claim info should be cleared
850+ userClaimedInfo = protocolVault.getUserClaimedInfo (userA_id);
851+ assertEq (userClaimedInfo.unClaimedAssets, 0 , "User unclaimed assets should be zero after claim " );
852+ assertEq (userClaimedInfo.requestIds.length , 0 , "User request IDs should be cleared after claim " );
853+ }
627854}
0 commit comments