From 508164b230bc69f32c267a0665cedc3361a17523 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Wed, 24 Sep 2025 16:09:02 -0400 Subject: [PATCH 1/3] feat: eraVM Spoke Pool upgrade Signed-off-by: Faisal Usmani --- contracts/SpokePool.sol | 2 +- contracts/ZkSync_SpokePool.sol | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index b9656ddb2..6c7f99fde 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -1637,7 +1637,7 @@ abstract contract SpokePool is * @param account The address to check. * @return True if the address is a 7702 delegated wallet, false otherwise. */ - function _is7702DelegatedWallet(address account) internal view returns (bool) { + function _is7702DelegatedWallet(address account) internal view virtual returns (bool) { return bytes3(account.code) == EIP7702_PREFIX; } diff --git a/contracts/ZkSync_SpokePool.sol b/contracts/ZkSync_SpokePool.sol index 83c51a633..5b6ec56aa 100644 --- a/contracts/ZkSync_SpokePool.sol +++ b/contracts/ZkSync_SpokePool.sol @@ -8,11 +8,7 @@ import "./SpokePool.sol"; // https://github.com/matter-labs/era-contracts/blob/6391c0d7bf6184d7f6718060e3991ba6f0efe4a7/zksync/contracts/bridge/L2ERC20Bridge.sol#L104 interface ZkBridgeLike { - function withdraw( - address _l1Receiver, - address _l2Token, - uint256 _amount - ) external; + function withdraw(address _l1Receiver, address _l2Token, uint256 _amount) external; } interface IL2ETH { @@ -131,6 +127,14 @@ contract ZkSync_SpokePool is SpokePool, CircleCCTPAdapter { * INTERNAL FUNCTIONS * **************************************/ + /** + * @notice Checks if an address is a 7702 delegated wallet (EOA with delegated code). + * @return False Since eraVM does not support 7702 delegated wallets, this function always returns false. + */ + function _is7702DelegatedWallet(address) internal pure override returns (bool) { + return false; + } + /** * @notice Wraps any ETH into WETH before executing base function. This is necessary because SpokePool receives * ETH over the canonical token bridge instead of WETH. From 932b8bbcff43af77f842a8faac4ad2de345c1a87 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Tue, 30 Sep 2025 11:11:59 -0400 Subject: [PATCH 2/3] Added tests Signed-off-by: Faisal Usmani --- foundry.toml | 5 +- hardhat.config.ts | 24 ++- test/evm/foundry/local/eraVM_EIP7702.sol | 208 +++++++++++++++++++++++ 3 files changed, 227 insertions(+), 10 deletions(-) create mode 100644 test/evm/foundry/local/eraVM_EIP7702.sol diff --git a/foundry.toml b/foundry.toml index 752db9948..6a8bfe380 100644 --- a/foundry.toml +++ b/foundry.toml @@ -35,6 +35,9 @@ fs_permissions = [{ access = "read", path = "./"}] solc = "0.8.23" evm_version = "prague" +[profile.zksync] +src = "contracts/Lens_SpokePool.sol" + [profile.zksync.zksync] compile = true fallback_oz = true @@ -47,7 +50,7 @@ base = "${NODE_URL_8453}" bsc = "${NODE_URL_56}" ethereum = "${NODE_URL_1}" ink = "${NODE_URL_57073}" -lens = "${NODE_URL_262}" +lens = "${NODE_URL_232}" linea = "${NODE_URL_59144}" lisk = "${NODE_URL_1135}" mode = "${NODE_URL_34443}" diff --git a/hardhat.config.ts b/hardhat.config.ts index 413e6c672..982069cce 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -129,7 +129,13 @@ const config: HardhatUserConfig = { enabled: true, }, suppressedErrors: ["sendtransfer"], - contractsToCompile: ["SpokePoolPeriphery", "MulticallHandler", "SpokePoolVerifier"], + contractsToCompile: [ + "SpokePoolPeriphery", + "MulticallHandler", + "SpokePoolVerifier", + "ZkSync_SpokePool", + "Lens_SpokePool", + ], }, }, networks: { @@ -245,6 +251,14 @@ const config: HardhatUserConfig = { browserURL: "https://era.zksync.network/", }, }, + { + network: "lens", + chainId: CHAIN_IDs.LENS, + urls: { + apiURL: "https://verify.lens.xyz/contract_verification", + browserURL: "https://explorer.lens.xyz/", + }, + }, ], }, blockscout: { @@ -266,14 +280,6 @@ const config: HardhatUserConfig = { browserURL: "https://explorer.inkonchain.com", }, }, - { - network: "lens", - chainId: CHAIN_IDs.LENS, - urls: { - apiURL: "https://verify.lens.xyz/contract_verification", - browserURL: "https://explorer.lens.xyz/", - }, - }, { network: "lisk", chainId: CHAIN_IDs.LISK, diff --git a/test/evm/foundry/local/eraVM_EIP7702.sol b/test/evm/foundry/local/eraVM_EIP7702.sol new file mode 100644 index 000000000..26017f0a5 --- /dev/null +++ b/test/evm/foundry/local/eraVM_EIP7702.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; +import { ZkSync_SpokePool, ZkBridgeLike, IERC20, ITokenMessenger } from "../../../../contracts/ZkSync_SpokePool.sol"; +import { WETH9 } from "../../../../contracts/external/WETH9.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { AddressToBytes32, Bytes32ToAddress } from "../../../../contracts/libraries/AddressConverters.sol"; +import { V3SpokePoolInterface } from "../../../../contracts/interfaces/V3SpokePoolInterface.sol"; + +// Simple mock contracts for testing +contract SimpleContract { + function doNothing() external pure returns (uint256) { + return 42; + } +} + +// Extension of ZkSync_SpokePool to expose internal functions for testing +contract TestableMockSpokePool is ZkSync_SpokePool { + constructor( + address _wrappedNativeTokenAddress + ) + ZkSync_SpokePool( + _wrappedNativeTokenAddress, + IERC20(address(0)), + ZkBridgeLike(address(0)), + ITokenMessenger(address(0)), + 1 hours, + 9 hours + ) + {} + + function test_unwrapwrappedNativeTokenTo(address payable to, uint256 amount) external { + _unwrapwrappedNativeTokenTo(to, amount); + } + + function test_is7702DelegatedWallet(address account) external view returns (bool) { + return _is7702DelegatedWallet(account); + } + + function test_fillRelayV3(V3RelayExecutionParams memory relayExecution, bytes32 relayer, bool isSlowFill) external { + _fillRelayV3(relayExecution, relayer, isSlowFill); + } +} + +/** + * @title SpokePool EIP-7702 Delegation Tests + * @notice Tests EIP-7702 delegation functionality in SpokePool contract + */ +contract SpokePoolEIP7702Test is Test { + using AddressToBytes32 for address; + using Bytes32ToAddress for bytes32; + + TestableMockSpokePool spokePool; + WETH9 weth; + + address owner; + address relayer; + address recipient; + + uint256 constant WETH_AMOUNT = 1 ether; + uint256 constant CHAIN_ID = 1; + address mockImplementation; + + function setUp() public { + weth = new WETH9(); + owner = vm.addr(1); + relayer = vm.addr(2); + recipient = makeAddr("recipient"); + mockImplementation = makeAddr("mockImplementation"); + + // Deploy SpokePool + vm.startPrank(owner); + ERC1967Proxy proxy = new ERC1967Proxy( + address(new TestableMockSpokePool(address(weth))), + abi.encodeCall(ZkSync_SpokePool.initialize, (0, ZkBridgeLike(address(0)), owner, makeAddr("hubPool"))) + ); + spokePool = TestableMockSpokePool(payable(proxy)); + vm.stopPrank(); + + // Fund contracts and accounts + // First give SpokePool some ETH, then deposit it as WETH + deal(address(spokePool), WETH_AMOUNT * 10); + vm.prank(address(spokePool)); + weth.deposit{ value: WETH_AMOUNT * 5 }(); // Deposit some of the ETH as WETH for testing + + deal(relayer, 10 ether); + deal(recipient, 1 ether); + } + + /** + * @dev Creates a test contract to simulate EIP-7702 delegated wallet + * EIP-7702 delegation code must be exactly 23 bytes: 0xef0100 + 20-byte address + */ + function createMockDelegatedWallet() internal returns (address) { + // Create bytecode that starts with EIP-7702 prefix (0xef0100) followed by implementation address + // This creates exactly 23 bytes: 3 bytes prefix + 20 bytes address = 23 bytes + bytes memory delegationCode = abi.encodePacked(bytes3(0xef0100), mockImplementation); + + address delegatedWallet = makeAddr("delegatedWallet"); + vm.etch(delegatedWallet, delegationCode); + return delegatedWallet; + } + + /** + * @dev Creates a regular contract (not delegated) + */ + function createRegularContract() internal returns (address) { + SimpleContract regularContract = new SimpleContract(); + return address(regularContract); + } + + // Test 1: Verify _is7702DelegatedWallet returns false for EIP-7702 delegated wallets and EOA + function test_is7702DelegatedWallet_ReturnsFalseForDelegatedWallet() public { + address delegatedWallet = createMockDelegatedWallet(); + address regularContract = createRegularContract(); + address eoa = makeAddr("eoa"); + + // + assertFalse( + spokePool.test_is7702DelegatedWallet(delegatedWallet), + "Should not detect EIP-7702 delegated wallet" + ); + assertFalse( + spokePool.test_is7702DelegatedWallet(regularContract), + "Should not detect regular contract as delegated" + ); + assertFalse(spokePool.test_is7702DelegatedWallet(eoa), "Should not detect EOA as delegated"); + } + + // Test 2: Verify _unwrapwrappedNativeTokenTo sends WETH to regular contracts + function test_unwrapToRegularContract() public { + address regularContract = createRegularContract(); + uint256 initialEthBalance = regularContract.balance; + uint256 initialWethBalance = weth.balanceOf(regularContract); + + // Sending to regular contract + spokePool.test_unwrapwrappedNativeTokenTo(payable(regularContract), WETH_AMOUNT); + + // Should receive WETH, not ETH + assertEq(regularContract.balance, initialEthBalance, "Regular contract should not receive ETH"); + assertEq( + weth.balanceOf(regularContract), + initialWethBalance + WETH_AMOUNT, + "Regular contract should receive WETH" + ); + } + + // Test 3: Verify _unwrapwrappedNativeTokenTo sends ETH to EOAs + function test_unwrapToEOA() public { + address eoa = makeAddr("eoa"); + uint256 initialBalance = eoa.balance; + + // Send to EOA + spokePool.test_unwrapwrappedNativeTokenTo(payable(eoa), WETH_AMOUNT); + + // Should receive ETH, not WETH + assertEq(eoa.balance, initialBalance + WETH_AMOUNT, "EOA should receive ETH"); + assertEq(weth.balanceOf(eoa), 0, "EOA should not receive WETH"); + } + + // Test 4: Test the functionality in context of fill relay operations with mock delegated wallet + // should not receive ETH from fill + function test_fillRelayWithDelegatedRecipient() public { + // Create a mock delegated wallet for the recipient + address delegatedRecipient = createMockDelegatedWallet(); + deal(delegatedRecipient, 1 ether); // Give it some initial ETH + + uint256 initialEthBalance = delegatedRecipient.balance; + uint256 fillAmount = 0.5 ether; + + // Setup a mock relay with delegated recipient + V3SpokePoolInterface.V3RelayExecutionParams memory relayExecution = V3SpokePoolInterface + .V3RelayExecutionParams({ + relay: V3SpokePoolInterface.V3RelayData({ + depositor: relayer.toBytes32(), + recipient: delegatedRecipient.toBytes32(), + exclusiveRelayer: bytes32(0), + inputToken: address(weth).toBytes32(), + outputToken: address(weth).toBytes32(), + inputAmount: fillAmount, + outputAmount: fillAmount, + originChainId: CHAIN_ID, + depositId: 1, + fillDeadline: uint32(block.timestamp + 1 hours), + exclusivityDeadline: 0, + message: "" + }), + relayHash: keccak256("test"), + updatedOutputAmount: fillAmount, + updatedRecipient: delegatedRecipient.toBytes32(), + updatedMessage: "", + repaymentChainId: CHAIN_ID + }); + + // Fund the SpokePool with WETH for the slow fill + deal(address(weth), address(spokePool), fillAmount); + + // Execute the fill + vm.startPrank(relayer); + spokePool.test_fillRelayV3(relayExecution, relayer.toBytes32(), true); + vm.stopPrank(); + + // Verify delegated recipient received ETH + assertEq(delegatedRecipient.balance, initialEthBalance, "Delegated recipient should not receive ETH from fill"); + assertEq(weth.balanceOf(delegatedRecipient), 0.5 ether, "Delegated recipient should receive WETH"); + } +} From dbb9e2087baeaf117bc5cf79f3eedd64cad7f868 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Tue, 25 Nov 2025 09:31:37 -0500 Subject: [PATCH 3/3] forge lib Signed-off-by: Faisal Usmani --- lib/forge-std | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/forge-std b/lib/forge-std index 6bce1540c..8e40513d6 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 6bce1540c7a5d1c40eec032a1ae16f0e01f82b92 +Subproject commit 8e40513d678f392f398620b3ef2b418648b33e89