diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 92bf14e3..88cf85ff 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,7 +31,7 @@ jobs: run: forge fmt --check - name: Run Forge build - run: forge build --sizes && cp .env.template .env + run: forge build && forge build './src' --sizes && cp .env.template .env - name: Run Foundry coverage run: make generate-coverage diff --git a/foundry.toml b/foundry.toml index c59f826f..f9c7bded 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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 [doc] out = "docs/soldoc" diff --git a/test/units/RLCLiquidityUnifierUpgrade.t.sol b/test/units/RLCLiquidityUnifierUpgrade.t.sol index ffa3412d..cdb858c4 100644 --- a/test/units/RLCLiquidityUnifierUpgrade.t.sol +++ b/test/units/RLCLiquidityUnifierUpgrade.t.sol @@ -22,8 +22,6 @@ 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 { @@ -31,8 +29,20 @@ contract RLCLiquidityUnifierUpgradeTest is TestHelperOz5 { 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 diff --git a/test/units/bridges/layerZero/IexecLayerZeroBridge.t.sol b/test/units/bridges/layerZero/IexecLayerZeroBridge.t.sol index c6dc1342..51dd7c59 100644 --- a/test/units/bridges/layerZero/IexecLayerZeroBridge.t.sol +++ b/test/units/bridges/layerZero/IexecLayerZeroBridge.t.sol @@ -3,14 +3,12 @@ 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 "../../mocks/IexecLayerZeroBridgeHarness.sol"; import {DualPausableUpgradeable} from "../../../../src/bridges/utils/DualPausableUpgradeable.sol"; import {TestUtils} from "../../utils/TestUtils.sol"; import {RLCCrosschainToken} from "../../../../src/RLCCrosschainToken.sol"; @@ -18,12 +16,11 @@ 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; @@ -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(); @@ -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.iexecLayerZeroBridgeWithApproval); + address iexecLayerZeroBridgeChainXAddress = address(deploymentResult.iexecLayerZeroBridgeWithoutApproval); + + 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 @@ -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); @@ -106,12 +115,11 @@ contract IexecLayerZeroBridgeTest is TestHelperOz5 { } function _test_SendToken_WhenOperational( - IexecLayerZeroBridge iexecLayerZeroBridge, + IexecLayerZeroBridgeHarness iexecLayerZeroBridge, address tokenAddress, bool approvalRequired ) internal { - // This interface can be use for both token as we only use balanceOf func - RLCMock token = RLCMock(tokenAddress); + IERC20 token = IERC20(tokenAddress); // Check initial balances uint256 initialBalance = token.balanceOf(user1); @@ -280,4 +288,110 @@ 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, address(rlcCrosschainToken)); + emit IERC20.Transfer(address(0), user2, TRANSFER_AMOUNT); + vm.expectEmit(true, true, true, true, address(rlcCrosschainToken)); + 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 test_credit_SendToDeadAddressInsteadOfZeroAddress() public { + uint256 initialBalance = rlcCrosschainToken.balanceOf(address(0xdead)); + uint256 amountReceived = iexecLayerZeroBridgeChainX.exposed_credit(address(0), TRANSFER_AMOUNT, SOURCE_EID); + + assertEq(amountReceived, TRANSFER_AMOUNT, "Amount received should equal mint amount"); + assertEq( + rlcCrosschainToken.balanceOf(address(0xdead)), + initialBalance + TRANSFER_AMOUNT, + "Actual recipient balance should increase" + ); + + 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) + uint256 totalSupply = 87_000_000 * 10 ** 9; // 87 million tokens with 9 decimals + vm.assume(amount <= totalSupply); + + 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_RevertsWhenPaused() 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_WorksWhenOutboundTransfersPaused() 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.pauseOutboundTransfers(); + + 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"); + } } diff --git a/test/units/bridges/layerZero/IexecLayerZeroBridgeUpgrade.t.sol b/test/units/bridges/layerZero/IexecLayerZeroBridgeUpgrade.t.sol index ceec7cac..1b83f317 100644 --- a/test/units/bridges/layerZero/IexecLayerZeroBridgeUpgrade.t.sol +++ b/test/units/bridges/layerZero/IexecLayerZeroBridgeUpgrade.t.sol @@ -22,8 +22,6 @@ contract IexecLayerZeroBridgeUpgradeTest is TestHelperOz5 { address public pauser = makeAddr("pauser"); address public proxyAddress; - string public name = "iEx.ec Network Token"; - string public symbol = "RLC"; uint256 public constant NEW_STATE_VARIABLE = 2; function setUp() public virtual override { @@ -31,8 +29,20 @@ contract IexecLayerZeroBridgeUpgradeTest is TestHelperOz5 { 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.iexecLayerZeroBridgeWithoutApproval; + rlcCrosschainToken = deploymentResult1.rlcCrosschainToken; + proxyAddress = address(iexecLayerZeroBridgeV1); } diff --git a/test/units/mocks/IexecLayerZeroBridgeHarness.sol b/test/units/mocks/IexecLayerZeroBridgeHarness.sol new file mode 100644 index 00000000..4e6f4da0 --- /dev/null +++ b/test/units/mocks/IexecLayerZeroBridgeHarness.sol @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 IEXEC BLOCKCHAIN TECH +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.22; + +import {IexecLayerZeroBridge} from "../../../src/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); + } +} diff --git a/test/units/utils/TestUtils.sol b/test/units/utils/TestUtils.sol index 88671f8b..b08dcd3d 100644 --- a/test/units/utils/TestUtils.sol +++ b/test/units/utils/TestUtils.sol @@ -10,82 +10,107 @@ import {UUPSProxyDeployer} from "../../../script/lib/UUPSProxyDeployer.sol"; import {RLCMock} from "../mocks/RLCMock.sol"; import {IexecLayerZeroBridge} from "../../../src/bridges/layerZero/IexecLayerZeroBridge.sol"; import {RLCLiquidityUnifier} from "../../../src/RLCLiquidityUnifier.sol"; +import {Deploy as RLCLiquidityUnifierDeployScript} from "../../../script/RLCLiquidityUnifier.s.sol"; import {RLCCrosschainToken} from "../../../src/RLCCrosschainToken.sol"; import {Deploy as RLCCrosschainTokenDeployScript} from "../../../script/RLCCrosschainToken.s.sol"; library TestUtils { using OptionsBuilder for bytes; - // TODO declare name and symbol inside the function. - function setupDeployment( - string memory name, - string memory symbol, - address lzEndpointSource, - address lzEndpointDestination, - address initialAdmin, - address initialUpgrader, - address initialPauser - ) - internal - returns ( - IexecLayerZeroBridge iexecLayerZeroBridgeChainA, - IexecLayerZeroBridge iexecLayerZeroBridgeChainB, - RLCMock rlcToken, - RLCCrosschainToken rlcCrosschainToken, - RLCLiquidityUnifier rlcLiquidityUnifier - ) - { - address createXFactory = address(new CreateX()); + // Struct to hold deployment parameters and reduce stack depth + struct DeploymentParams { + string iexecLayerZeroBridgeContractName; + address lzEndpointSource; + address lzEndpointDestination; + address initialAdmin; + address initialUpgrader; + address initialPauser; + } - // Deploy RLC token mock for L1 - rlcToken = new RLCMock(); + // Struct to hold deployment results + struct DeploymentResult { + IexecLayerZeroBridge iexecLayerZeroBridgeWithApproval; + IexecLayerZeroBridge iexecLayerZeroBridgeWithoutApproval; + RLCMock rlcToken; + RLCCrosschainToken rlcCrosschainToken; + RLCLiquidityUnifier rlcLiquidityUnifier; + } - // salt for createX + function setupDeployment(DeploymentParams memory params) public returns (DeploymentResult memory result) { + string memory name = "iEx.ec Network Token"; + string memory symbol = "RLC"; + address createXFactory = address(new CreateX()); bytes32 salt = keccak256("salt"); + // Deploy RLC token mock for L1 + result.rlcToken = new RLCMock(); + // Deploy Liquidity Unifier - rlcLiquidityUnifier = RLCLiquidityUnifier( - UUPSProxyDeployer.deployUsingCreateX( - "RLCLiquidityUnifier", - abi.encode(rlcToken), - abi.encodeWithSelector(RLCLiquidityUnifier.initialize.selector, initialAdmin, initialUpgrader), - createXFactory, - salt + result.rlcLiquidityUnifier = _deployLiquidityUnifier(params, result.rlcToken, createXFactory, salt); + + // Deploy IexecLayerZeroBridge for Sepolia + result.iexecLayerZeroBridgeWithApproval = + _deployBridge(params, true, address(result.rlcLiquidityUnifier), createXFactory, salt); + + // Deploy RLC Crosschain token and Bridge for ChainX + result.rlcCrosschainToken = _deployCrosschainToken(params, name, symbol, createXFactory, salt); + + result.iexecLayerZeroBridgeWithoutApproval = + _deployBridge(params, false, address(result.rlcCrosschainToken), createXFactory, salt); + } + + function _deployLiquidityUnifier( + DeploymentParams memory params, + RLCMock rlcToken, + address createXFactory, + bytes32 salt + ) private returns (RLCLiquidityUnifier) { + return RLCLiquidityUnifier( + new RLCLiquidityUnifierDeployScript().deploy( + address(rlcToken), params.initialAdmin, params.initialUpgrader, createXFactory, salt ) ); + } - // Deploy IexecLayerZeroBridgeAdapter - iexecLayerZeroBridgeChainA = IexecLayerZeroBridge( + function _deployBridge( + DeploymentParams memory params, + bool approvalRequired, + address bridgeableToken, + address createXFactory, + bytes32 salt + ) private returns (IexecLayerZeroBridge) { + return IexecLayerZeroBridge( UUPSProxyDeployer.deployUsingCreateX( - "IexecLayerZeroBridge", - abi.encode(true, rlcLiquidityUnifier, lzEndpointSource), + params.iexecLayerZeroBridgeContractName, + abi.encode( + approvalRequired, + bridgeableToken, + approvalRequired ? params.lzEndpointSource : params.lzEndpointDestination + ), abi.encodeWithSelector( - IexecLayerZeroBridge.initialize.selector, initialAdmin, initialUpgrader, initialPauser + IexecLayerZeroBridge.initialize.selector, + params.initialAdmin, + params.initialUpgrader, + params.initialPauser ), createXFactory, salt ) ); + } - // Deploy RLC Crosschain token (for L2) - rlcCrosschainToken = RLCCrosschainToken( + function _deployCrosschainToken( + DeploymentParams memory params, + string memory name, + string memory symbol, + address createXFactory, + bytes32 salt + ) private returns (RLCCrosschainToken) { + return RLCCrosschainToken( new RLCCrosschainTokenDeployScript().deploy( - name, symbol, initialAdmin, initialUpgrader, createXFactory, salt - ) - ); - // Deploy IexecLayerZeroBridge - iexecLayerZeroBridgeChainB = IexecLayerZeroBridge( - UUPSProxyDeployer.deployUsingCreateX( - "IexecLayerZeroBridge", - abi.encode(false, rlcCrosschainToken, lzEndpointDestination), - abi.encodeWithSelector( - IexecLayerZeroBridge.initialize.selector, initialAdmin, initialUpgrader, initialPauser - ), - createXFactory, - salt + name, symbol, params.initialAdmin, params.initialUpgrader, createXFactory, salt ) ); - // TODO: see if it's possible to authorize the bridge here. } /**