Skip to content

Conversation

@s2imonovic
Copy link
Collaborator

@s2imonovic s2imonovic commented Oct 16, 2025

  • Adds fork tests for testing contract upgrades on live networks.
  • Includes BaseForkTest abstract contract and specific test implementations for GatewayEVM, ERC20Custody and GatewayZEVM contracts with state preservation verification.
  • Fork Testing: https://getfoundry.sh/forge/tests/fork-testing/

Closes: #571

Summary by CodeRabbit

  • New Features

    • Added a reusable multi-chain fork testing framework and new fork test suites for ERC20 custody and gateway contracts.
  • Tests

    • Cross-chain upgrade tests validate pre/post-upgrade state and implementation address changes with logging across Ethereum, BSC, Polygon, Base, Arbitrum, Avalanche, and ZetaChain.
  • Security

    • Added TSS role restriction to two gateway entry points to tighten access control.
  • Style

    • Widespread formatting consolidation of public/interface declarations (no behavioral changes).

@s2imonovic s2imonovic requested review from a team as code owners October 16, 2025 18:21
@github-actions github-actions bot added the feat label Oct 16, 2025
@s2imonovic s2imonovic changed the title feat: add fork tests for upgradable contracts test: add fork tests for upgradable contracts Oct 16, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

📝 Walkthrough

Walkthrough

Adds a Foundry fork-testing framework (BaseForkTest) and three concrete fork tests (ERC20CustodyForkTest, GatewayEVMForkTest, GatewayZEVMForkTest) that configure multi-chain forks and perform per-chain proxy upgrade flows with pre/post-state checks. Also includes mostly formatting changes across contracts and an added TSS_ROLE restriction on GatewayEVM.execute/executeRevert.

Changes

Cohort / File(s) Summary
Base fork testing framework
test/fork/BaseForkTest.sol
New abstract contract: ChainConfig struct and chains array, setUp() creating forks, abstract hooks _setupChains() and _testUpgradeContract(...), helpers testForkIdDiffer(), testCanSwitchForks(), testUpgradeGatewayOnAllChains(), and _getImplementation(...).
ERC20 custody fork test
test/fork/ERC20CustodyFork.t.sol
New ERC20CustodyForkTest implementing _setupChains() for six networks and _testUpgradeContract(...) performing fork selection, pre-upgrade state capture, deploy new ERC20Custody impl, call upgradeToAndCall, verify implementation and preserved state.
Gateway EVM fork test
test/fork/GatewayEVMFork.t.sol
New GatewayEVMForkTest with six-chain configs and _testUpgradeContract(...) that selects fork, records custody/TSS/connector/token state, deploys new GatewayEVM impl under admin prank, upgrades proxy, verifies implementation and state persistence.
Gateway ZEVM fork test
test/fork/GatewayZEVMFork.t.sol
New GatewayZEVMForkTest registering ZetaChain in _setupChains() and _testUpgradeContract(...) that selects fork, records zetaToken(), deploys new impl, performs admin upgrade, verifies implementation and token preservation.
GatewayEVM access control change
contracts/evm/GatewayEVM.sol, contracts/evm/interfaces/IGatewayEVM.sol
Adds onlyRole(TSS_ROLE) restriction to executeRevert and execute in GatewayEVM (plus formatting; interface signatures collapsed to single-line).
Formatting-only changes (collapsed signatures / minor reflows)
contracts/... test/... (many files; examples below)
Widespread formatting: multi-line function and constructor parameter lists collapsed to single-line forms or minor parenthesis reflows. Files include but not limited to: contracts/evm/Registry.sol, ZetaConnector*.sol, CoreRegistry.sol, GatewayZEVM.sol, ZRC20.sol, various contracts/helpers/*, contracts/zevm/*, and many test files under test/* and test/utils/*. No behavioral changes in these formatting edits.
ERC20Custody behavior formatting
contracts/evm/ERC20Custody.sol
Minor reformatting of deposit/withdraw function declarations only; no semantic changes.

Sequence Diagram(s)

sequenceDiagram
    participant Runner as Fork Test Runner
    participant Base as BaseForkTest
    participant VM as Foundry VM (vm)
    participant Proxy as On-chain Proxy
    participant Impl as New Implementation

    Runner->>Base: setUp()
    activate Base
    Base->>VM: createFork(rpcUrl) for each chain -> forkId
    Base->>Runner: _setupChains()
    deactivate Base

    Runner->>Base: testUpgradeGatewayOnAllChains()
    activate Base
    loop per ChainConfig
        Base->>VM: selectFork(config.forkId)
        Base->>Runner: _testUpgradeContract(config)
        activate Runner
        Runner->>Proxy: read pre-upgrade state (storage)
        Runner->>VM: startPrank(admin) [if required]
        Runner->>VM: deploy Impl
        VM-->>Runner: implAddr
        Runner->>Proxy: upgradeToAndCall(implAddr, initData)
        Proxy-->>Runner: implementation updated
        Runner->>Proxy: read post-upgrade state (verify preserved)
        Runner->>VM: stopPrank()
        Runner-->>Base: log success/failure
        deactivate Runner
    end
    deactivate Base
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The PR introduces an out-of-scope functional change to GatewayEVM.sol. In addition to formatting adjustments, the execute and executeRevert functions now include onlyRole(TSS_ROLE) access control restrictions. This access control addition changes the authorization model of these functions and is unrelated to the PR's primary objective of adding fork tests for upgrade validation. The extensive formatting changes throughout other files, while not ideal, are minor stylistic consolidations that do not alter behavior. The access control change, however, represents a meaningful functional modification outside the scope of testing infrastructure. Remove the onlyRole(TSS_ROLE) access control modifications to executeRevert and execute functions in GatewayEVM.sol, as these represent a functional contract change unrelated to the fork testing infrastructure. If access control changes to GatewayEVM are intended, they should be submitted as a separate pull request with appropriate issue tracking and testing requirements.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "test: add fork tests for upgradable contracts" is concise, specific, and directly reflects the primary change in the changeset. The PR introduces a BaseForkTest abstract contract and three specific fork test implementations (ERC20CustodyForkTest, GatewayEVMForkTest, and GatewayZEVMForkTest) to test contract upgrades across multiple chains. The title accurately captures this main objective without using vague terminology or unnecessary detail.
Linked Issues Check ✅ Passed The fork test implementations address the requirements in issue #571. Each fork test (ERC20CustodyForkTest, GatewayEVMForkTest, GatewayZEVMForkTest) implements the _testUpgradeContract method to perform actual upgrade testing with state preservation verification. The tests deploy new contract implementations, execute upgrades via proxy, verify implementation addresses match, and assert that critical state variables (e.g., gateway, tssAddress, zetaToken) are preserved post-upgrade. BaseForkTest provides the scaffolding for multi-chain upgrade testing. These implementations directly satisfy the issue's objective to ensure upgrade tests validate storage layout and state preservation.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch test/fork-tests-upgrades

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added test and removed feat labels Oct 16, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6b9d7c0 and 42ddb51.

📒 Files selected for processing (4)
  • test/fork/BaseForkTest.sol (1 hunks)
  • test/fork/ERC20CustodyFork.t.sol (1 hunks)
  • test/fork/GatewayEVMFork.t.sol (1 hunks)
  • test/fork/GatewayZEVMFork.t.sol (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
test/**

⚙️ CodeRabbit configuration file

Review the test files for proper coverage, edge cases, and best practices.

Files:

  • test/fork/GatewayEVMFork.t.sol
  • test/fork/ERC20CustodyFork.t.sol
  • test/fork/GatewayZEVMFork.t.sol
  • test/fork/BaseForkTest.sol
🪛 GitHub Actions: Test
test/fork/GatewayEVMFork.t.sol

[error] 1-1: vm.envString: environment variable "ETHEREUM_RPC_URL" not found

test/fork/ERC20CustodyFork.t.sol

[error] 1-1: vm.envString: environment variable "ETHEREUM_RPC_URL" not found

test/fork/GatewayZEVMFork.t.sol

[error] 1-1: vm.envString: environment variable "ETHEREUM_RPC_URL" not found

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: slither
  • GitHub Check: generate

@s2imonovic
Copy link
Collaborator Author

s2imonovic commented Oct 16, 2025

@lumtis @skosito

  1. We can either hardcode the RPC URLs directly in the code or keep reading them from .env file (as we do now), but in that case, we should define them in the secrets.

  2. Can I delete the old upgrade tests that only change one event and don’t check the actual storage?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
test/fork/BaseForkTest.sol (1)

9-15: Guard missing RPC env vars to prevent CI failures.

This issue was already flagged in a previous review: vm.envString reverts when the environment variable is unset, causing CI failures when RPC URLs are missing. Use vm.envOr with a default value or check vm.envExists first, and update setUp() to gracefully skip or bail out when required RPC URLs are unavailable.

🧹 Nitpick comments (3)
test/fork/BaseForkTest.sol (3)

59-66: Prefer assertNotEq over assert for better test output.

Using assert provides minimal information on failure. Replace with assertNotEq(ethereumForkId, bscForkId, "Fork IDs must differ") (and so on) to get clearer diagnostics showing which fork IDs collided.

Apply this diff:

 function testForkIdDiffer() public view {
-    assert(ethereumForkId != bscForkId);
-    assert(bscForkId != polygonForkId);
-    assert(polygonForkId != baseForkId);
-    assert(baseForkId != arbitrumForkId);
-    assert(arbitrumForkId != avalancheForkId);
-    assert(avalancheForkId != zetachainForkId);
+    assertNotEq(ethereumForkId, bscForkId, "Ethereum and BSC fork IDs must differ");
+    assertNotEq(bscForkId, polygonForkId, "BSC and Polygon fork IDs must differ");
+    assertNotEq(polygonForkId, baseForkId, "Polygon and Base fork IDs must differ");
+    assertNotEq(baseForkId, arbitrumForkId, "Base and Arbitrum fork IDs must differ");
+    assertNotEq(arbitrumForkId, avalancheForkId, "Arbitrum and Avalanche fork IDs must differ");
+    assertNotEq(avalancheForkId, zetachainForkId, "Avalanche and ZetaChain fork IDs must differ");
 }

75-80: Consider renaming for clarity.

The function name testUpgradeGatewayOnAllChains is specific to Gateway contracts, but this is a base class that can test any upgradable contract (as evidenced by ERC20CustodyForkTest also using this base). Consider renaming to testUpgradeContractOnAllChains for better clarity.


45-55: Refactor fork creation to avoid requiring all RPC env vars
Eagerly calling vm.createFork for every supported chain in setUp() forces all seven RPC_URL env vars to be set, even if a subclass only needs a subset. Move those createFork calls into _setupChains() or guard against empty URLs so tests can opt into only the networks they actually use.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 42ddb51 and 9666280.

📒 Files selected for processing (1)
  • test/fork/BaseForkTest.sol (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
test/**

⚙️ CodeRabbit configuration file

Review the test files for proper coverage, edge cases, and best practices.

Files:

  • test/fork/BaseForkTest.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: generate
  • GitHub Check: slither
  • GitHub Check: test

Copy link
Member

@lumtis lumtis left a comment

Choose a reason for hiding this comment

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

uint256 avalancheForkId;
uint256 zetachainForkId;

// Mainnet admins.
Copy link
Member

Choose a reason for hiding this comment

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

The test only work for mainnet? We should precise it somewhere. Otherwise can't we make it dependent on the network used?

assertEq(address(custody.gateway()), gatewayBefore, "Gateway address changed.");
assertEq(custody.tssAddress(), tssAddressBefore, "TSS address changed.");
assertEq(custody.supportsLegacy(), supportsLegacyBefore, "Supports legacy changed.");
console.log("State preserved after upgrade");
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can also check the the admin is preserved? Same comment for other contracts

@lumtis
Copy link
Member

lumtis commented Oct 17, 2025

@lumtis @skosito

  1. We can either hardcode the RPC URLs directly in the code or keep reading them from .env file (as we do now), but in that case, we should define them in the secrets.
  2. Can I delete the old upgrade tests that only change one event and don’t check the actual storage?
  1. I think it's fine using public RPC, they are generally stable and we can still update the hardcoded value easily
  2. As I put in a comment, yes we can.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
test/utils/upgrades/ZetaConnectorNonNativeUpgradeTest.sol (1)

47-56: Action: Align withdraw() Natspec with actual role check (TSS vs WITHDRAWER_ROLE).

Natspec says "only TSS address" but the function uses onlyRole(WITHDRAWER_ROLE). Update the @dev to mention WITHDRAWER_ROLE or change the modifier to onlyRole(TSS_ROLE) depending on intended authority. File: test/utils/upgrades/ZetaConnectorNonNativeUpgradeTest.sol (withdraw()).

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9666280 and 9048ce0.

📒 Files selected for processing (32)
  • contracts/evm/ERC20Custody.sol (2 hunks)
  • contracts/evm/GatewayEVM.sol (6 hunks)
  • contracts/evm/Registry.sol (7 hunks)
  • contracts/evm/ZetaConnectorBase.sol (1 hunks)
  • contracts/evm/ZetaConnectorNative.sol (1 hunks)
  • contracts/evm/ZetaConnectorNonNative.sol (2 hunks)
  • contracts/evm/interfaces/IGatewayEVM.sol (3 hunks)
  • contracts/evm/legacy/ZetaConnector.base.sol (2 hunks)
  • contracts/evm/legacy/ZetaConnector.eth.sol (3 hunks)
  • contracts/evm/legacy/ZetaConnector.non-eth.sol (3 hunks)
  • contracts/evm/legacy/ZetaInterfaces.sol (1 hunks)
  • contracts/helpers/BaseRegistry.sol (3 hunks)
  • contracts/helpers/interfaces/IBaseRegistry.sol (3 hunks)
  • contracts/zevm/CoreRegistry.sol (7 hunks)
  • contracts/zevm/GatewayZEVM.sol (5 hunks)
  • contracts/zevm/ZRC20.sol (1 hunks)
  • contracts/zevm/interfaces/IGatewayZEVM.sol (3 hunks)
  • contracts/zevm/interfaces/UniversalContract.sol (2 hunks)
  • contracts/zevm/legacy/ZetaConnectorZEVM.sol (3 hunks)
  • test/CoreRegistry.t.sol (1 hunks)
  • test/ERC20Custody.t.sol (3 hunks)
  • test/GatewayEVM.t.sol (1 hunks)
  • test/GatewayEVMZEVM.t.sol (1 hunks)
  • test/GatewayZEVM.t.sol (2 hunks)
  • test/Registry.t.sol (1 hunks)
  • test/ZRC20.t.sol (1 hunks)
  • test/utils/ReceiverEVM.sol (1 hunks)
  • test/utils/upgrades/ERC20CustodyUpgradeTest.sol (2 hunks)
  • test/utils/upgrades/GatewayEVMUpgradeTest.sol (7 hunks)
  • test/utils/upgrades/GatewayZEVMUpgradeTest.sol (4 hunks)
  • test/utils/upgrades/ZetaConnectorNativeUpgradeTest.sol (1 hunks)
  • test/utils/upgrades/ZetaConnectorNonNativeUpgradeTest.sol (2 hunks)
✅ Files skipped from review due to trivial changes (25)
  • contracts/evm/interfaces/IGatewayEVM.sol
  • contracts/zevm/legacy/ZetaConnectorZEVM.sol
  • contracts/evm/ZetaConnectorBase.sol
  • contracts/zevm/CoreRegistry.sol
  • test/GatewayEVMZEVM.t.sol
  • test/CoreRegistry.t.sol
  • contracts/evm/ERC20Custody.sol
  • contracts/zevm/ZRC20.sol
  • test/Registry.t.sol
  • test/ZRC20.t.sol
  • contracts/evm/legacy/ZetaConnector.non-eth.sol
  • contracts/evm/ZetaConnectorNonNative.sol
  • contracts/zevm/GatewayZEVM.sol
  • contracts/evm/ZetaConnectorNative.sol
  • contracts/evm/legacy/ZetaConnector.base.sol
  • test/ERC20Custody.t.sol
  • test/utils/upgrades/ERC20CustodyUpgradeTest.sol
  • test/GatewayZEVM.t.sol
  • test/GatewayEVM.t.sol
  • contracts/evm/legacy/ZetaConnector.eth.sol
  • contracts/helpers/interfaces/IBaseRegistry.sol
  • test/utils/upgrades/ZetaConnectorNativeUpgradeTest.sol
  • contracts/zevm/interfaces/IGatewayZEVM.sol
  • contracts/zevm/interfaces/UniversalContract.sol
  • test/utils/upgrades/GatewayEVMUpgradeTest.sol
🧰 Additional context used
📓 Path-based instructions (2)
contracts/**

⚙️ CodeRabbit configuration file

Review the Solidity contracts for security vulnerabilities and best practices.

Files:

  • contracts/helpers/BaseRegistry.sol
  • contracts/evm/Registry.sol
  • contracts/evm/legacy/ZetaInterfaces.sol
  • contracts/evm/GatewayEVM.sol
test/**

⚙️ CodeRabbit configuration file

Review the test files for proper coverage, edge cases, and best practices.

Files:

  • test/utils/ReceiverEVM.sol
  • test/utils/upgrades/GatewayZEVMUpgradeTest.sol
  • test/utils/upgrades/ZetaConnectorNonNativeUpgradeTest.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: generate
  • GitHub Check: slither
🔇 Additional comments (10)
test/utils/ReceiverEVM.sol (1)

76-83: LGTM! Formatting change only.

The function signature has been consolidated to a single line without any functional changes.

contracts/helpers/BaseRegistry.sol (1)

284-284: LGTM! Formatting changes align with PR-wide standardization.

These function declarations have been reformatted from multiline to single-line signatures without any semantic changes. The modifications are consistent with the broader formatting effort across the PR and introduce no security, correctness, or behavioral issues.

Also applies to: 298-298, 339-339

contracts/evm/Registry.sol (1)

35-35: LGTM! Formatting changes improve consistency.

The function declarations have been reformatted from multi-line to single-line forms without altering signatures, visibility, or behavior. This is consistent with the broader formatting standardization effort across the codebase.

Also applies to: 65-65, 96-96, 110-110, 124-124, 200-200, 222-222

contracts/evm/legacy/ZetaInterfaces.sol (1)

96-98: Formatting-only change; ABI/signature unchanged — OK.

Params, visibility, and return type are identical; selector unaffected. Good to merge.

contracts/evm/GatewayEVM.sol (1)

106-106: LGTM: Formatting consolidation improves consistency.

The function signature formatting changes (multi-line to single-line) are purely cosmetic and don't alter parameter types, names, visibility, or modifiers. The consolidation improves code consistency across the contract.

Also applies to: 127-127, 246-246, 263-263, 308-308, 426-426

test/utils/upgrades/GatewayZEVMUpgradeTest.sol (4)

149-171: Formatting-only change; behavior unchanged. Confirm event rename scope.

Signature reflow looks good. Also, only this ZRC20 withdraw emits WithdrawnV2 while other withdraw/withdrawAndCall still emit Withdrawn—confirm this asymmetry is intentional for the upgrade test.


255-277: LGTM: single-line signature reflow.

No ABI/behavior change introduced.


475-486: LGTM: signature reflow only.

No logic changes.


503-513: LGTM: signature reflow only.

No logic changes.

test/utils/upgrades/ZetaConnectorNonNativeUpgradeTest.sol (1)

24-32: LGTM: initialize signature reflow.

No functional change.

@sherlock-ai-beta
Copy link

Sherlock AI Findings

The automated tool identified the following potential security issues in the codebase. Please review the details for each issue in the linked dashboard.

# Title Severity Details
1 Insufficient Approval Handling in Cross-Chain Message Propagation Leading to Failed Transactions and Locked Funds Medium View Details
2 Initialization reverts due to nested initializer modifiers in ZetaConnectorNative.initialize Medium View Details

Next Steps: Review the linked issues in the dashboard and address high-severity bugs first. Contact the team if you need assistance.

Full report available at: https://ai.sherlock.xyz/runs/e2e6633a-9ed0-4774-9a11-1c38ded4ee9c

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GatewayEVMUpgradeTest is not checking storage layout

2 participants