diff --git a/.github/workflows/createPRsAsDraft.yml b/.github/workflows/createPRsAsDraft.yml new file mode 100644 index 000000000..e0b91f9fd --- /dev/null +++ b/.github/workflows/createPRsAsDraft.yml @@ -0,0 +1,17 @@ +name: Mark new PRs as Draft +# - Marks all newly opened pull requests as drafts + +on: + pull_request: + types: [opened] + +jobs: + mark-new-PRs-as-draft: + name: Mark as draft + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: Mark as draft + uses: voiceflow/draft-pr@v1.1 + with: + token: ${{ secrets.GIT_ACTIONS_BOT_PAT_CLASSIC }} diff --git a/.github/workflows/enforceTestCoverage.yml b/.github/workflows/enforceTestCoverage.yml new file mode 100644 index 000000000..5360b91a6 --- /dev/null +++ b/.github/workflows/enforceTestCoverage.yml @@ -0,0 +1,144 @@ +name: Enforce Min Test Coverage + +# - will make sure that (Foundry) unit test coverage is above min threshold +# - we start with 74% (status today), planning to increase to 100% until EOY 2024 +# - Only the 'lines' coverage counts as 'branch' coverage is not reliable + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + enforce-min-test-coverage: + runs-on: ubuntu-latest + # will only run once the PR is in "Ready for Review" state + if: ${{ github.event.pull_request.draft == false }} + + permissions: + pull-requests: write + contents: read + env: + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_GOERLI: ${{ secrets.ETH_NODE_URI_GOERLI }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_BSC: ${{ secrets.ETH_NODE_URI_BSC }} + ETH_NODE_URI_GNOSIS: ${{ secrets.ETH_NODE_URI_GNOSIS }} + MIN_TEST_COVERAGE: 74 # = 74% line coverage + steps: + - uses: actions/checkout@v4.1.7 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dev dependencies + run: yarn install + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1.2.0 + with: + version: nightly + + - name: Install Dependencies + run: forge install + + - name: Generate Coverage Report + run: | + forge coverage --report lcov --force + + echo "Filtering coverage report to only contain coverage info for 'src/'' folder now" + + npx ts-node utils/filter_lcov.ts lcov.info lcov-filtered.info 'test/' 'script/' + + echo "Coverage report successfully filtered" + + - name: Generate Coverage Summary + run: | + + + # Path to the lcov info file + LCOV_FILE="lcov-filtered.info" + + # Initialize counters + TOTAL_LINES_FOUND=0 + TOTAL_LINES_HIT=0 + TOTAL_FUNCTIONS_FOUND=0 + TOTAL_FUNCTIONS_HIT=0 + TOTAL_BRANCHES_FOUND=0 + TOTAL_BRANCHES_HIT=0 + + # Read through the lcov file + while IFS= read -r line; do + case $line in + LF:*) + TOTAL_LINES_FOUND=$((TOTAL_LINES_FOUND + ${line#LF:})) + ;; + LH:*) + TOTAL_LINES_HIT=$((TOTAL_LINES_HIT + ${line#LH:})) + ;; + FNF:*) + TOTAL_FUNCTIONS_FOUND=$((TOTAL_FUNCTIONS_FOUND + ${line#FNF:})) + ;; + FNH:*) + TOTAL_FUNCTIONS_HIT=$((TOTAL_FUNCTIONS_HIT + ${line#FNH:})) + ;; + BRF:*) + TOTAL_BRANCHES_FOUND=$((TOTAL_BRANCHES_FOUND + ${line#BRF:})) + ;; + BRH:*) + TOTAL_BRANCHES_HIT=$((TOTAL_BRANCHES_HIT + ${line#BRH:})) + ;; + esac + done < "$LCOV_FILE" + + # Calculate percentages with high precision + LINE_COVERAGE_PERCENTAGE=$(echo "scale=4; $TOTAL_LINES_HIT / $TOTAL_LINES_FOUND * 100" | bc) + FUNCTION_COVERAGE_PERCENTAGE=$(echo "scale=4; $TOTAL_FUNCTIONS_HIT / $TOTAL_FUNCTIONS_FOUND * 100" | bc) + BRANCH_COVERAGE_PERCENTAGE=$(echo "scale=4; $TOTAL_BRANCHES_HIT / $TOTAL_BRANCHES_FOUND * 100" | bc) + + # Format results with two decimal places and alignment + LINE_COVERAGE_PERCENTAGE=$(printf "%.2f" "$LINE_COVERAGE_PERCENTAGE") + FUNCTION_COVERAGE_PERCENTAGE=$(printf "%.2f" "$FUNCTION_COVERAGE_PERCENTAGE") + BRANCH_COVERAGE_PERCENTAGE=$(printf "%.2f" "$BRANCH_COVERAGE_PERCENTAGE") + + # Prepare aligned output + LINE_COVERAGE_REPORT=$(printf "Line Coverage: %6s%% (%4d / %4d lines)" "$LINE_COVERAGE_PERCENTAGE" "$TOTAL_LINES_HIT" "$TOTAL_LINES_FOUND") + FUNCTION_COVERAGE_REPORT=$(printf "Function Coverage: %6s%% (%4d / %4d functions)" "$FUNCTION_COVERAGE_PERCENTAGE" "$TOTAL_FUNCTIONS_HIT" "$TOTAL_FUNCTIONS_FOUND") + BRANCH_COVERAGE_REPORT=$(printf "Branch Coverage: %6s%% (%4d / %4d branches)" "$BRANCH_COVERAGE_PERCENTAGE" "$TOTAL_BRANCHES_HIT" "$TOTAL_BRANCHES_FOUND") + + # Check against minimum threshold + if (( $(echo "$LINE_COVERAGE_PERCENTAGE >= $MIN_TEST_COVERAGE" | bc -l) )); then + RESULT_COVERAGE_REPORT="Test coverage ($LINE_COVERAGE_PERCENTAGE%) is above min threshold ($MIN_TEST_COVERAGE%). Check passed." + echo -e "\033[32m$RESULT_COVERAGE_REPORT\033[0m" + else + RESULT_COVERAGE_REPORT="Test coverage ($LINE_COVERAGE_PERCENTAGE%) is below min threshold ($MIN_TEST_COVERAGE%). Check failed." + echo -e "\033[31m$RESULT_COVERAGE_REPORT\033[0m" + exit 1 + fi + + # Output result_COVERAGE_REPORTs + echo "$LINE_COVERAGE_REPORT" + echo "$FUNCTION_COVERAGE_REPORT" + echo "$BRANCH_COVERAGE_REPORT" + echo "$RESULT_COVERAGE_REPORT" + + # Store in GitHub environment variables + { + echo "LINE_COVERAGE_REPORT=$LINE_COVERAGE_REPORT" + echo "FUNCTION_COVERAGE_REPORT=$FUNCTION_COVERAGE_REPORT" + echo "BRANCH_COVERAGE_REPORT=$BRANCH_COVERAGE_REPORT" + echo "RESULT_COVERAGE_REPORT=$RESULT_COVERAGE_REPORT" + } >> "$GITHUB_ENV" + + - name: Comment with Coverage Summary in PR + uses: mshick/add-pr-comment@v2.8.2 + with: + repo-token: ${{ secrets.GIT_ACTIONS_BOT_PAT_CLASSIC }} + message: | + ## Test Coverage Report + ${{ env.LINE_COVERAGE_REPORT }} + ${{ env.FUNCTION_COVERAGE_REPORT }} + ${{ env.BRANCH_COVERAGE_REPORT }} + ${{ env.RESULT_COVERAGE_REPORT }} diff --git a/.github/workflows/forge.yml b/.github/workflows/forge.yml index b4734dca8..56bb97a8c 100644 --- a/.github/workflows/forge.yml +++ b/.github/workflows/forge.yml @@ -1,12 +1,21 @@ -name: Forge +# - Run (Foundry) Unit Test Suite +# - will make sure that all tests pass + +name: Run Unit Tests on: + pull_request: + types: [opened, synchronize, reopened] push: + branches: + - main # makes sure that it runs on main branch after a PR has been merged - # Allows you to run this workflow manually from the Actions tab + # Allows to run this workflow manually from the Actions tab workflow_dispatch: jobs: - test: + run-unit-tests: + # will only run once the PR is in "Ready for Review" state + if: ${{ github.event.pull_request.draft == false }} runs-on: ubuntu-latest env: ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} @@ -15,31 +24,30 @@ jobs: ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} ETH_NODE_URI_BSC: ${{ secrets.ETH_NODE_URI_BSC }} ETH_NODE_URI_GNOSIS: ${{ secrets.ETH_NODE_URI_GNOSIS }} - FORK_NUMBER: ${{ secrets.FORK_NUMBER }} - POLYGON_FORK_NUMBER: ${{ secrets.POLYGON_FORK_NUMBER }} - FORK_NUMBER_POLYGON: 36004499 + steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.1.7 with: submodules: recursive + - uses: actions/setup-node@v4.0.0 with: node-version: 20 + - name: Install dev dependencies run: yarn install - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1.0.10 + uses: foundry-rs/foundry-toolchain@v1.2.0 with: version: nightly - - name: Install Deps + - name: Install Dependencies run: forge install - - name: Run forge tests - uses: Wandalen/wretry.action@v1.3.0 + + - name: Run forge tests (with auto-repeat in case of error) + uses: Wandalen/wretry.action@v3.5.0 with: command: forge test attempt_limit: 10 - attempt_delay: 5000 - - name: Get forge test coverage - run: forge coverage + attempt_delay: 15000 diff --git a/.github/workflows/protectAuditorsGroup.yml b/.github/workflows/protectAuditorsGroup.yml index 71afd805f..7fcbd35c1 100644 --- a/.github/workflows/protectAuditorsGroup.yml +++ b/.github/workflows/protectAuditorsGroup.yml @@ -1,25 +1,26 @@ +# Protect Auditors Group +# - makes sure that members of the auditor group cannot be members of a any smart-contract group +# - this ensures that no member can have multiple roles and use this to bypass audit requirements + name: Protect Auditors Group on: push: jobs: - check_membership: + protect-auditors-group: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Compare Group Members env: - GH_PAT: ${{ secrets.GIT_TOKEN }} + GH_PAT: ${{ secrets.GIT_ACTIONS_BOT_PAT_CLASSIC }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | ##### unset the default git token (does not have sufficient rights to get team members) unset GITHUB_TOKEN ##### use the Personal Access Token to log into git CLI - echo $GH_PAT | gh auth login --with-token + echo $GH_PAT | gh auth login --with-token || { echo "GitHub authentication failed"; exit 1; } # Function to get team members getTeamMembers() { @@ -38,30 +39,44 @@ jobs: ##### Get members of each group echo "Fetching members of $SC_ADMINS..." - groupAMembers=$(getTeamMembers $ORG_NAME $SC_ADMINS) + SC_ADMINS_MEMBERS=$(getTeamMembers "$ORG_NAME" "$SC_ADMINS") || { echo "Failed to fetch members of $SC_ADMINS"; exit 1; } echo "Fetching members of $SC_CORE..." - groupBMembers=$(getTeamMembers $ORG_NAME $SC_CORE) + SC_CORE_MEMBERS=$(getTeamMembers "$ORG_NAME" "$SC_CORE") || { echo "Failed to fetch members of $SC_CORE"; exit 1; } echo "Fetching members of $AUDITORS..." - groupCMembers=$(getTeamMembers $ORG_NAME $AUDITORS) + AUDITORS_MEMBERS=$(getTeamMembers "$ORG_NAME" "$AUDITORS") || { echo "Failed to fetch members of $AUDITORS"; exit 1; } - ##### Check overlap between smart-contract-core and auditors - overlap=$(echo "$groupAMembers" | grep -Fxf - <(echo "$groupCMembers")) - if [ -n "$overlap" ]; then - echo -e "\033[31mERROR: The following members are in both $SC_ADMINS and $AUDITORS: $overlap\033[0m" - echo -e "\033[31mAuditors must be external personnel and cannot be team members or admins\033[0m" + # Convert string to sorted lines and remove empty lines + echo "$SC_ADMINS_MEMBERS" | tr ' ' '\n' | sort | uniq > sc_admins_sorted.txt + echo "$SC_CORE_MEMBERS" | tr ' ' '\n' | sort | uniq > sc_core_sorted.txt + echo "$AUDITORS_MEMBERS" | tr ' ' '\n' | sort | uniq > auditors_sorted.txt + + # Check if both files exist and are not empty + if [ ! -s sc_admins_sorted.txt ] || [ ! -s auditors_sorted.txt ]; then + echo -e "\033[31mERROR: One of the membership lists is empty or failed to be generated.\033[0m" exit 1 - else - echo -e "\033[32mNo overlap found between $SC_ADMINS and $AUDITORS.\033[0m" - fi + fi + + echo "Checking for git users that are members of both $SC_ADMINS and $AUDITORS team..." + OVERLAP=$(comm -12 sc_admins_sorted.txt auditors_sorted.txt) + + if [ -n "$OVERLAP" ]; then + echo -e "\033[31mERROR: The following git users are members of both $SC_ADMINS and $AUDITORS groups: $OVERLAP\033[0m" + echo -e "\033[31mAuditors must be external personnel and cannot be team members or admins\033[0m" + exit 1 + else + echo -e "\033[32mNo overlap found between $SC_ADMINS and $AUDITORS.\033[0m" + fi + + echo "Checking for git users that are members of both $SC_CORE and $AUDITORS team..." + OVERLAP=$(comm -12 sc_admins_sorted.txt auditors_sorted.txt) - ##### Check overlap between smart-contract-admins and auditors - overlap2=$(echo "$groupBMembers" | grep -Fxf - <(echo "$groupCMembers")) - if [ -n "$overlap2" ]; then - echo -e "\033[31mERROR: The following members are in both $SC_CORE and $AUDITORS: $overlap2\033[0m" + if [ -n "$OVERLAP" ]; then + echo -e "\033[31mERROR: The following git users are members of both $SC_CORE and $AUDITORS groups: $OVERLAP\033[0m" echo -e "\033[31mAuditors must be external personnel and cannot be team members or admins\033[0m" exit 1 else echo -e "\033[32mNo overlap found between $SC_CORE and $AUDITORS.\033[0m" + echo -e "\033[32mAll checks passed\033[0m" fi diff --git a/.github/workflows_deactivated/enforceTestCoverage.yml b/.github/workflows_deactivated/enforceTestCoverage.yml index f9a326f27..aa0bda2e8 100644 --- a/.github/workflows_deactivated/enforceTestCoverage.yml +++ b/.github/workflows_deactivated/enforceTestCoverage.yml @@ -1,10 +1,18 @@ -name: Enforce Test Coverage +name: Enforce Min Test Coverage + +# - will make sure that (Foundry) unit test coverage is above min threshold +# - we start with 75%, planning to increase to 100% until EOY 2024 +# - Only the 'lines' coverage counts as 'branch' coverage is not reliable + on: - push: + pull_request: + types: [opened, synchronize, reopened] jobs: - enforce-coverage: + enforce-min-test-coverage: runs-on: ubuntu-latest + # will only run once the PR is in "Ready for Review" state + if: ${{ github.event.pull_request.draft == false }} permissions: pull-requests: write @@ -17,23 +25,26 @@ jobs: ETH_NODE_URI_BSC: ${{ secrets.ETH_NODE_URI_BSC }} ETH_NODE_URI_GNOSIS: ${{ secrets.ETH_NODE_URI_GNOSIS }} GIT_TOKEN: ${{ secrets.GIT_TOKEN }} - MIN_TEST_COVERAGE: 80 # 80 percent for now, will be increased to 100% gradually + MIN_TEST_COVERAGE: 75 # 75 percent for now, will be increased to 100% gradually until the end of 2024 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4.1.7 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' - - name: Install Dependencies - run: npm install --save-dev ts-node @types/node --legacy-peer-deps + - name: Install dev dependencies + run: yarn install - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1.0.10 + uses: foundry-rs/foundry-toolchain@v1.2.0 with: version: nightly + - name: Install Dependencies + run: forge install + - name: Install Git Submodules run: | git config --global url."https://github.com/".insteadOf "git@github.com:" diff --git a/config/amarok.json b/config/amarok.json index 93b12762d..2535c0296 100644 --- a/config/amarok.json +++ b/config/amarok.json @@ -8,7 +8,9 @@ "0x9e32b13ce7f2e80a01932b42553652e053d6ed8e", "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xdac17f958d2ee523a2206206994597c13d831ec7" + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xBC6DA0FE9aD5f3b0d58160288917AA56653660E9", + "0x0100546F2cD4C9D97f798fFC9755E47865FF7Ee6" ] }, "optimism": { @@ -19,7 +21,9 @@ "0x4200000000000000000000000000000000000006", "0x7f5c764cbc14f9669b88837ca1490cca17c31607", "0x94b008aa00579c1307b0ef2c499ad98a8ce58e58", - "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1" + "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1", + "0xCB8FA9a76b8e203D8C3797bF438d8FB81Ea3326A", + "0x3E29D3A9316dAB217754d13b28646B76607c5f04" ] }, "bsc": { @@ -73,7 +77,9 @@ "tokensToApprove": [ "0x420000000000000000000000000000000000000a", "0xbb06dca3ae6887fabf931640f67cab3e3a16f4dc", - "0xea32a96608495e54156ae48931a7c20f0dcc1a21" + "0xea32a96608495e54156ae48931a7c20f0dcc1a21", + "0x303241e2B3b4aeD0bb0F8623e7442368FED8Faf3", + "0x0E17934B9735D479B2388347fAeF0F4e58b9cc06" ] }, "base": { @@ -107,7 +113,9 @@ "chainId": 34443, "connextHandler": "0x7380511493DD4c2f1dD75E9CCe5bD52C787D4B51", "domain": "1836016741", - "tokensToApprove": ["0x4200000000000000000000000000000000000006"] + "tokensToApprove": [ + "0x4200000000000000000000000000000000000006" + ] }, "arbitrum": { "chainId": 42161, @@ -117,7 +125,9 @@ "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1", "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9", - "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8" + "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8", + "0xCB8FA9a76b8e203D8C3797bF438d8FB81Ea3326A", + "0x17573150d67d820542EFb24210371545a4868B03" ] }, "linea": { @@ -285,4 +295,4 @@ "domain": "", "tokensToApprove": [] } -} +} \ No newline at end of file diff --git a/test/solidity/LiFiDiamond.t.sol b/test/solidity/LiFiDiamond.t.sol new file mode 100644 index 000000000..fccbd8541 --- /dev/null +++ b/test/solidity/LiFiDiamond.t.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.17; + +import { LiFiDiamond } from "lifi/LiFiDiamond.sol"; +import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; +import { DiamondLoupeFacet } from "lifi/Facets/DiamondLoupeFacet.sol"; +import { OwnershipFacet } from "lifi/Facets/OwnershipFacet.sol"; +import { IDiamondCut } from "lifi/Interfaces/IDiamondCut.sol"; +import { DSTest } from "ds-test/test.sol"; +import { Vm } from "forge-std/Vm.sol"; + +contract LiFiDiamondTest is DSTest { + Vm internal immutable vm = Vm(HEVM_ADDRESS); + LiFiDiamond public diamond; + DiamondCutFacet public diamondCutFacet; + OwnershipFacet public ownershipFacet; + address public diamondOwner; + + event DiamondCut( + IDiamondCut.FacetCut[] _diamondCut, + address _init, + bytes _calldata + ); + + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + + error FunctionDoesNotExist(); + + function setUp() public { + diamondOwner = address(123456); + diamondCutFacet = new DiamondCutFacet(); + ownershipFacet = new OwnershipFacet(); + + // prepare function selector for diamondCut (OwnershipFacet) + bytes4[] memory functionSelectors = new bytes4[](1); + functionSelectors[0] = ownershipFacet.owner.selector; + + // prepare parameters for diamondCut (OwnershipFacet) + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); + cut[0] = IDiamondCut.FacetCut({ + facetAddress: address(ownershipFacet), + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: functionSelectors + }); + + diamond = new LiFiDiamond(diamondOwner, address(diamondCutFacet)); + } + + function test_DeploysWithoutErrors() public { + diamond = new LiFiDiamond(diamondOwner, address(diamondCutFacet)); + } + + function test_ForwardsCallsViaDelegateCall() public { + // only one facet with one selector is registered (diamondCut) + vm.startPrank(diamondOwner); + + DiamondLoupeFacet diamondLoupe = new DiamondLoupeFacet(); + + // make sure that this call fails (without ending the test) + bool failed = false; + try DiamondLoupeFacet(address(diamond)).facetAddresses() returns ( + address[] memory + ) {} catch { + failed = true; + } + if (!failed) revert("InvalidDiamondSetup"); + + // prepare function selectors + bytes4[] memory functionSelectors = new bytes4[](4); + functionSelectors[0] = diamondLoupe.facets.selector; + functionSelectors[1] = diamondLoupe.facetFunctionSelectors.selector; + functionSelectors[2] = diamondLoupe.facetAddresses.selector; + functionSelectors[3] = diamondLoupe.facetAddress.selector; + + // prepare diamondCut + IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](1); + cuts[0] = IDiamondCut.FacetCut({ + facetAddress: address(diamondLoupe), + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: functionSelectors + }); + + DiamondCutFacet(address(diamond)).diamondCut(cuts, address(0), ""); + } + + function test_RevertsOnUnknownFunctionSelector() public { + // call random function selectors + bytes memory callData = hex"a516f0f3"; // getPeripheryContract(string) + + vm.expectRevert(FunctionDoesNotExist.selector); + address(diamond).call(callData); + } + + function test_CanReceiveETH() public { + (bool success, ) = address(diamond).call{ value: 1 ether }(""); + if (!success) revert("ExternalCallFailed"); + + assertEq(address(diamond).balance, 1 ether); + } +} diff --git a/test/solidity/LiFiDiamondImmutable.t.sol b/test/solidity/LiFiDiamondImmutable.t.sol new file mode 100644 index 000000000..eb95103d9 --- /dev/null +++ b/test/solidity/LiFiDiamondImmutable.t.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.17; + +import { LiFiDiamondImmutable } from "lifi/LiFiDiamondImmutable.sol"; +import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; +import { DiamondLoupeFacet } from "lifi/Facets/DiamondLoupeFacet.sol"; +import { OwnershipFacet } from "lifi/Facets/OwnershipFacet.sol"; +import { IDiamondCut } from "lifi/Interfaces/IDiamondCut.sol"; +import { DSTest } from "ds-test/test.sol"; +import { Vm } from "forge-std/Vm.sol"; + +contract LiFiDiamondImmutableTest is DSTest { + Vm internal immutable vm = Vm(HEVM_ADDRESS); + LiFiDiamondImmutable public diamond; + DiamondCutFacet public diamondCutFacet; + OwnershipFacet public ownershipFacet; + address public diamondOwner; + + event DiamondCut( + IDiamondCut.FacetCut[] _diamondCut, + address _init, + bytes _calldata + ); + + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + + error FunctionDoesNotExist(); + error OnlyContractOwner(); + + function setUp() public { + diamondOwner = address(123456); + diamondCutFacet = new DiamondCutFacet(); + ownershipFacet = new OwnershipFacet(); + + // prepare function selector for diamondCut (OwnershipFacet) + bytes4[] memory functionSelectors = new bytes4[](1); + functionSelectors[0] = ownershipFacet.owner.selector; + + // prepare parameters for diamondCut (OwnershipFacet) + IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); + cut[0] = IDiamondCut.FacetCut({ + facetAddress: address(ownershipFacet), + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: functionSelectors + }); + + diamond = new LiFiDiamondImmutable( + diamondOwner, + address(diamondCutFacet) + ); + } + + function test_DeploysWithoutErrors() public { + diamond = new LiFiDiamondImmutable( + diamondOwner, + address(diamondCutFacet) + ); + } + + function test_ForwardsCallsViaDelegateCall() public { + // only one facet with one selector is registered (diamondCut) + vm.startPrank(diamondOwner); + + DiamondLoupeFacet diamondLoupe = new DiamondLoupeFacet(); + + // make sure that this call fails (without ending the test) + bool failed = false; + try DiamondLoupeFacet(address(diamond)).facetAddresses() returns ( + address[] memory + ) {} catch { + failed = true; + } + if (!failed) revert("InvalidDiamondSetup"); + + // prepare function selectors + bytes4[] memory functionSelectors = new bytes4[](4); + functionSelectors[0] = diamondLoupe.facets.selector; + functionSelectors[1] = diamondLoupe.facetFunctionSelectors.selector; + functionSelectors[2] = diamondLoupe.facetAddresses.selector; + functionSelectors[3] = diamondLoupe.facetAddress.selector; + + // prepare diamondCut + IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](1); + cuts[0] = IDiamondCut.FacetCut({ + facetAddress: address(diamondLoupe), + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: functionSelectors + }); + + DiamondCutFacet(address(diamond)).diamondCut(cuts, address(0), ""); + } + + function test_NonOwnerCannotAddFacet() public { + DiamondLoupeFacet diamondLoupe = new DiamondLoupeFacet(); + + // prepare function selectors + bytes4[] memory functionSelectors = new bytes4[](4); + functionSelectors[0] = diamondLoupe.facets.selector; + functionSelectors[1] = diamondLoupe.facetFunctionSelectors.selector; + functionSelectors[2] = diamondLoupe.facetAddresses.selector; + functionSelectors[3] = diamondLoupe.facetAddress.selector; + + // prepare diamondCut + IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](1); + cuts[0] = IDiamondCut.FacetCut({ + facetAddress: address(diamondLoupe), + action: IDiamondCut.FacetCutAction.Add, + functionSelectors: functionSelectors + }); + + vm.expectRevert(OnlyContractOwner.selector); + DiamondCutFacet(address(diamond)).diamondCut(cuts, address(0), ""); + } + + function test_RevertsOnUnknownFunctionSelector() public { + // call random function selectors + bytes memory callData = hex"a516f0f3"; // getPeripheryContract(string) + + vm.expectRevert(FunctionDoesNotExist.selector); + address(diamond).call(callData); + } + + function test_CanReceiveETH() public { + (bool success, ) = address(diamond).call{ value: 1 ether }(""); + if (!success) revert("ExternalCallFailed"); + + assertEq(address(diamond).balance, 1 ether); + } +} diff --git a/test/solidity/Periphery/ERC20Proxy.t.sol b/test/solidity/Periphery/ERC20Proxy.t.sol new file mode 100644 index 000000000..0e445cb32 --- /dev/null +++ b/test/solidity/Periphery/ERC20Proxy.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.17; + +import { ERC20Proxy } from "lifi/Periphery/ERC20Proxy.sol"; +import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; +import { DiamondLoupeFacet } from "lifi/Facets/DiamondLoupeFacet.sol"; +import { OwnershipFacet } from "lifi/Facets/OwnershipFacet.sol"; +import { IDiamondCut } from "lifi/Interfaces/IDiamondCut.sol"; +import { DSTest } from "ds-test/test.sol"; +import { Vm } from "forge-std/Vm.sol"; + +contract ERC20ProxyTest is DSTest { + Vm internal immutable vm = Vm(HEVM_ADDRESS); + ERC20Proxy public erc20Proxy; + DiamondCutFacet public diamondCutFacet; + OwnershipFacet public ownershipFacet; + address public proxyOwner; + + error FunctionDoesNotExist(); + error OnlyContractOwner(); + + function setUp() public { + proxyOwner = address(123456); + erc20Proxy = new ERC20Proxy(proxyOwner); + } + + function test_DeploysWithoutErrors() public { + erc20Proxy = new ERC20Proxy(proxyOwner); + + assertEq(erc20Proxy.owner(), proxyOwner); + } + + function test_CannotReceiveETH() public { + (bool success, ) = address(erc20Proxy).call{ value: 1 ether }(""); + + assertTrue( + success == false, + "Contract can receive ETH but should not be able to" + ); + } +}