Skip to content

Commit a18740f

Browse files
authored
feat: eraVM Spoke Pool 7702 Handling (#1194)
* feat: eraVM Spoke Pool upgrade Signed-off-by: Faisal Usmani <faisal.of.usmani@gmail.com> * Added tests Signed-off-by: Faisal Usmani <faisal.of.usmani@gmail.com> * forge lib Signed-off-by: Faisal Usmani <faisal.of.usmani@gmail.com> --------- Signed-off-by: Faisal Usmani <faisal.of.usmani@gmail.com>
1 parent 2e3b81f commit a18740f

File tree

5 files changed

+235
-10
lines changed

5 files changed

+235
-10
lines changed

contracts/SpokePool.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1661,7 +1661,7 @@ abstract contract SpokePool is
16611661
* @param account The address to check.
16621662
* @return True if the address is a 7702 delegated wallet, false otherwise.
16631663
*/
1664-
function _is7702DelegatedWallet(address account) internal view returns (bool) {
1664+
function _is7702DelegatedWallet(address account) internal view virtual returns (bool) {
16651665
return bytes3(account.code) == EIP7702_PREFIX;
16661666
}
16671667

contracts/ZkSync_SpokePool.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ contract ZkSync_SpokePool is SpokePool, CircleCCTPAdapter {
127127
* INTERNAL FUNCTIONS *
128128
**************************************/
129129

130+
/**
131+
* @notice Checks if an address is a 7702 delegated wallet (EOA with delegated code).
132+
* @return False Since eraVM does not support 7702 delegated wallets, this function always returns false.
133+
*/
134+
function _is7702DelegatedWallet(address) internal pure override returns (bool) {
135+
return false;
136+
}
137+
130138
/**
131139
* @notice Wraps any ETH into WETH before executing base function. This is necessary because SpokePool receives
132140
* ETH over the canonical token bridge instead of WETH.

foundry.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ fs_permissions = [{ access = "read", path = "./"}]
3434
solc = "0.8.30"
3535
evm_version = "prague"
3636

37+
[profile.zksync]
38+
src = "contracts/Lens_SpokePool.sol"
39+
3740
[profile.zksync.zksync]
3841
compile = true
3942
fallback_oz = true

hardhat.config.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,13 @@ const config: HardhatUserConfig = {
119119
enabled: true,
120120
},
121121
suppressedErrors: ["sendtransfer"],
122-
contractsToCompile: ["SpokePoolPeriphery", "MulticallHandler", "SpokePoolVerifier", "AcrossEventEmitter"],
122+
contractsToCompile: [
123+
"SpokePoolPeriphery",
124+
"MulticallHandler",
125+
"SpokePoolVerifier",
126+
"ZkSync_SpokePool",
127+
"Lens_SpokePool",
128+
],
123129
},
124130
},
125131
networks: {
@@ -268,6 +274,14 @@ const config: HardhatUserConfig = {
268274
browserURL: "https://era.zksync.network/",
269275
},
270276
},
277+
{
278+
network: "lens",
279+
chainId: CHAIN_IDs.LENS,
280+
urls: {
281+
apiURL: "https://verify.lens.xyz/contract_verification",
282+
browserURL: "https://explorer.lens.xyz/",
283+
},
284+
},
271285
],
272286
},
273287
blockscout: {
@@ -289,14 +303,6 @@ const config: HardhatUserConfig = {
289303
browserURL: "https://explorer.inkonchain.com",
290304
},
291305
},
292-
{
293-
network: "lens",
294-
chainId: CHAIN_IDs.LENS,
295-
urls: {
296-
apiURL: "https://verify.lens.xyz/contract_verification",
297-
browserURL: "https://explorer.lens.xyz/",
298-
},
299-
},
300306
{
301307
network: "lisk",
302308
chainId: CHAIN_IDs.LISK,
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
import { Test } from "forge-std/Test.sol";
5+
import { ZkSync_SpokePool, ZkBridgeLike, IERC20, ITokenMessenger } from "../../../../contracts/ZkSync_SpokePool.sol";
6+
import { WETH9 } from "../../../../contracts/external/WETH9.sol";
7+
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
8+
import { AddressToBytes32, Bytes32ToAddress } from "../../../../contracts/libraries/AddressConverters.sol";
9+
import { V3SpokePoolInterface } from "../../../../contracts/interfaces/V3SpokePoolInterface.sol";
10+
11+
// Simple mock contracts for testing
12+
contract SimpleContract {
13+
function doNothing() external pure returns (uint256) {
14+
return 42;
15+
}
16+
}
17+
18+
// Extension of ZkSync_SpokePool to expose internal functions for testing
19+
contract TestableMockSpokePool is ZkSync_SpokePool {
20+
constructor(
21+
address _wrappedNativeTokenAddress
22+
)
23+
ZkSync_SpokePool(
24+
_wrappedNativeTokenAddress,
25+
IERC20(address(0)),
26+
ZkBridgeLike(address(0)),
27+
ITokenMessenger(address(0)),
28+
1 hours,
29+
9 hours
30+
)
31+
{}
32+
33+
function test_unwrapwrappedNativeTokenTo(address payable to, uint256 amount) external {
34+
_unwrapwrappedNativeTokenTo(to, amount);
35+
}
36+
37+
function test_is7702DelegatedWallet(address account) external view returns (bool) {
38+
return _is7702DelegatedWallet(account);
39+
}
40+
41+
function test_fillRelayV3(V3RelayExecutionParams memory relayExecution, bytes32 relayer, bool isSlowFill) external {
42+
_fillRelayV3(relayExecution, relayer, isSlowFill);
43+
}
44+
}
45+
46+
/**
47+
* @title SpokePool EIP-7702 Delegation Tests
48+
* @notice Tests EIP-7702 delegation functionality in SpokePool contract
49+
*/
50+
contract SpokePoolEIP7702Test is Test {
51+
using AddressToBytes32 for address;
52+
using Bytes32ToAddress for bytes32;
53+
54+
TestableMockSpokePool spokePool;
55+
WETH9 weth;
56+
57+
address owner;
58+
address relayer;
59+
address recipient;
60+
61+
uint256 constant WETH_AMOUNT = 1 ether;
62+
uint256 constant CHAIN_ID = 1;
63+
address mockImplementation;
64+
65+
function setUp() public {
66+
weth = new WETH9();
67+
owner = vm.addr(1);
68+
relayer = vm.addr(2);
69+
recipient = makeAddr("recipient");
70+
mockImplementation = makeAddr("mockImplementation");
71+
72+
// Deploy SpokePool
73+
vm.startPrank(owner);
74+
ERC1967Proxy proxy = new ERC1967Proxy(
75+
address(new TestableMockSpokePool(address(weth))),
76+
abi.encodeCall(ZkSync_SpokePool.initialize, (0, ZkBridgeLike(address(0)), owner, makeAddr("hubPool")))
77+
);
78+
spokePool = TestableMockSpokePool(payable(proxy));
79+
vm.stopPrank();
80+
81+
// Fund contracts and accounts
82+
// First give SpokePool some ETH, then deposit it as WETH
83+
deal(address(spokePool), WETH_AMOUNT * 10);
84+
vm.prank(address(spokePool));
85+
weth.deposit{ value: WETH_AMOUNT * 5 }(); // Deposit some of the ETH as WETH for testing
86+
87+
deal(relayer, 10 ether);
88+
deal(recipient, 1 ether);
89+
}
90+
91+
/**
92+
* @dev Creates a test contract to simulate EIP-7702 delegated wallet
93+
* EIP-7702 delegation code must be exactly 23 bytes: 0xef0100 + 20-byte address
94+
*/
95+
function createMockDelegatedWallet() internal returns (address) {
96+
// Create bytecode that starts with EIP-7702 prefix (0xef0100) followed by implementation address
97+
// This creates exactly 23 bytes: 3 bytes prefix + 20 bytes address = 23 bytes
98+
bytes memory delegationCode = abi.encodePacked(bytes3(0xef0100), mockImplementation);
99+
100+
address delegatedWallet = makeAddr("delegatedWallet");
101+
vm.etch(delegatedWallet, delegationCode);
102+
return delegatedWallet;
103+
}
104+
105+
/**
106+
* @dev Creates a regular contract (not delegated)
107+
*/
108+
function createRegularContract() internal returns (address) {
109+
SimpleContract regularContract = new SimpleContract();
110+
return address(regularContract);
111+
}
112+
113+
// Test 1: Verify _is7702DelegatedWallet returns false for EIP-7702 delegated wallets and EOA
114+
function test_is7702DelegatedWallet_ReturnsFalseForDelegatedWallet() public {
115+
address delegatedWallet = createMockDelegatedWallet();
116+
address regularContract = createRegularContract();
117+
address eoa = makeAddr("eoa");
118+
119+
//
120+
assertFalse(
121+
spokePool.test_is7702DelegatedWallet(delegatedWallet),
122+
"Should not detect EIP-7702 delegated wallet"
123+
);
124+
assertFalse(
125+
spokePool.test_is7702DelegatedWallet(regularContract),
126+
"Should not detect regular contract as delegated"
127+
);
128+
assertFalse(spokePool.test_is7702DelegatedWallet(eoa), "Should not detect EOA as delegated");
129+
}
130+
131+
// Test 2: Verify _unwrapwrappedNativeTokenTo sends WETH to regular contracts
132+
function test_unwrapToRegularContract() public {
133+
address regularContract = createRegularContract();
134+
uint256 initialEthBalance = regularContract.balance;
135+
uint256 initialWethBalance = weth.balanceOf(regularContract);
136+
137+
// Sending to regular contract
138+
spokePool.test_unwrapwrappedNativeTokenTo(payable(regularContract), WETH_AMOUNT);
139+
140+
// Should receive WETH, not ETH
141+
assertEq(regularContract.balance, initialEthBalance, "Regular contract should not receive ETH");
142+
assertEq(
143+
weth.balanceOf(regularContract),
144+
initialWethBalance + WETH_AMOUNT,
145+
"Regular contract should receive WETH"
146+
);
147+
}
148+
149+
// Test 3: Verify _unwrapwrappedNativeTokenTo sends ETH to EOAs
150+
function test_unwrapToEOA() public {
151+
address eoa = makeAddr("eoa");
152+
uint256 initialBalance = eoa.balance;
153+
154+
// Send to EOA
155+
spokePool.test_unwrapwrappedNativeTokenTo(payable(eoa), WETH_AMOUNT);
156+
157+
// Should receive ETH, not WETH
158+
assertEq(eoa.balance, initialBalance + WETH_AMOUNT, "EOA should receive ETH");
159+
assertEq(weth.balanceOf(eoa), 0, "EOA should not receive WETH");
160+
}
161+
162+
// Test 4: Test the functionality in context of fill relay operations with mock delegated wallet
163+
// should not receive ETH from fill
164+
function test_fillRelayWithDelegatedRecipient() public {
165+
// Create a mock delegated wallet for the recipient
166+
address delegatedRecipient = createMockDelegatedWallet();
167+
deal(delegatedRecipient, 1 ether); // Give it some initial ETH
168+
169+
uint256 initialEthBalance = delegatedRecipient.balance;
170+
uint256 fillAmount = 0.5 ether;
171+
172+
// Setup a mock relay with delegated recipient
173+
V3SpokePoolInterface.V3RelayExecutionParams memory relayExecution = V3SpokePoolInterface
174+
.V3RelayExecutionParams({
175+
relay: V3SpokePoolInterface.V3RelayData({
176+
depositor: relayer.toBytes32(),
177+
recipient: delegatedRecipient.toBytes32(),
178+
exclusiveRelayer: bytes32(0),
179+
inputToken: address(weth).toBytes32(),
180+
outputToken: address(weth).toBytes32(),
181+
inputAmount: fillAmount,
182+
outputAmount: fillAmount,
183+
originChainId: CHAIN_ID,
184+
depositId: 1,
185+
fillDeadline: uint32(block.timestamp + 1 hours),
186+
exclusivityDeadline: 0,
187+
message: ""
188+
}),
189+
relayHash: keccak256("test"),
190+
updatedOutputAmount: fillAmount,
191+
updatedRecipient: delegatedRecipient.toBytes32(),
192+
updatedMessage: "",
193+
repaymentChainId: CHAIN_ID
194+
});
195+
196+
// Fund the SpokePool with WETH for the slow fill
197+
deal(address(weth), address(spokePool), fillAmount);
198+
199+
// Execute the fill
200+
vm.startPrank(relayer);
201+
spokePool.test_fillRelayV3(relayExecution, relayer.toBytes32(), true);
202+
vm.stopPrank();
203+
204+
// Verify delegated recipient received ETH
205+
assertEq(delegatedRecipient.balance, initialEthBalance, "Delegated recipient should not receive ETH from fill");
206+
assertEq(weth.balanceOf(delegatedRecipient), 0.5 ether, "Delegated recipient should receive WETH");
207+
}
208+
}

0 commit comments

Comments
 (0)