@@ -2986,6 +2986,163 @@ contract LendingBrokerTest is Test {
29862986 Position memory posAfter = moolah.position (marketParams.id (), borrower);
29872987 assertLt (posAfter.borrowShares, posBefore.borrowShares, "clean shares should liquidate " );
29882988 }
2989+
2990+ // ==========================================================================
2991+ // Relayer protocol fee: 10% cut on broker revenue (interest + penalty)
2992+ // ==========================================================================
2993+
2994+ uint256 constant FEE_WAD = 1e18 ;
2995+ address constant FEE_RECIPIENT = address (0xFEE );
2996+
2997+ /// @dev enable the protocol fee on the LISUSD relayer via the V2 reinitializer (rate hardcoded to 10%)
2998+ function _enableRelayerFee (address recipient ) internal {
2999+ vm.prank (ADMIN);
3000+ relayer.initializeV2 (recipient);
3001+ }
3002+
3003+ function test_relayer_initializeV2_setsFeeConfig () public {
3004+ _enableRelayerFee (FEE_RECIPIENT);
3005+ assertEq (relayer.feeRate (), relayer.DEFAULT_FEE_RATE (), "feeRate not defaulted to 10% " );
3006+ assertEq (relayer.feeRate (), 0.1e18 , "feeRate not 10% " );
3007+ assertEq (relayer.feeRecipient (), FEE_RECIPIENT, "feeRecipient not set " );
3008+ }
3009+
3010+ function test_relayer_initializeV2_onlyAdmin () public {
3011+ vm.prank (MANAGER); // MANAGER lacks DEFAULT_ADMIN_ROLE
3012+ vm.expectRevert ();
3013+ relayer.initializeV2 (FEE_RECIPIENT);
3014+ }
3015+
3016+ function test_relayer_initializeV2_cannotRunTwice () public {
3017+ _enableRelayerFee (FEE_RECIPIENT);
3018+ vm.prank (ADMIN);
3019+ vm.expectRevert (); // reinitializer(2) already consumed
3020+ relayer.initializeV2 (FEE_RECIPIENT);
3021+ }
3022+
3023+ function test_relayer_initializeV2_rejectsZeroRecipient () public {
3024+ vm.prank (ADMIN);
3025+ vm.expectRevert (bytes ("relayer/zero-address-provided " ));
3026+ relayer.initializeV2 (address (0 ));
3027+ }
3028+
3029+ /// @dev the fee is floored: recipient receives exactly floor(amount * feeRate / WAD),
3030+ /// the remainder stays in the relayer/vault flow.
3031+ function test_relayer_supplyToVault_skimsFee () public {
3032+ _enableRelayerFee (FEE_RECIPIENT);
3033+
3034+ uint256 amount = 1000 ether ;
3035+ uint256 expectedFee = (amount * 0.1e18 ) / FEE_WAD; // 100 ether
3036+
3037+ uint256 before = LISUSD.balanceOf (FEE_RECIPIENT);
3038+ _triggerInterestFlush (amount);
3039+ uint256 received = LISUSD.balanceOf (FEE_RECIPIENT) - before;
3040+
3041+ assertEq (received, expectedFee, "fee recipient did not receive 10% " );
3042+ }
3043+
3044+ /// @dev with no fee configured (default state) the relayer behaves exactly as before.
3045+ function test_relayer_supplyToVault_noFeeWhenDisabled () public {
3046+ uint256 before = LISUSD.balanceOf (FEE_RECIPIENT);
3047+ _triggerInterestFlush (1000 ether);
3048+ assertEq (LISUSD.balanceOf (FEE_RECIPIENT), before, "no fee should be taken when disabled " );
3049+ assertEq (relayer.feeRate (), 0 , "feeRate should default to 0 " );
3050+ }
3051+
3052+ /// @dev end-to-end: repaying a fixed position early routes 10% of both the accrued
3053+ /// interest and the early-repay penalty to the fee recipient.
3054+ function test_relayer_fixedRepay_feeOnInterestAndPenalty () public {
3055+ _enableRelayerFee (FEE_RECIPIENT);
3056+
3057+ FixedTermAndRate memory term = FixedTermAndRate ({ termId: 30 , duration: 30 days, apr: 110 * 1e25 });
3058+ vm.prank (BOT);
3059+ broker.updateFixedTermAndRate (term, false );
3060+
3061+ uint256 fixedAmt = 500 ether ;
3062+ vm.prank (borrower);
3063+ broker.borrow (fixedAmt, 30 );
3064+
3065+ skip (10 days); // repay early -> penalty applies
3066+
3067+ FixedLoanPosition memory pos = broker.userFixedPositions (borrower)[0 ];
3068+ uint256 posId = pos.posId;
3069+
3070+ // full close: interest + principal + penalty
3071+ uint256 repayAmt = broker.getUserTotalDebt (borrower) + 10 ether ;
3072+ (uint256 interestRepaid , uint256 penalty , ) = broker.previewRepayFixedLoanPosition (borrower, repayAmt, posId);
3073+ assertGt (interestRepaid, 0 , "no interest accrued " );
3074+ assertGt (penalty, 0 , "no penalty for early repay " );
3075+
3076+ // interest and penalty are supplied to the relayer in two separate calls,
3077+ // so the fee is floored on each independently.
3078+ uint256 expectedFee = (interestRepaid * 0.1e18 ) / FEE_WAD + (penalty * 0.1e18 ) / FEE_WAD;
3079+
3080+ LISUSD.setBalance (borrower, repayAmt);
3081+ uint256 feeBefore = LISUSD.balanceOf (FEE_RECIPIENT);
3082+ vm.startPrank (borrower);
3083+ IERC20 (address (LISUSD)).approve (address (broker), type (uint256 ).max);
3084+ broker.repay (repayAmt, posId, borrower);
3085+ vm.stopPrank ();
3086+
3087+ assertEq (LISUSD.balanceOf (FEE_RECIPIENT) - feeBefore, expectedFee, "fee on interest+penalty mismatch " );
3088+ }
3089+
3090+ /// @dev end-to-end: repaying a dynamic position routes 10% of the accrued interest to the fee recipient.
3091+ function test_relayer_dynamicRepay_feeOnInterest () public {
3092+ _enableRelayerFee (FEE_RECIPIENT);
3093+
3094+ uint256 amount = 400 ether ;
3095+ vm.prank (borrower);
3096+ broker.borrow (amount);
3097+
3098+ skip (30 days);
3099+
3100+ uint256 accruedInterest = broker.getUserTotalDebt (borrower) - amount;
3101+ assertGt (accruedInterest, 0 , "no dynamic interest accrued " );
3102+ uint256 expectedFee = (accruedInterest * 0.1e18 ) / FEE_WAD;
3103+
3104+ LISUSD.setBalance (borrower, accruedInterest); // repay interest only
3105+ uint256 feeBefore = LISUSD.balanceOf (FEE_RECIPIENT);
3106+ vm.startPrank (borrower);
3107+ IERC20 (address (LISUSD)).approve (address (broker), type (uint256 ).max);
3108+ broker.repay (accruedInterest, borrower);
3109+ vm.stopPrank ();
3110+
3111+ assertApproxEqAbs (LISUSD.balanceOf (FEE_RECIPIENT) - feeBefore, expectedFee, 1 , "fee on dynamic interest mismatch " );
3112+ }
3113+
3114+ function test_relayer_setFeeRate_onlyManager () public {
3115+ _enableRelayerFee (FEE_RECIPIENT);
3116+
3117+ vm.prank (address (0xdead ));
3118+ vm.expectRevert ();
3119+ relayer.setFeeRate (0.2e18 );
3120+
3121+ vm.prank (MANAGER);
3122+ relayer.setFeeRate (0.2e18 );
3123+ assertEq (relayer.feeRate (), 0.2e18 );
3124+
3125+ uint256 tooHigh = relayer.MAX_FEE () + 1 ;
3126+ vm.prank (MANAGER);
3127+ vm.expectRevert (bytes ("relayer/max-fee-exceeded " ));
3128+ relayer.setFeeRate (tooHigh);
3129+ }
3130+
3131+ function test_relayer_setFeeRecipient_onlyManager () public {
3132+ _enableRelayerFee (FEE_RECIPIENT);
3133+
3134+ vm.prank (address (0xdead ));
3135+ vm.expectRevert ();
3136+ relayer.setFeeRecipient (address (0xabcd ));
3137+
3138+ vm.prank (MANAGER);
3139+ relayer.setFeeRecipient (address (0xabcd ));
3140+ assertEq (relayer.feeRecipient (), address (0xabcd ));
3141+
3142+ vm.prank (MANAGER);
3143+ vm.expectRevert (bytes ("relayer/zero-address-provided " ));
3144+ relayer.setFeeRecipient (address (0 ));
3145+ }
29893146}
29903147
29913148/// @dev Mock swap pair that converts tokenIn -> tokenOut at oracle price.
0 commit comments