Skip to content

Conversation

@shunkakinoki
Copy link
Collaborator

  • Updated TrailsRouter contract to initialize MULTICALL3 address via constructor instead of as an immutable variable.
  • Modified deployRouter function to pass Multicall3 address during TrailsRouter deployment.
  • Added a mock AlwaysFailingMulticall3 for testing purposes to simulate failure scenarios in tests.
  • Updated relevant test cases to reflect changes in constructor and deployment logic.

…rgument

- Updated TrailsRouter contract to initialize MULTICALL3 address via constructor instead of as an immutable variable.
- Modified deployRouter function to pass Multicall3 address during TrailsRouter deployment.
- Added a mock AlwaysFailingMulticall3 for testing purposes to simulate failure scenarios in tests.
- Updated relevant test cases to reflect changes in constructor and deployment logic.
…icall3 address

- Adjusted gas usage for various test cases in TrailsRouter and TrailsRouterTest to reflect recent performance changes.
- Updated expectedRouterAddress function in TrailsRouterDeploymentTest and TrailsRouterShimDeploymentTest to incorporate the Multicall3 address in the CREATE2 address calculation.
@shunkakinoki shunkakinoki self-assigned this Nov 27, 2025
@shunkakinoki shunkakinoki marked this pull request as ready for review November 27, 2025 14:52
@shunkakinoki shunkakinoki requested review from a team and Copilot November 27, 2025 14:52
// -------------------------------------------------------------------------

address public immutable MULTICALL3 = 0xcA11bde05977b3631167028862bE2a173976CA11;
address public MULTICALL3;
Copy link
Member

Choose a reason for hiding this comment

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

Why is this no longer immutable?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thank you! Fixed 🙏

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the TrailsRouter contract to accept the Multicall3 address as a constructor argument instead of using a hardcoded immutable variable. However, this refactoring introduces critical security vulnerabilities by changing MULTICALL3 from an immutable to a mutable state variable.

Key Changes:

  • Modified TrailsRouter to initialize MULTICALL3 via constructor parameter instead of hardcoded immutable
  • Updated deployment scripts to pass the Multicall3 address during contract creation
  • Added AlwaysFailingMulticall3 mock to simplify failure testing (replacing storage manipulation approach)

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/TrailsRouter.sol CRITICAL: Changed MULTICALL3 from immutable to mutable state variable and added constructor - introduces storage slot conflicts in delegatecall context
script/TrailsRouter.s.sol Updated deployRouter to pass Multicall3 address as constructor argument
test/TrailsRouter.t.sol Added AlwaysFailingMulticall3 mock and simplified failure tests by removing storage manipulation
test/TrailsRouterShim.t.sol CRITICAL BUG: Updated vm.etch calls with constructor argument, but storage is not copied, leaving MULTICALL3 uninitialized
test/script/TrailsRouter.s.t.sol Updated CREATE2 address calculation to include constructor argument in initCode
test/script/TrailsRouterShim.s.t.sol Updated CREATE2 address calculation to include constructor argument in initCode
.gas-snapshot Reflects gas cost changes from deployment modifications and simplified test mocks

Critical Issues Identified:

  1. Security vulnerability: MULTICALL3 as a mutable state variable creates storage slot 0 conflicts when TrailsRouter is used via delegatecall, potentially allowing the address to be overwritten
  2. Test bugs: vm.etch only copies bytecode, not storage, so MULTICALL3 will be address(0) in etched contexts
  3. Missing validation: Constructor doesn't validate that _multicall3 is not address(0)

Recommendation: Revert MULTICALL3 to immutable while keeping the constructor parameterization. Immutable variables are stored in bytecode, not storage, making them safe for delegatecall usage and eliminating the need for storage initialization in tests.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// -------------------------------------------------------------------------

address public immutable MULTICALL3 = 0xcA11bde05977b3631167028862bE2a173976CA11;
address public MULTICALL3;
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

Critical security issue: Changing MULTICALL3 from immutable to a regular state variable introduces storage slot 0 conflicts when TrailsRouter is used via delegatecall (its primary use case). This could lead to the MULTICALL3 address being accidentally overwritten by the calling contract's storage operations at slot 0, potentially redirecting multicall operations to an attacker-controlled address. The immutable modifier should be restored, as it ensures the address is stored in contract bytecode rather than storage, avoiding this vulnerability.

Suggested change
address public MULTICALL3;
address public immutable MULTICALL3;

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +35
constructor(address _multicall3) {
MULTICALL3 = _multicall3;
}
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

The constructor parameter should accept a immutable assignment instead of a mutable state variable. With immutable variables, the address is encoded in the deployed bytecode rather than stored in contract storage. This is critical for a contract primarily used via delegatecall, as it prevents storage slot conflicts with the calling contract. Consider: address public immutable MULTICALL3; constructor(address _multicall3) { MULTICALL3 = _multicall3; }

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +35
constructor(address _multicall3) {
MULTICALL3 = _multicall3;
}
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

Missing input validation: The constructor should validate that _multicall3 is not the zero address to prevent deployment with an invalid multicall address. Consider adding: if (_multicall3 == address(0)) revert InvalidMulticall3Address(); This pattern is already used in TrailsRouterShim (see TrailsRouterShim.sol:34).

Copilot uses AI. Check for mistakes.
// Verify sentinel by re-etching TrailsRouter and validating via delegated entrypoint
bytes memory original = address(shimImpl).code;
vm.etch(holder, address(new TrailsRouter()).code);
vm.etch(holder, address(new TrailsRouter(0xcA11bde05977b3631167028862bE2a173976CA11)).code);
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

Critical bug: vm.etch only copies runtime bytecode, not storage. Since MULTICALL3 is now a storage variable (not immutable), it will be uninitialized (address(0)) at the holder address after this etch operation. This means any subsequent calls to validateOpHashAndSweep that perform multicall operations will fail or behave unexpectedly. The original code worked because immutable variables are embedded in the bytecode itself. Either revert MULTICALL3 to immutable, or explicitly set the storage slot after etching: vm.store(holder, bytes32(uint256(0)), bytes32(uint256(uint160(0xcA11bde05977b3631167028862bE2a173976CA11))));

Suggested change
vm.etch(holder, address(new TrailsRouter(0xcA11bde05977b3631167028862bE2a173976CA11)).code);
vm.etch(holder, address(new TrailsRouter(0xcA11bde05977b3631167028862bE2a173976CA11)).code);
// Set MULTICALL3 storage slot (slot 0) to the correct address after etching
vm.store(holder, bytes32(uint256(0)), bytes32(uint256(uint160(0xcA11bde05977b3631167028862bE2a173976CA11))));

Copilot uses AI. Check for mistakes.
// Verify via TrailsRouter delegated validation
bytes memory original = address(shimImpl).code;
vm.etch(holder, address(new TrailsRouter()).code);
vm.etch(holder, address(new TrailsRouter(0xcA11bde05977b3631167028862bE2a173976CA11)).code);
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

Critical bug: vm.etch only copies runtime bytecode, not storage. Since MULTICALL3 is now a storage variable (not immutable), it will be uninitialized (address(0)) at the holder address after this etch operation. Any subsequent operations that rely on MULTICALL3 will fail. The original code worked because immutable variables are embedded in the bytecode itself. Either revert MULTICALL3 to immutable, or explicitly set the storage slot after etching: vm.store(holder, bytes32(uint256(0)), bytes32(uint256(uint160(0xcA11bde05977b3631167028862bE2a173976CA11))));

Suggested change
vm.etch(holder, address(new TrailsRouter(0xcA11bde05977b3631167028862bE2a173976CA11)).code);
vm.etch(holder, address(new TrailsRouter(0xcA11bde05977b3631167028862bE2a173976CA11)).code);
// Explicitly set MULTICALL3 storage slot after etch
vm.store(holder, bytes32(uint256(0)), bytes32(uint256(uint160(0xcA11bde05977b3631167028862bE2a173976CA11))));

Copilot uses AI. Check for mistakes.
Comment on lines 24 to 25
// State Variables
// -------------------------------------------------------------------------
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

Misleading section header: This section is labeled "State Variables" but should be "Immutable Variables" (if the critical security issue is fixed and MULTICALL3 is made immutable). If MULTICALL3 remains mutable, the comment should explain why this is necessary despite the security risks of storage slot conflicts during delegatecall usage.

Copilot uses AI. Check for mistakes.
- Updated the MULTICALL3 state variable in the TrailsRouter contract to be immutable, ensuring it can only be set once during contract deployment. This change enhances gas efficiency and contract integrity.
- Adjusted gas usage metrics for various test cases in TrailsRouter and TrailsRouterTest to reflect recent performance optimizations.
- Minor updates made to individual test cases for consistency and accuracy in gas calculations.
…ariables

- Changed the comment for the MULTICALL3 variable to clarify its immutable status, enhancing code readability and consistency with naming conventions.
@shunkakinoki shunkakinoki merged commit f4c92b6 into master Nov 27, 2025
1 check passed
@shunkakinoki shunkakinoki deleted the multicall3-router-version branch November 27, 2025 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants