Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
70 changes: 35 additions & 35 deletions .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,20 @@ TrailsIntentEntrypointTest:testNonceIncrementsOnDeposit() (gas: 91830)
TrailsIntentEntrypointTest:testPermitAmountExcessiveWithFee() (gas: 61879)
TrailsIntentEntrypointTest:testPermitAmountInsufficientWithFee() (gas: 60889)
TrailsIntentEntrypointTest:testVersionConstant() (gas: 10506)
TrailsRouterDeploymentTest:test_DeployTrailsRouter_SameAddress() (gas: 1856117)
TrailsRouterDeploymentTest:test_DeployTrailsRouter_Success() (gas: 1846511)
TrailsRouterDeploymentTest:test_DeployedRouter_HasCorrectConfiguration() (gas: 1846307)
TrailsRouterShimDeploymentTest:test_DeployRouterShim_SameAddress() (gas: 7530847)
TrailsRouterShimDeploymentTest:test_DeployRouterShim_Success() (gas: 4873481)
TrailsRouterShimDeploymentTest:test_DeployedContract_HasCorrectConfiguration() (gas: 4873526)
TrailsRouterDeploymentTest:test_DeployTrailsRouter_SameAddress() (gas: 1878664)
TrailsRouterDeploymentTest:test_DeployTrailsRouter_Success() (gas: 1866502)
TrailsRouterDeploymentTest:test_DeployedRouter_HasCorrectConfiguration() (gas: 1866298)
TrailsRouterShimDeploymentTest:test_DeployRouterShim_SameAddress() (gas: 7617549)
TrailsRouterShimDeploymentTest:test_DeployRouterShim_Success() (gas: 4925623)
TrailsRouterShimDeploymentTest:test_DeployedContract_HasCorrectConfiguration() (gas: 4925670)
TrailsRouterShimTest:testConstructorValidation() (gas: 69336)
TrailsRouterShimTest:testForwardToRouterReturnValue() (gas: 713355)
TrailsRouterShimTest:testRouterAddressImmutable() (gas: 1391679)
TrailsRouterShimTest:test_constructor_revert_zeroRouter() (gas: 68984)
TrailsRouterShimTest:test_delegatecall_forwards_and_sets_sentinel_sstore_inactive() (gas: 1848663)
TrailsRouterShimTest:test_delegatecall_forwards_and_sets_sentinel_sstore_inactive() (gas: 1863211)
TrailsRouterShimTest:test_delegatecall_forwards_and_sets_sentinel_tstore_active() (gas: 38392)
TrailsRouterShimTest:test_delegatecall_router_revert_bubbles_as_RouterCallFailed() (gas: 82109)
TrailsRouterShimTest:test_delegatecall_sets_sentinel_with_sstore_when_no_tstore() (gas: 1830888)
TrailsRouterShimTest:test_delegatecall_sets_sentinel_with_sstore_when_no_tstore() (gas: 1845437)
TrailsRouterShimTest:test_delegatecall_sets_sentinel_with_tstore_when_supported() (gas: 20706)
TrailsRouterShimTest:test_direct_handleSequenceDelegateCall_reverts_not_delegatecall() (gas: 9840)
TrailsRouterShimTest:test_forwardToRouter_return_data_handling() (gas: 729052)
Expand All @@ -78,12 +78,12 @@ TrailsRouterShimTest:test_handleSequenceDelegateCall_with_eth_value() (gas: 3210
TrailsRouterShimTest:test_handleSequenceDelegateCall_zero_call_value() (gas: 26873)
TrailsRouterShimTest:test_sentinel_setting_with_different_op_hashes() (gas: 36405)
TrailsRouterTest:testDelegateCallWithETH() (gas: 326226)
TrailsRouterTest:testExecute_WithFailingMulticall() (gas: 450176)
TrailsRouterTest:testExecute_WithFailingMulticall() (gas: 158857)
TrailsRouterTest:testHandleSequenceDelegateCall_InjectAndCall() (gas: 72655)
TrailsRouterTest:testHandleSequenceDelegateCall_RefundAndSweep() (gas: 85388)
TrailsRouterTest:testHandleSequenceDelegateCall_Sweep() (gas: 48693)
TrailsRouterTest:testHandleSequenceDelegateCall_ValidateOpHashAndSweep() (gas: 60042)
TrailsRouterTest:testIncorrectValueValidation() (gas: 24727)
TrailsRouterTest:testHandleSequenceDelegateCall_ValidateOpHashAndSweep() (gas: 60010)
TrailsRouterTest:testIncorrectValueValidation() (gas: 24724)
TrailsRouterTest:testInjectAndCall_NoReplacementNeeded() (gas: 989350)
TrailsRouterTest:testInjectAndCall_WithReplacement() (gas: 996073)
TrailsRouterTest:testInjectAndCall_WithTokenZeroBalance() (gas: 647158)
Expand All @@ -104,20 +104,20 @@ TrailsRouterTest:testRevertWhen_injectSweepAndCall_InsufficientAllowance() (gas:
TrailsRouterTest:testRevertWhen_injectSweepAndCall_NoValueAvailable() (gas: 14469)
TrailsRouterTest:testSweepAndCallETH() (gas: 78660)
TrailsRouterTest:testValidateOpHashAndSweep_WithoutSentinel() (gas: 16498)
TrailsRouterTest:test_Execute_FromContract_ShouldPreserveContractAsSender() (gas: 24543)
TrailsRouterTest:test_Execute_FromEOA_ShouldPreserveEOAAsSender() (gas: 29232)
TrailsRouterTest:test_Execute_WithMultipleCalls() (gas: 30853)
TrailsRouterTest:test_Multicall3Address_IsCorrect() (gas: 6658)
TrailsRouterTest:test_Execute_FromContract_ShouldPreserveContractAsSender() (gas: 24655)
TrailsRouterTest:test_Execute_FromEOA_ShouldPreserveEOAAsSender() (gas: 29344)
TrailsRouterTest:test_Execute_WithMultipleCalls() (gas: 30965)
TrailsRouterTest:test_Multicall3Address_IsCorrect() (gas: 8766)
TrailsRouterTest:test_ReceiveETH_ShouldAcceptETH() (gas: 18186)
TrailsRouterTest:test_RevertWhen_allowFailure_true_allCalls() (gas: 102086)
TrailsRouterTest:test_RevertWhen_allowFailure_true_firstOfMultipleCalls() (gas: 100522)
TrailsRouterTest:test_RevertWhen_allowFailure_true_lastOfMultipleCalls() (gas: 101195)
TrailsRouterTest:test_RevertWhen_allowFailure_true_middleOfMultipleCalls() (gas: 100738)
TrailsRouterTest:test_RevertWhen_allowFailure_true_singleCall() (gas: 97583)
TrailsRouterTest:test_RevertWhen_execute_withETH_allowFailure_true() (gas: 24244)
TrailsRouterTest:test_RevertWhen_pullAmountAndExecute_InsufficientAllowance() (gas: 28697)
TrailsRouterTest:test_RevertWhen_pullAndExecute_InsufficientAllowance() (gas: 31289)
TrailsRouterTest:test_RevertWhen_pullAndExecute_allowFailure_true() (gas: 98784)
TrailsRouterTest:test_RevertWhen_allowFailure_true_allCalls() (gas: 102083)
TrailsRouterTest:test_RevertWhen_allowFailure_true_firstOfMultipleCalls() (gas: 100519)
TrailsRouterTest:test_RevertWhen_allowFailure_true_lastOfMultipleCalls() (gas: 101192)
TrailsRouterTest:test_RevertWhen_allowFailure_true_middleOfMultipleCalls() (gas: 100735)
TrailsRouterTest:test_RevertWhen_allowFailure_true_singleCall() (gas: 97580)
TrailsRouterTest:test_RevertWhen_execute_withETH_allowFailure_true() (gas: 24241)
TrailsRouterTest:test_RevertWhen_pullAmountAndExecute_InsufficientAllowance() (gas: 28694)
TrailsRouterTest:test_RevertWhen_pullAndExecute_InsufficientAllowance() (gas: 31286)
TrailsRouterTest:test_RevertWhen_pullAndExecute_allowFailure_true() (gas: 98781)
TrailsRouterTest:test_amount_offset_out_of_bounds() (gas: 278887)
TrailsRouterTest:test_direct_sweep_reverts_not_delegatecall() (gas: 12249)
TrailsRouterTest:test_handleSequenceDelegateCall_dispatches_to_sweep_native() (gas: 50491)
Expand All @@ -126,23 +126,23 @@ TrailsRouterTest:test_native_transfer_failed() (gas: 89708)
TrailsRouterTest:test_no_tokens_to_pull() (gas: 650507)
TrailsRouterTest:test_no_tokens_to_sweep() (gas: 909170)
TrailsRouterTest:test_placeholder_mismatch() (gas: 279607)
TrailsRouterTest:test_pullAmountAndExecute_WithETH_IncorrectValue() (gas: 27818)
TrailsRouterTest:test_pullAmountAndExecute_WithETH_ShouldTransferAndExecute() (gas: 71312)
TrailsRouterTest:test_pullAmountAndExecute_WithToken_IncorrectValue() (gas: 58165)
TrailsRouterTest:test_pullAmountAndExecute_WithToken_ShouldTransferAndExecute() (gas: 75789)
TrailsRouterTest:test_pullAmountAndExecute_WithValidToken_ShouldTransferAndExecute() (gas: 75490)
TrailsRouterTest:test_pullAmountAndExecute_WithETH_IncorrectValue() (gas: 27815)
TrailsRouterTest:test_pullAmountAndExecute_WithETH_ShouldTransferAndExecute() (gas: 71424)
TrailsRouterTest:test_pullAmountAndExecute_WithToken_IncorrectValue() (gas: 58162)
TrailsRouterTest:test_pullAmountAndExecute_WithToken_ShouldTransferAndExecute() (gas: 75879)
TrailsRouterTest:test_pullAmountAndExecute_WithValidToken_ShouldTransferAndExecute() (gas: 75580)
TrailsRouterTest:test_pullAndExecute_WithETH_NoEthSent() (gas: 18454)
TrailsRouterTest:test_pullAndExecute_WithETH_ShouldTransferAndExecute() (gas: 72439)
TrailsRouterTest:test_pullAndExecute_WithFailingMulticall() (gas: 494404)
TrailsRouterTest:test_pullAndExecute_WithToken_IncorrectValue() (gas: 60552)
TrailsRouterTest:test_pullAndExecute_WithValidToken_ShouldTransferFullBalanceAndExecute() (gas: 77324)
TrailsRouterTest:test_pullAndExecute_WithETH_ShouldTransferAndExecute() (gas: 72551)
TrailsRouterTest:test_pullAndExecute_WithFailingMulticall() (gas: 204398)
TrailsRouterTest:test_pullAndExecute_WithToken_IncorrectValue() (gas: 60549)
TrailsRouterTest:test_pullAndExecute_WithValidToken_ShouldTransferFullBalanceAndExecute() (gas: 77414)
TrailsRouterTest:test_refundAndSweep_erc20_partialRefund() (gas: 110365)
TrailsRouterTest:test_refundAndSweep_native_partialRefund() (gas: 92249)
TrailsRouterTest:test_success_sentinel_not_set() (gas: 15816)
TrailsRouterTest:test_sweep_erc20Token() (gas: 77791)
TrailsRouterTest:test_sweep_nativeToken() (gas: 50071)
TrailsRouterTest:test_validateOpHashAndSweep_native_success() (gas: 62452)
TrailsRouterTest:test_validateOpHashAndSweep_native_success_tstore() (gas: 131007)
TrailsRouterTest:test_validateOpHashAndSweep_native_success() (gas: 62419)
TrailsRouterTest:test_validateOpHashAndSweep_native_success_tstore() (gas: 130945)
TrailsSentinelLibTest:test_Constants_DoNotChange() (gas: 735)
TrailsSentinelLibTest:test_SentinelNamespace_Computation() (gas: 328)
TrailsSentinelLibTest:test_SentinelNamespace_Constant() (gas: 548)
Expand Down
5 changes: 3 additions & 2 deletions script/TrailsRouter.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ contract Deploy is SingletonDeployer {

function deployRouter(uint256 pk) public returns (address) {
bytes32 salt = bytes32(0);
address multicall3 = 0xcA11bde05977b3631167028862bE2a173976CA11;

// Deploy TrailsRouter
bytes memory initCode = type(TrailsRouter).creationCode;
// Deploy TrailsRouter with constructor arguments
bytes memory initCode = abi.encodePacked(type(TrailsRouter).creationCode, abi.encode(multicall3));
address router = _deployIfNotAlready("TrailsRouter", initCode, salt, pk);

return router;
Expand Down
12 changes: 10 additions & 2 deletions src/TrailsRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,18 @@ contract TrailsRouter is IDelegatedExtension, ITrailsRouter, DelegatecallGuard,
using SafeERC20 for IERC20;

// -------------------------------------------------------------------------
// Immutable Variables
// 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.

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 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.

// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------

constructor(address _multicall3) {
MULTICALL3 = _multicall3;
}
Comment on lines +33 to +35
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
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.

// -------------------------------------------------------------------------
// Errors
Expand Down
28 changes: 16 additions & 12 deletions test/TrailsRouter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ contract RevertingReceiver {
}
}

// Mock multicall that always fails (for testing storage conflicts)
contract AlwaysFailingMulticall3 {
function aggregate3(IMulticall3.Call3[] calldata) external payable {
revert("MockMulticall3: forced failure");
}

function aggregate3Value(IMulticall3.Call3Value[] calldata) external payable {
revert("MockMulticall3: forced failure");
}
}

contract MockTarget {
uint256 public lastAmount;
bool public shouldRevert;
Expand Down Expand Up @@ -199,7 +210,7 @@ contract TrailsRouterTest is Test {
MockMulticall3 mockMulticall3 = new MockMulticall3();
vm.etch(0xcA11bde05977b3631167028862bE2a173976CA11, address(mockMulticall3).code);

router = new TrailsRouter();
router = new TrailsRouter(0xcA11bde05977b3631167028862bE2a173976CA11);
getter = new MockSenderGetter();
mockToken = new MockERC20("MockToken", "MTK", 18);
failingToken = new FailingToken();
Expand Down Expand Up @@ -865,17 +876,13 @@ contract TrailsRouterTest is Test {
// Save original multicall code
bytes memory originalCode = 0xcA11bde05977b3631167028862bE2a173976CA11.code;

// Deploy and etch failing multicall
MockMulticall3 failingMulticall = new MockMulticall3();
// Deploy and etch always-failing multicall (doesn't use storage, always reverts)
AlwaysFailingMulticall3 failingMulticall = new AlwaysFailingMulticall3();
vm.etch(0xcA11bde05977b3631167028862bE2a173976CA11, address(failingMulticall).code);

// Verify the etch worked
assertEq(keccak256(0xcA11bde05977b3631167028862bE2a173976CA11.code), keccak256(address(failingMulticall).code));

// Set the failure flag directly in storage since delegatecall uses caller's storage
// The shouldFail variable is at slot 0 in MockMulticall3
vm.store(address(router), bytes32(0), bytes32(uint256(1))); // Set shouldFail = true in router's storage

IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](1);
calls[0] = IMulticall3.Call3({
target: address(getter), allowFailure: false, callData: abi.encodeWithSignature("getSender()")
Expand All @@ -901,13 +908,10 @@ contract TrailsRouterTest is Test {
// Save original multicall code
bytes memory originalCode = 0xcA11bde05977b3631167028862bE2a173976CA11.code;

// Mock multicall3 to return failure
MockMulticall3 failingMulticall = new MockMulticall3();
// Deploy and etch always-failing multicall (doesn't use storage, always reverts)
AlwaysFailingMulticall3 failingMulticall = new AlwaysFailingMulticall3();
vm.etch(0xcA11bde05977b3631167028862bE2a173976CA11, address(failingMulticall).code);

// Set the failure flag directly in storage since delegatecall uses caller's storage
vm.store(address(router), bytes32(0), bytes32(uint256(1))); // Set shouldFail = true in router's storage

IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](1);
calls[0] = IMulticall3.Call3({
target: address(getter), allowFailure: false, callData: abi.encodeWithSignature("getSender()")
Expand Down
4 changes: 2 additions & 2 deletions test/TrailsRouterShim.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ contract TrailsRouterShimTest is Test {

// 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.

address payable recipient = payable(address(0x111));
vm.deal(holder, callValue);
Expand Down Expand Up @@ -309,7 +309,7 @@ contract TrailsRouterShimTest is Test {

// 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.
address payable recipient = payable(address(0x112));
vm.deal(holder, 1 ether);
bytes memory data =
Expand Down
4 changes: 3 additions & 1 deletion test/script/TrailsRouter.s.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ contract TrailsRouterDeploymentTest is Test {

// Expected predetermined address (calculated using CREATE2)
function expectedRouterAddress() internal pure returns (address payable) {
return Create2Utils.calculateCreate2Address(type(TrailsRouter).creationCode, Create2Utils.standardSalt());
address multicall3 = 0xcA11bde05977b3631167028862bE2a173976CA11;
bytes memory initCode = abi.encodePacked(type(TrailsRouter).creationCode, abi.encode(multicall3));
return Create2Utils.calculateCreate2Address(initCode, Create2Utils.standardSalt());
}

// -------------------------------------------------------------------------
Expand Down
4 changes: 3 additions & 1 deletion test/script/TrailsRouterShim.s.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ contract TrailsRouterShimDeploymentTest is Test {

// Expected predetermined addresses (calculated using CREATE2)
function expectedRouterAddress() internal pure returns (address payable) {
return Create2Utils.calculateCreate2Address(type(TrailsRouter).creationCode, Create2Utils.standardSalt());
address multicall3 = 0xcA11bde05977b3631167028862bE2a173976CA11;
bytes memory initCode = abi.encodePacked(type(TrailsRouter).creationCode, abi.encode(multicall3));
return Create2Utils.calculateCreate2Address(initCode, Create2Utils.standardSalt());
}

function expectedShimAddress() internal pure returns (address payable) {
Expand Down