@@ -12,6 +12,7 @@ import {IAccessControlDefaultAdminRules} from
1212import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol " ;
1313import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol " ;
1414import {TestHelperOz5} from "@layerzerolabs/test-devtools-evm-foundry/contracts/TestHelperOz5.sol " ;
15+ import {stdError} from "forge-std/StdError.sol " ;
1516import {IexecLayerZeroBridgeHarness} from "../../mocks/IexecLayerZeroBridgeHarness.sol " ;
1617import {IIexecLayerZeroBridge} from "../../../../src/interfaces/IIexecLayerZeroBridge.sol " ;
1718import {DualPausableUpgradeable} from "../../../../src/bridges/utils/DualPausableUpgradeable.sol " ;
@@ -458,4 +459,203 @@ contract IexecLayerZeroBridgeTest is TestHelperOz5 {
458459 assertEq (amountReceived, TRANSFER_AMOUNT, "Amount received should equal mint amount " );
459460 assertEq (rlcCrosschainToken.balanceOf (user2), initialBalance + TRANSFER_AMOUNT, "User balance should increase " );
460461 }
462+
463+ // ============ _debit ============
464+ function test_debit_WithApproval_SuccessfulTransfer () public {
465+ vm.prank (user1);
466+ rlcToken.approve (address (iexecLayerZeroBridgeEthereum), TRANSFER_AMOUNT);
467+
468+ _test_debit (iexecLayerZeroBridgeEthereum, address (rlcToken), true );
469+ }
470+
471+ function test_debit_WithoutApproval_SuccessfulBurn () public {
472+ _test_debit (iexecLayerZeroBridgeChainX, address (rlcCrosschainToken), false );
473+ }
474+
475+ function _test_debit (IexecLayerZeroBridgeHarness iexecLayerZeroBridge , address tokenAddress , bool approvalRequired )
476+ internal
477+ {
478+ RLCMock token = RLCMock (tokenAddress);
479+ uint256 initialUserBalance = token.balanceOf (user1);
480+
481+ vm.expectEmit (true , true , true , true , address (tokenAddress));
482+ emit IERC20 .Transfer (user1, approvalRequired ? address (rlcLiquidityUnifier) : address (0 ), TRANSFER_AMOUNT);
483+ if (! approvalRequired) {
484+ vm.expectEmit (true , true , true , true , address (tokenAddress));
485+ emit IERC7802 .CrosschainBurn (user1, TRANSFER_AMOUNT, address (iexecLayerZeroBridge));
486+ }
487+
488+ (uint256 amountSentLD , uint256 amountReceivedLD ) =
489+ iexecLayerZeroBridge.exposed_debit (user1, TRANSFER_AMOUNT, TRANSFER_AMOUNT, DEST_EID);
490+
491+ if (approvalRequired) {
492+ assertEq (
493+ token.balanceOf (address (rlcLiquidityUnifier)),
494+ TRANSFER_AMOUNT,
495+ "Unifier balance should increase by the transferred amount "
496+ );
497+ } else {
498+ assertEq (token.totalSupply (), INITIAL_BALANCE - TRANSFER_AMOUNT, "Total supply should decrease " );
499+ }
500+ assertEq (token.balanceOf (user1), initialUserBalance - TRANSFER_AMOUNT, "User balance should decrease " );
501+ assertEq (amountSentLD, TRANSFER_AMOUNT, "Amount sent should equal transfer amount " );
502+ assertEq (amountReceivedLD, TRANSFER_AMOUNT, "Amount received should equal transfer amount " );
503+ }
504+
505+ function test_debit_WithApproval_InsufficientApproval () public {
506+ // Setup: User approves less than required
507+ vm.prank (user1);
508+ rlcToken.approve (address (iexecLayerZeroBridgeEthereum), TRANSFER_AMOUNT - 1 );
509+
510+ // Should revert with arithmetic underflow or overflow
511+ vm.expectRevert (stdError.arithmeticError);
512+ iexecLayerZeroBridgeEthereum.exposed_debit (user1, TRANSFER_AMOUNT, TRANSFER_AMOUNT, DEST_EID);
513+ }
514+
515+ function test_debit_WithApproval_InsufficientBalance () public {
516+ _test_debit_InsufficientBalance (iexecLayerZeroBridgeEthereum, address (rlcToken), true );
517+ }
518+
519+ function test_debit_WithoutApproval_InsufficientBalance () public {
520+ _test_debit_InsufficientBalance (iexecLayerZeroBridgeChainX, address (rlcCrosschainToken), false );
521+ }
522+
523+ function _test_debit_InsufficientBalance (
524+ IexecLayerZeroBridgeHarness bridge ,
525+ address tokenAddress ,
526+ bool approvalRequired
527+ ) internal {
528+ uint256 excessiveAmount = INITIAL_BALANCE * 2 ;
529+ if (approvalRequired) {
530+ vm.prank (user1);
531+ IERC20 (tokenAddress).approve (address (bridge), excessiveAmount);
532+ // Should revert with arithmetic underflow or overflow from transferFrom
533+ vm.expectRevert (stdError.arithmeticError);
534+ } else {
535+ // Should revert with ERC20InsufficientBalance from crosschainBurn
536+ vm.expectRevert (
537+ abi.encodeWithSignature (
538+ "ERC20InsufficientBalance(address,uint256,uint256) " , user1, INITIAL_BALANCE, excessiveAmount
539+ )
540+ );
541+ }
542+ bridge.exposed_debit (user1, excessiveAmount, excessiveAmount, DEST_EID);
543+ }
544+
545+ function test_debit_WithApproval_SlippageExceeded () public {
546+ _test_debit_SlippageExceeded (iexecLayerZeroBridgeEthereum, address (rlcToken), true );
547+ }
548+
549+ function test_debit_WithoutApproval_SlippageExceeded () public {
550+ _test_debit_SlippageExceeded (iexecLayerZeroBridgeChainX, address (rlcCrosschainToken), false );
551+ }
552+
553+ function _test_debit_SlippageExceeded (
554+ IexecLayerZeroBridgeHarness bridge ,
555+ address tokenAddress ,
556+ bool approvalRequired
557+ ) internal {
558+ uint256 actualExpectedAmount = _removeDust (bridge, TRANSFER_AMOUNT);
559+ uint256 excessiveMinAmount = actualExpectedAmount + 1 ; // Unacceptable slippage because actualAmount < minAmount.
560+
561+ if (approvalRequired) {
562+ vm.prank (user1);
563+ IERC20 (tokenAddress).approve (address (bridge), TRANSFER_AMOUNT);
564+ }
565+
566+ // Should revert with SlippageExceeded
567+ vm.expectRevert (
568+ abi.encodeWithSignature ("SlippageExceeded(uint256,uint256) " , actualExpectedAmount, excessiveMinAmount)
569+ );
570+ bridge.exposed_debit (user1, TRANSFER_AMOUNT, excessiveMinAmount, DEST_EID);
571+ }
572+
573+ function testFuzz_debit_WithApproval_Amount (uint256 amount ) public {
574+ uint256 totalSupply = 87_000_000 * 10 ** 9 ; // 87 million tokens with 9 decimals
575+ vm.assume (amount <= totalSupply);
576+
577+ // Set up a sufficient balance for user1 (an INITIAL_BALANCE has already been sent)
578+ if (amount > INITIAL_BALANCE) {
579+ rlcToken.transfer (user1, amount - INITIAL_BALANCE);
580+ }
581+ vm.prank (user1);
582+ rlcToken.approve (address (iexecLayerZeroBridgeEthereum), amount);
583+
584+ _testFuzz_debit_Amount (iexecLayerZeroBridgeEthereum, rlcToken, amount);
585+ }
586+
587+ function testFuzz_debit_WithoutApproval_Amount (uint256 amount ) public {
588+ uint256 totalSupply = 87_000_000 * 10 ** 9 ; // 87 million tokens with 9 decimals
589+ vm.assume (amount <= totalSupply);
590+ // Set up a sufficient balance for user1 (an INITIAL_BALANCE has already been minted)
591+ if (amount > INITIAL_BALANCE) {
592+ vm.prank (address (iexecLayerZeroBridgeChainX));
593+ rlcCrosschainToken.crosschainMint (user1, amount - INITIAL_BALANCE);
594+ }
595+ _testFuzz_debit_Amount (iexecLayerZeroBridgeChainX, rlcCrosschainToken, amount);
596+ }
597+
598+ function _testFuzz_debit_Amount (IexecLayerZeroBridgeHarness bridge , IERC20 token , uint256 amount ) internal {
599+ // Fuzz test with different amounts for testing edge case (0 & max RLC supply)
600+ uint256 initialBalance = token.balanceOf (user1);
601+ uint256 expectedMinAmount = _removeDust (bridge, amount);
602+
603+ (uint256 amountSentLD , uint256 amountReceivedLD ) =
604+ bridge.exposed_debit (user1, amount, expectedMinAmount, DEST_EID);
605+
606+ assertEq (amountSentLD, expectedMinAmount, "Amount sent should equal dust-removed input " );
607+ assertEq (amountReceivedLD, expectedMinAmount, "Amount received should equal dust-removed input " );
608+ assertEq (
609+ token.balanceOf (user1), initialBalance - expectedMinAmount, "User balance should decrease by sent amount "
610+ );
611+ }
612+
613+ function test_debit_RevertsWhenFullyPaused () public {
614+ // Pause the contract
615+ vm.prank (pauser);
616+ iexecLayerZeroBridgeChainX.pause ();
617+
618+ // Should revert when fully paused
619+ vm.expectRevert (PausableUpgradeable.EnforcedPause.selector );
620+ iexecLayerZeroBridgeChainX.exposed_debit (user1, TRANSFER_AMOUNT, TRANSFER_AMOUNT, DEST_EID);
621+ }
622+
623+ function test_debit_RevertsWhenOutboundTransfersPaused () public {
624+ // Pause only sends
625+ vm.prank (pauser);
626+ iexecLayerZeroBridgeChainX.pauseOutboundTransfers ();
627+
628+ // Should revert when send is paused
629+ vm.expectRevert (DualPausableUpgradeable.EnforcedOutboundTransfersPause.selector );
630+ iexecLayerZeroBridgeChainX.exposed_debit (user1, TRANSFER_AMOUNT, TRANSFER_AMOUNT, DEST_EID);
631+ }
632+
633+ function test_debit_WorksAfterUnpause () public {
634+ // Pause then unpause
635+ vm.startPrank (pauser);
636+ iexecLayerZeroBridgeChainX.pause ();
637+ iexecLayerZeroBridgeChainX.unpause ();
638+ vm.stopPrank ();
639+
640+ uint256 initialBalance = rlcCrosschainToken.balanceOf (user1);
641+
642+ // Should work after unpause
643+ (uint256 amountSentLD , uint256 amountReceivedLD ) =
644+ iexecLayerZeroBridgeChainX.exposed_debit (user1, TRANSFER_AMOUNT, TRANSFER_AMOUNT, DEST_EID);
645+
646+ assertEq (amountSentLD, TRANSFER_AMOUNT, "Amount sent should equal transfer amount " );
647+ assertEq (amountReceivedLD, TRANSFER_AMOUNT, "Amount received should equal transfer amount " );
648+ assertEq (rlcCrosschainToken.balanceOf (user1), initialBalance - TRANSFER_AMOUNT, "User balance should decrease " );
649+ }
650+
651+ // ============ UTILITY FUNCTIONS ============
652+
653+ /// @dev Removes dust from amount based on the bridge's decimal conversion rate
654+ /// @param bridge The bridge contract to get the conversion rate from
655+ /// @param amount The amount to remove dust from
656+ /// @return The amount with dust removed
657+ function _removeDust (IexecLayerZeroBridgeHarness bridge , uint256 amount ) internal view returns (uint256 ) {
658+ uint256 conversionRate = bridge.decimalConversionRate ();
659+ return (amount / conversionRate) * conversionRate;
660+ }
461661}
0 commit comments