Skip to content

Commit 8ccc0e4

Browse files
Le-Caigneczguesmi
andauthored
feat: add unit tests for internal _debit function (#66)
Co-authored-by: Zied Guesmi <[email protected]>
1 parent 83d1069 commit 8ccc0e4

File tree

1 file changed

+200
-0
lines changed

1 file changed

+200
-0
lines changed

test/units/bridges/layerZero/IexecLayerZeroBridge.t.sol

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {IAccessControlDefaultAdminRules} from
1212
import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";
1313
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
1414
import {TestHelperOz5} from "@layerzerolabs/test-devtools-evm-foundry/contracts/TestHelperOz5.sol";
15+
import {stdError} from "forge-std/StdError.sol";
1516
import {IexecLayerZeroBridgeHarness} from "../../mocks/IexecLayerZeroBridgeHarness.sol";
1617
import {IIexecLayerZeroBridge} from "../../../../src/interfaces/IIexecLayerZeroBridge.sol";
1718
import {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

Comments
 (0)