Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9120dc5
feat: Implement IexecLayerZeroBridgeHarness and refactor deployment s…
Le-Caignec Jul 3, 2025
bf05f1b
feat: Add IexecLayerZeroBridgeHarness test suite for enhanced cross-c…
Le-Caignec Jul 4, 2025
9dc52a1
feat: Refactor deployment results and update test contracts for Layer…
Le-Caignec Jul 4, 2025
c79968a
feat: Update IexecLayerZeroBridge tests to use harness and remove obs…
Le-Caignec Jul 4, 2025
90c48cf
Merge branch 'main' into feature/add-unit-tests-for-_mint-function
Le-Caignec Jul 4, 2025
6ff1736
Merge branch 'main' into feature/add-unit-tests-for-_mint-function
Le-Caignec Jul 4, 2025
21f67b1
feat: Remove unused import from IexecLayerZeroBridge test file
Le-Caignec Jul 4, 2025
62f3ee5
feat: Add event emission expectations for CrosschainMint in IexecLaye…
Le-Caignec Jul 4, 2025
515c01b
Merge remote-tracking branch 'origin/main' into feature/add-unit-test…
Le-Caignec Jul 7, 2025
960acd0
refactor: Rename test functions for clarity on paused states
Le-Caignec Jul 7, 2025
6ee5934
refactor: Update token type from RLCMock to IERC20 for improved inter…
Le-Caignec Jul 7, 2025
218e43f
Merge branch 'main' into feature/add-unit-tests-for-_mint-function
Le-Caignec Jul 7, 2025
713fcfc
Merge remote-tracking branch 'origin/main' into feature/add-unit-test…
Le-Caignec Jul 8, 2025
21b37f0
fix: Correct parameter order in UUPSProxyDeployer deployment
Le-Caignec Jul 8, 2025
090d5a5
fix: Use dynamic contract name for IexecLayerZeroBridge deployment
Le-Caignec Jul 8, 2025
efd97f5
refactor: Simplify liquidity unifier and crosschain token deployment …
Le-Caignec Jul 8, 2025
7878f71
fix: Update Forge build command to include size analysis for source f…
Le-Caignec Jul 8, 2025
fd25e94
test ci
Le-Caignec Jul 8, 2025
cbffc8b
fix: Add environment file copy step after Forge build
Le-Caignec Jul 8, 2025
67f778a
Merge branch 'main' into feature/add-unit-tests-for-_mint-function
Le-Caignec Jul 8, 2025
d9cbc1d
Update test/units/bridges/layerZero/IexecLayerZeroBridge.t.sol
Le-Caignec Jul 8, 2025
34dbb20
Update test/units/bridges/layerZero/IexecLayerZeroBridge.t.sol
Le-Caignec Jul 8, 2025
49f461c
refactor: rename fuzz test for credit function to clarify address han…
Le-Caignec Jul 8, 2025
795cf82
fix: correct expected emit address in credit function test
Le-Caignec Jul 8, 2025
f3db488
refactor: comment out existing tests for send functionality and credi…
Le-Caignec Jul 8, 2025
c5836a9
refactor: update variable names for IexecLayerZeroBridge to improve c…
Le-Caignec Jul 8, 2025
c57974a
fix: uncomment
Le-Caignec Jul 9, 2025
f076db0
test: uncomment tests
Le-Caignec Jul 9, 2025
cda19cd
Update test/units/bridges/layerZero/IexecLayerZeroBridge.t.sol
zguesmi Jul 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ extra_output = ["storageLayout"]

[profile.test]
optimizer = false
fuzz = true
runs = 1000

# TODO configure this profile and use it in CI.
# [profile.ci]
# fuzz = true
# fuzz.runs = 1000

# TODO use vm.rpcUrl("arbitrumSepolia") instead of env variable.
# https://getfoundry.sh/config/reference/testing#rpc_endpoints
Expand Down
23 changes: 23 additions & 0 deletions src/mocks/IexecLayerZeroBridgeHarness.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 IEXEC BLOCKCHAIN TECH <contact@iex.ec>
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.22;

import {IexecLayerZeroBridge} from "../bridges/layerZero/IexecLayerZeroBridge.sol";

contract IexecLayerZeroBridgeHarness is IexecLayerZeroBridge {
constructor(bool approvalRequired_, address bridgeableToken, address lzEndpoint)
IexecLayerZeroBridge(approvalRequired_, bridgeableToken, lzEndpoint)
{}

function exposed_debit(address from, uint256 amountLD, uint256 minAmountLD, uint32 dstEid)
external
returns (uint256 amountSentLD, uint256 amountReceivedLD)
{
return _debit(from, amountLD, minAmountLD, dstEid);
}

function exposed_credit(address to, uint256 amountLD, uint32 srcEid) external returns (uint256 amountReceivedLD) {
return _credit(to, amountLD, srcEid);
}
}
18 changes: 14 additions & 4 deletions test/units/RLCLiquidityUnifierUpgrade.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,27 @@ contract RLCLiquidityUnifierUpgradeTest is TestHelperOz5 {
address private upgrader = makeAddr("upgrader");

address public proxyAddress;
string private name = "iEx.ec Network Token";
string public symbol = "RLC";
uint256 public constant NEW_STATE_VARIABLE = 2;

function setUp() public virtual override {
super.setUp();
setUpEndpoints(2, LibraryType.UltraLightNode);
mockEndpoint = address(endpoints[1]);

(,, rlcToken,, rlcLiquidityUnifierV1) =
TestUtils.setupDeployment(name, symbol, mockEndpoint, mockEndpoint, admin, upgrader, pauser);
TestUtils.DeploymentResult memory deploymentResult = TestUtils.setupDeployment(
TestUtils.DeploymentParams({
iexecLayerZeroBridgeContractName: "IexecLayerZeroBridge",
lzEndpointSource: mockEndpoint,
lzEndpointDestination: mockEndpoint,
initialAdmin: admin,
initialUpgrader: upgrader,
initialPauser: pauser
})
);

rlcToken = deploymentResult.rlcToken;
rlcLiquidityUnifierV1 = deploymentResult.rlcLiquidityUnifier;

proxyAddress = address(rlcLiquidityUnifierV1);

//Add label to make logs more readable
Expand Down
147 changes: 133 additions & 14 deletions test/units/bridges/layerZero/IexecLayerZeroBridge.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,24 @@

pragma solidity ^0.8.22;

import {OptionsBuilder} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";
import {MessagingFee, SendParam, IOFT} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import {IERC7802} from "@openzeppelin/contracts/interfaces/draft-IERC7802.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {TestHelperOz5} from "@layerzerolabs/test-devtools-evm-foundry/contracts/TestHelperOz5.sol";
import {CreateX} from "@createx/contracts/CreateX.sol";
import {IexecLayerZeroBridge} from "../../../../src/bridges/layerZero/IexecLayerZeroBridge.sol";
import {IexecLayerZeroBridgeHarness} from "../../../../src/mocks/IexecLayerZeroBridgeHarness.sol";
import {DualPausableUpgradeable} from "../../../../src/bridges/utils/DualPausableUpgradeable.sol";
import {TestUtils} from "../../utils/TestUtils.sol";
import {RLCCrosschainToken} from "../../../../src/RLCCrosschainToken.sol";
import {RLCLiquidityUnifier} from "../../../../src/RLCLiquidityUnifier.sol";
import {RLCMock} from "../../mocks/RLCMock.sol";

contract IexecLayerZeroBridgeTest is TestHelperOz5 {
using OptionsBuilder for bytes;
using TestUtils for *;

// ============ STATE VARIABLES ============
IexecLayerZeroBridge private iexecLayerZeroBridgeEthereum; // A chain with approval required.
IexecLayerZeroBridge private iexecLayerZeroBridgeChainX;
IexecLayerZeroBridgeHarness private iexecLayerZeroBridgeEthereum; // A chain with approval required.
IexecLayerZeroBridgeHarness private iexecLayerZeroBridgeChainX;
RLCCrosschainToken private rlcCrosschainToken;
RLCLiquidityUnifier private rlcLiquidityUnifier;
RLCMock private rlcToken;
Expand All @@ -40,8 +37,6 @@ contract IexecLayerZeroBridgeTest is TestHelperOz5 {

uint256 private constant INITIAL_BALANCE = 100 * 10 ** 9; // 100 RLC tokens with 9 decimals
uint256 private constant TRANSFER_AMOUNT = 1 * 10 ** 9; // 1 RLC token with 9 decimals
string private name = "iEx.ec Network Token";
string private symbol = "RLC";

function setUp() public virtual override {
super.setUp();
Expand All @@ -51,11 +46,26 @@ contract IexecLayerZeroBridgeTest is TestHelperOz5 {
address lzEndpointSource = address(endpoints[SOURCE_EID]); // Source endpoint for Sepolia - Destination endpoint for Arbitrum Sepolia
address lzEndpointDestination = address(endpoints[DEST_EID]); // Source endpoint for Arbitrum Sepolia - Destination endpoint for Sepolia

(iexecLayerZeroBridgeEthereum, iexecLayerZeroBridgeChainX, rlcToken, rlcCrosschainToken, rlcLiquidityUnifier) =
TestUtils.setupDeployment(name, symbol, lzEndpointSource, lzEndpointDestination, admin, upgrader, pauser);
TestUtils.DeploymentResult memory deploymentResult = TestUtils.setupDeployment(
TestUtils.DeploymentParams({
iexecLayerZeroBridgeContractName: "IexecLayerZeroBridgeHarness",
lzEndpointSource: lzEndpointSource,
lzEndpointDestination: lzEndpointDestination,
initialAdmin: admin,
initialUpgrader: upgrader,
initialPauser: pauser
})
);

address iexecLayerZeroBridgeEthereumAddress = address(deploymentResult.iexecLayerZeroBridgeChainWithApproval);
address iexecLayerZeroBridgeChainXAddress = address(deploymentResult.iexecLayerZeroBridgeChainWithoutApproval);

iexecLayerZeroBridgeEthereum = IexecLayerZeroBridgeHarness(iexecLayerZeroBridgeEthereumAddress);
iexecLayerZeroBridgeChainX = IexecLayerZeroBridgeHarness(iexecLayerZeroBridgeChainXAddress);
rlcToken = deploymentResult.rlcToken;
rlcCrosschainToken = deploymentResult.rlcCrosschainToken;
rlcLiquidityUnifier = deploymentResult.rlcLiquidityUnifier;

address iexecLayerZeroBridgeEthereumAddress = address(iexecLayerZeroBridgeEthereum);
address iexecLayerZeroBridgeChainXAddress = address(iexecLayerZeroBridgeChainX);
// Wire the contracts
address[] memory contracts = new address[](2);
contracts[0] = iexecLayerZeroBridgeEthereumAddress; // Index 0 → EID 1
Expand All @@ -79,7 +89,6 @@ contract IexecLayerZeroBridgeTest is TestHelperOz5 {
vm.startPrank(admin);
rlcLiquidityUnifier.grantRole(rlcLiquidityUnifier.TOKEN_BRIDGE_ROLE(), iexecLayerZeroBridgeEthereumAddress);
vm.stopPrank();

// Transfer initial RLC balance to user1
rlcToken.transfer(user1, INITIAL_BALANCE);

Expand All @@ -106,7 +115,7 @@ contract IexecLayerZeroBridgeTest is TestHelperOz5 {
}

function _test_SendToken_WhenOperational(
IexecLayerZeroBridge iexecLayerZeroBridge,
IexecLayerZeroBridgeHarness iexecLayerZeroBridge,
address tokenAddress,
bool approvalRequired
) internal {
Expand Down Expand Up @@ -280,4 +289,114 @@ contract IexecLayerZeroBridgeTest is TestHelperOz5 {
"token() should return the correct token contract address"
);
}

// ============ _credit ============
function test_credit_SuccessfulMintToUser() public {
// Test successful minting to a regular user address
uint256 initialBalance = rlcCrosschainToken.balanceOf(user2);

// Expect the Transfer & CrosschainMint event
vm.expectEmit(true, true, true, true);
emit IERC20.Transfer(address(0), user2, TRANSFER_AMOUNT);
vm.expectEmit(true, true, true, true);
emit IERC7802.CrosschainMint(user2, TRANSFER_AMOUNT, address(iexecLayerZeroBridgeChainX));

uint256 amountReceived = iexecLayerZeroBridgeChainX.exposed_credit(user2, TRANSFER_AMOUNT, SOURCE_EID);

assertEq(amountReceived, TRANSFER_AMOUNT, "Amount received should equal mint amount");
assertEq(
rlcCrosschainToken.balanceOf(user2),
initialBalance + TRANSFER_AMOUNT,
"User balance should increase by mint amount"
);
}

function testFuzz_credit_Address(address to) public {
// Fuzz test with different addresses including zero address
// Handle zero address redirection
address actualRecipient = (to == address(0)) ? address(0xdead) : to;
uint256 initialBalance = rlcCrosschainToken.balanceOf(actualRecipient);
uint256 amountReceived = iexecLayerZeroBridgeChainX.exposed_credit(to, TRANSFER_AMOUNT, SOURCE_EID);

assertEq(amountReceived, TRANSFER_AMOUNT, "Amount received should equal mint amount");
assertEq(
rlcCrosschainToken.balanceOf(actualRecipient),
initialBalance + TRANSFER_AMOUNT,
"Actual recipient balance should increase"
);

// Additional check for zero address case
if (to == address(0)) {
assertEq(rlcCrosschainToken.balanceOf(address(0)), 0, "Zero address balance should remain zero");
}
}

function testFuzz_credit_Amount(uint256 amount) public {
// Fuzz test with different amounts for testing edge case (0 & max RLC supply)
vm.assume(amount <= INITIAL_BALANCE);
uint256 initialBalance = rlcCrosschainToken.balanceOf(user2);
uint256 amountReceived = iexecLayerZeroBridgeChainX.exposed_credit(user2, amount, SOURCE_EID);

assertEq(amountReceived, amount, "Amount received should equal mint amount");
assertEq(
rlcCrosschainToken.balanceOf(user2), initialBalance + amount, "User balance should increase by mint amount"
);
}

function test_credit_RevertsWhenFullyPaused() public {
// Test that _credit reverts when contract is fully paused
// Pause the contract
vm.prank(pauser);
iexecLayerZeroBridgeChainX.pause();

vm.expectRevert(PausableUpgradeable.EnforcedPause.selector);
iexecLayerZeroBridgeChainX.exposed_credit(user2, TRANSFER_AMOUNT, SOURCE_EID);
}

function test_credit_WorksWhenOnlySendPaused() public {
// Test that _credit still works when only sends are paused (Level 2 pause)
uint256 initialBalance = rlcCrosschainToken.balanceOf(user2);

// Pause only sends
vm.prank(pauser);
iexecLayerZeroBridgeChainX.pauseSend();

vm.expectEmit(true, true, true, true);
emit IERC20.Transfer(address(0), user2, TRANSFER_AMOUNT);
vm.expectEmit(true, true, true, true);
emit IERC7802.CrosschainMint(user2, TRANSFER_AMOUNT, address(iexecLayerZeroBridgeChainX));

uint256 amountReceived = iexecLayerZeroBridgeChainX.exposed_credit(user2, TRANSFER_AMOUNT, SOURCE_EID);

assertEq(amountReceived, TRANSFER_AMOUNT, "Amount received should equal mint amount");
assertEq(rlcCrosschainToken.balanceOf(user2), initialBalance + TRANSFER_AMOUNT, "User balance should increase");
}

function test_credit_WorksAfterUnpause() public {
// Test that _credit works after unpausing
uint256 initialBalance = rlcCrosschainToken.balanceOf(user2);

// Pause the contract
vm.prank(pauser);
iexecLayerZeroBridgeChainX.pause();

// Verify it's paused
vm.expectRevert(PausableUpgradeable.EnforcedPause.selector);
iexecLayerZeroBridgeChainX.exposed_credit(user2, TRANSFER_AMOUNT, SOURCE_EID);

// Unpause the contract
vm.prank(pauser);
iexecLayerZeroBridgeChainX.unpause();

// Now it should work
vm.expectEmit(true, true, true, true);
emit IERC20.Transfer(address(0), user2, TRANSFER_AMOUNT);
vm.expectEmit(true, true, true, true);
emit IERC7802.CrosschainMint(user2, TRANSFER_AMOUNT, address(iexecLayerZeroBridgeChainX));

uint256 amountReceived = iexecLayerZeroBridgeChainX.exposed_credit(user2, TRANSFER_AMOUNT, SOURCE_EID);

assertEq(amountReceived, TRANSFER_AMOUNT, "Amount received should equal mint amount");
assertEq(rlcCrosschainToken.balanceOf(user2), initialBalance + TRANSFER_AMOUNT, "User balance should increase");
}
}
18 changes: 14 additions & 4 deletions test/units/bridges/layerZero/IexecLayerZeroBridgeUpgrade.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,27 @@ contract IexecLayerZeroBridgeUpgradeTest is TestHelperOz5 {
address public pauser = makeAddr("pauser");

address public proxyAddress;
string public name = "iEx.ec Network Token";
string public symbol = "RLC";
Comment on lines -25 to -26
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding those two string memory variables into our utils library caused a Stack too Deep issue. So the library needs to be refactored to use a struct and to improve storage management.

uint256 public constant NEW_STATE_VARIABLE = 2;

function setUp() public virtual override {
super.setUp();
setUpEndpoints(2, LibraryType.UltraLightNode);
mockEndpoint = address(endpoints[1]);

(, iexecLayerZeroBridgeV1,, rlcCrosschainToken,) =
TestUtils.setupDeployment(name, symbol, mockEndpoint, mockEndpoint, admin, upgrader, pauser);
TestUtils.DeploymentResult memory deploymentResult1 = TestUtils.setupDeployment(
TestUtils.DeploymentParams({
iexecLayerZeroBridgeContractName: "IexecLayerZeroBridge",
lzEndpointSource: mockEndpoint,
lzEndpointDestination: mockEndpoint,
initialAdmin: admin,
initialUpgrader: upgrader,
initialPauser: pauser
})
);

iexecLayerZeroBridgeV1 = deploymentResult1.iexecLayerZeroBridgeChainWithoutApproval;
rlcCrosschainToken = deploymentResult1.rlcCrosschainToken;

proxyAddress = address(iexecLayerZeroBridgeV1);
}

Expand Down
Loading
Loading